1. Overview

The Non-Operational Days System is a comprehensive solution designed to manage and track off days for both sites (physical locations) and users (employees) within an organization. The system consists of two primary microservices: api-sites for managing site-level off days and api-users for managing individual user off days. These services work in tandem to provide a complete off-day management solution with real-time synchronization capabilities.

Key Features

  • Dual Management: Separate handling of site-wide closures and individual user time-off
  • Real-time Synchronization: PubSub integration for immediate updates across systems
  • Flexible Querying: Advanced filtering and reporting capabilities
  • Scalable Architecture: Microservices design with MongoDB persistence
  • Security: JWT-based authentication and role-based [access control](../Settings/Access control/AccessControlOverview.md)

2. API Endpoints Reference

2.1. Site Off Days API (api-sites)

MethodEndpointDescriptionAuthenticationValidation
GET/siteOffdays/:siteIDRetrieves off days for a specific siteJWT RequiredNone
PUT/siteOffdays/:siteIDCreates or updates off days for a siteJWT RequiredBody validation

2.2. User Off Days API (api-users)

MethodEndpointDescriptionAuthenticationQuery Parameters
GET/userOffDay/Retrieves all user off days for the organizationJWT RequiredNone
GET/userOffDay/countGets statistical counts of user off daysJWT RequiredNone
GET/userOffDay/upcomingRetrieves upcoming scheduled off daysJWT RequiredNone
GET/userOffDay/userOffDaysDataAdvanced query for user off days with filteringJWT RequiredMultiple filters
GET/userOffDay/userOffDaysPerMonthGets user off days for a specific monthJWT RequiredyearMonth
POST/userOffDay/Creates or updates user off daysJWT RequiredBody validation

3. Libraries and Dependencies

Core Framework Libraries

  • Express.js: Web application framework for Node.js
  • TypeScript: Type-safe JavaScript superset for enhanced development
  • Mongoose: MongoDB object modeling for Node.js

Authentication & Security

  • jsonwebtoken: JWT token generation and verification
  • @nimbly-technologies/nimbly-backend-utils: Custom authentication middleware
  • cors: Cross-Origin Resource Sharing middleware

Validation & Data Processing

  • Joi: Schema validation library
  • Validator: Custom validation wrapper from nimbly-backend-utils

Database & Caching

  • MongoDB: Primary data persistence layer
  • Redis: Caching layer for performance optimization
  • @nimbly-technologies/entity-node: Database repository implementations

Communication & Integration

  • Axios: HTTP client for external API calls
  • PubSub: Event-driven messaging for system integration

Logging & Monitoring

  • Winston: Comprehensive logging solution
  • @nimbly-technologies/nimbly-common: Shared logging utilities

Development & Testing

  • Nodemon: Development server with auto-reload
  • Babel: JavaScript transpilation
  • Jest: Testing framework

4. Data Models and Schemas

4.1. Site Off Day Model

interface SiteOffdayMongo {
    siteID: string;              // Unique identifier for the site
    dates: string[];             // Array of date strings (YYYY-MM-DD format)
    disabled: boolean;           // Flag to disable off days
    organizationID: string;      // Organization identifier
    updatedAt: number;           // Unix timestamp of last update
    updatedBy: string;           // User ID who made the last update
}

4.2. User Off Day Model

interface UserOffdayMongo {
    userID: string;              // Unique identifier for the user
    dates: string[];             // Array of date strings (YYYY-MM-DD format)
    disabled: boolean;           // Flag to disable off days
    organizationID: string;      // Organization identifier
    updatedAt: number;           // Unix timestamp of last update
    updatedBy: string;           // User ID who made the last update
}

4.3. Database Indexing Strategy

Both collections utilize compound indexes for optimal query performance:

Site Off Days Indexes

  1. {organizationID: 1, siteID: 1} - Primary lookup index
  2. {organizationID: 1, siteID: 1, disabled: 1} - Status filtering
  3. {organizationID: 1, siteID: 1, dates: 1} - Date-based queries

User Off Days Indexes

  1. {organizationID: 1, userID: 1} - Primary lookup index
  2. {organizationID: 1, userID: 1, disabled: 1} - Status filtering
  3. {organizationID: 1, userID: 1, dates: 1} - Date-based queries

5. Implementation Details

5.1. Request Processing Flow

sequenceDiagram
    participant Client
    participant Router
    participant Middleware
    participant Controller
    participant UseCase
    participant Repository
    participant MongoDB
    participant PubSub
    
    Client->>Router: HTTP Request
    Router->>Middleware: JWT Validation
    Middleware->>Middleware: Extract User Context
    Middleware->>Controller: Validated Request
    Controller->>UseCase: Business Logic
    UseCase->>Repository: Data Operation
    Repository->>MongoDB: Query/Update
    MongoDB-->>Repository: Result
    Repository-->>UseCase: Data
    UseCase->>PubSub: Sync Event
    PubSub-->>UseCase: Confirmation
    UseCase-->>Controller: Response Data
    Controller-->>Client: HTTP Response

5.2. Site Off Days Update Flow

The site off days update process follows a sophisticated pattern to ensure data consistency and proper synchronization:

  1. Validation Phase

    • Request validation using Joi schema
    • JWT token verification
    • User permission checks
  2. Existence Check

    • Query MongoDB for existing site off days
    • Determine create vs update operation
  3. Data Transformation

    • Convert date object format to array format
    • Calculate added and removed dates
    • Prepare synchronization payload
  4. Persistence Phase

    • Create new document or update existing
    • Set audit fields (updatedAt, updatedBy)
  5. Synchronization Phase

    • Publish changes to PubSub topic
    • Include detailed change information

5.3. User Off Days Batch Update Flow

The user off days system supports batch updates with complex processing:

flowchart TD
    A[Start Batch Update] --> B[Parse User IDs Array]
    B --> C{For Each User}
    C --> D[Check Existing Record]
    D --> E{Record Exists?}
    E -->|No| F[Create New Record]
    E -->|Yes| G[Calculate Differences]
    F --> H[Add to Create Queue]
    G --> I[Add to Update Queue]
    H --> J{More Users?}
    I --> J
    J -->|Yes| C
    J -->|No| K[Execute All Operations]
    K --> L[Publish Sync Events]
    L --> M[Return Success]

5.4. Complex Query Implementation

The userOffDaysData endpoint implements a sophisticated aggregation pipeline:

  1. Base Filtering

    • Organization ID restriction
    • Date range filtering if provided
  2. Type-Based Processing

    • Date Type: Groups users by date
    • User Type: Groups dates by user
  3. Department/Site Filtering

    • Join with users collection
    • Filter by department assignments
    • Filter by site assignments
  4. Search Implementation

    • Text search on user display names
    • Case-insensitive matching
  5. Pagination & Sorting

    • Dynamic sort field selection
    • Configurable page size
    • Total count calculation

6. Business Logic Flows

6.1. Site Off Days Management Flow

The site off days management system handles the complete lifecycle of site closures:

stateDiagram-v2
    [*] --> RequestReceived
    RequestReceived --> ValidateRequest
    ValidateRequest --> CheckExistence
    
    CheckExistence --> CreateNew: Record Not Found
    CheckExistence --> UpdateExisting: Record Found
    
    CreateNew --> PrepareData
    UpdateExisting --> CalculateChanges
    
    PrepareData --> SaveToDatabase
    CalculateChanges --> SaveToDatabase
    
    SaveToDatabase --> PublishEvent
    PublishEvent --> [*]
    
    note right of CalculateChanges
        Identifies:
        - Added dates
        - Removed dates
        - Status changes
    end note
    
    note right of PublishEvent
        Sync with:
        - Statistics service
        - Scheduling service
        - Notification service
    end note

6.2. Detailed Function Flows

Site Off Days Functions

1. GET /siteOffdays/:siteID - Find Site Off Days
flowchart TD
    A[GET Request to /siteOffdays/:siteID] --> B[Router: siteOffdays.router.ts]
    B --> C[AuthMiddleware.expressHandler]
    C --> D{Valid JWT?}
    D -->|No| E[Return 401 Unauthorized]
    D -->|Yes| F[Extract User Context]
    F --> G[SiteOffdaysController.findOneBySiteID]
    G --> H[SiteOffdaysUsecase.findOneBySiteID]
    H --> I[Build Query Object]
    I --> J[SiteOffdayMongoRepository.findOneByQuery]
    J --> K[MongoDB Query Execution]
    K --> L{Record Found?}
    L -->|No| M[Return null]
    L -->|Yes| N[Return SiteOffdayMongo Object]
    N --> O[Controller Response Handler]
    O --> P[Return HTTP 200 with Data]

Function Implementation Details:

// Controller: siteOffdaysController.ts
public async findOneBySiteID({ context, payload }: FindOneBySiteIDParams) {
    try {
        // Extract siteID from route params
        const siteID = payload.params.siteID;
        
        // Call use case with context and siteID
        const siteOffdays = await this.siteOffdaysUsecase.findOneBySiteID(
            context, 
            siteID
        );
        
        // Return successful response
        return response(siteOffdays, null);
    } catch (error) {
        log.error('findOneBySiteID', error);
        return response(null, error.code || errors.INTERNAL);
    }
}
 
// Use Case: siteOffdays.usecase.ts
public async findOneBySiteID(ctx: Context<UserAuth>, siteID: string): Promise<SiteOffdayMongo | null> {
    // Build query with organization isolation
    const defaultQuery = {
        organizationID: ctx.user.organizationID,
        siteID: siteID,
    };
 
    // Execute repository query
    let siteOffDay = await this.siteOffdaysRepo.findOneByQuery(defaultQuery);
 
    if (!siteOffDay) {
        return null;
    }
 
    return siteOffDay;
}
2. PUT /siteOffdays/:siteID - Upsert Site Off Days
flowchart TD
    A[PUT Request to /siteOffdays/:siteID] --> B[Router: siteOffdays.router.ts]
    B --> C[AuthMiddleware.expressHandler]
    C --> D[ValidatorHandler - Joi Validation]
    D --> E{Valid Request Body?}
    E -->|No| F[Return 400 Bad Request]
    E -->|Yes| G[SiteOffdaysController.upsert]
    G --> H[SiteOffdaysUsecase.upsert]
    H --> I[Check Existing Record]
    I --> J{Record Exists?}
    J -->|No| K[Create New Flow]
    J -->|Yes| L[Update Existing Flow]
    
    K --> M[Prepare New Document]
    M --> N[Calculate Added Dates]
    N --> O[Create Sync Request]
    O --> P[SiteOffdayMongoRepository.create]
    
    L --> Q[Calculate Date Differences]
    Q --> R[Identify Added/Removed Dates]
    R --> S[Create Sync Request]
    S --> T[SiteOffdayMongoRepository.update]
    
    P --> U[Publish to PubSub]
    T --> U
    U --> V[Return Success Response]

Detailed Upsert Function Implementation:

// Use Case: siteOffdays.usecase.ts
public async upsert(ctx: Context<UserAuth>, siteID: string, data: Partial<SiteOffdayMongo>): Promise<string> {
    const { organizationID, userID } = ctx.user;
    
    // Check if site off days already exist
    const checkSiteOffdays = await this.siteOffdaysRepo.findOneByQuery({
        organizationID,
        siteID,
    });
 
    // Initialize sync request for PubSub
    const scheduleStatisticPubsubRequest: SyncOffDaysRequest = {
        organizations: {
            addedDates: [],
            removedDates: [],
        },
        sites: [],
    };
    
    // CREATE FLOW
    if (!checkSiteOffdays) {
        // Prepare new document
        const newSiteOffdays = new SiteOffdayMongo({
            ...data,
            organizationID,
            siteID,
            updatedAt: new Date().getTime(),
            updatedBy: userID,
        });
 
        // Prepare sync request
        let siteRequest: SiteOffDaysDetail = {
            siteID,
            removedDates: [],
            addedDates: [],
        };
        
        if (!newSiteOffdays.disabled) {
            siteRequest.addedDates = newSiteOffdays.dates;
            siteRequest.isDisabled = false;
        } else {
            siteRequest.isDisabled = true;
        }
        
        scheduleStatisticPubsubRequest.sites.push(siteRequest);
        
        // Publish event before saving
        await this.pubsubRepo.publish(ctx, 'scheduleStatistic:syncOffDays', scheduleStatisticPubsubRequest);
        
        // Save to database
        await this.siteOffdaysRepo.create(newSiteOffdays);
    } else {
        // UPDATE FLOW
        const updatedSiteOffdays = {
            ...data,
            updatedAt: new Date().getTime(),
            updatedBy: userID,
        };
 
        const siteRequest: SiteOffDaysDetail = {
            siteID,
            addedDates: [],
            removedDates: [],
        };
 
        // Check if disabled status changed
        if (checkSiteOffdays.disabled !== updatedSiteOffdays.disabled) {
            siteRequest.isDisabled = updatedSiteOffdays.disabled;
        }
 
        // Calculate date differences
        if (updatedSiteOffdays.dates && checkSiteOffdays.dates) {
            const addedDates = (updatedSiteOffdays.dates || []).filter(
                (date) => !checkSiteOffdays.dates.includes(date)
            );
            const removedDates = checkSiteOffdays.dates.filter(
                (date) => !(updatedSiteOffdays.dates || []).includes(date)
            );
            siteRequest.addedDates = addedDates;
            siteRequest.removedDates = removedDates;
        }
 
        scheduleStatisticPubsubRequest.sites.push(siteRequest);
 
        // Update database
        await this.siteOffdaysRepo.update(
            { organizationID, siteID }, 
            { $set: updatedSiteOffdays }
        );
        
        // Publish sync event
        await this.pubsubRepo.publish(ctx, 'scheduleStatistic:syncOffDays', scheduleStatisticPubsubRequest);
    }
 
    return siteID;
}

User Off Days Functions

1. GET /userOffDay/count - Get Statistical Counts
flowchart TD
    A[GET Request to /userOffDay/count] --> B[UserOffDayController.getUserOffDayCount]
    B --> C[UserOffDayUsecase.getUserOffDayCount]
    C --> D[Calculate Today's Date]
    D --> E[Query Today Count]
    E --> F[Calculate Week Range]
    F --> G[Query Week Count]
    G --> H[Calculate Month Range]
    H --> I[Query Month Count]
    I --> J[Calculate Year Range]
    J --> K[Query Year Count]
    K --> L[Aggregate Results]
    L --> M[Return Statistics Object]

Statistical Count Implementation:

// Use Case: userOffDay.usecase.ts
public async getUserOffDayCount(ctx: Context<UserAuth>): Promise<any> {
    // Today's Date
    const today = new Date();
    const year = today.getFullYear();
    const month = today.getMonth() + 1;
    const day = today.getDate();
    const formattedDate = `${year}-${month.toString().padStart(2, "0")}-${day
        .toString()
        .padStart(2, "0")}`;
    
    // Count users off today
    const todayCount = await this.modelUserOffDays.countDocuments({
        dates: formattedDate,
        organizationID: ctx.user.organizationID,
    });
 
    // Calculate current week range (Monday to Sunday)
    const { monday, sunday } = getMondayAndNextSunday();
    const weekCount = await this.modelUserOffDays
        .find({
            dates: {
                $elemMatch: {
                    $gte: monday.toISOString().split("T")[0],
                    $lte: sunday.toISOString().split("T")[0],
                },
            },
            organizationID: ctx.user.organizationID,
        })
        .count();
 
    // Calculate current month range
    const { startOfMonth, endOfMonth } = getCurrentMonthStartAndEnd();
    const monthCount = await this.modelUserOffDays
        .find({
            dates: {
                $elemMatch: {
                    $gte: startOfMonth.toISOString().split("T")[0],
                    $lte: endOfMonth.toISOString().split("T")[0],
                },
            },
            organizationID: ctx.user.organizationID,
        })
        .count();
 
    // Calculate current year range
    const { start, end } = getCurrentYearStartAndEnd();
    const yearCount = await this.modelUserOffDays
        .find({
            dates: {
                $elemMatch: {
                    $gte: start.toISOString().split("T")[0],
                    $lte: end.toISOString().split("T")[0],
                },
            },
            organizationID: ctx.user.organizationID,
        })
        .count();
 
    return {
        today: todayCount,
        week: weekCount,
        month: monthCount,
        year: yearCount,
    };
}
2. POST /userOffDay - Batch Upsert User Off Days
flowchart TD
    A[POST Request to /userOffDay] --> B[UserOffDayController.upsertUserOffDay]
    B --> C[UserOffDayUsecase.upsertUserOffDay]
    C --> D[Parse userIDs Array]
    D --> E[Load Existing User Off Days]
    E --> F{For Each User}
    F --> G[Extract User ID and Dates]
    G --> H{User Exists?}
    H -->|No| I[Create New User Off Day]
    H -->|Yes| J[Update Existing Off Day]
    I --> K[Build Create Operation]
    J --> L[Calculate Date Differences]
    L --> M[Build Update Operation]
    K --> N[Add to Batch Operations]
    M --> N
    N --> O{More Users?}
    O -->|Yes| F
    O -->|No| P[Execute All Operations]
    P --> Q[Publish Sync Events]
    Q --> R[Return Success]

Batch Upsert Implementation:

// Use Case: userOffDay.usecase.ts
public async upsertUserOffDay(
    ctx: Context<UserAuth>,
    data: { userIDs: [{ string: string[] }] }
): Promise<string> {
    const { organizationID } = ctx.user;
    const newUserOffDays = data.userIDs || [];
 
    // Load all existing user off days for organization
    const userOffDay: UserOffdayMongo[] =
        await this.userOffDayRepo.findByOrganizationID(organizationID, {});
 
    const scheduleStatisticPubsubRequest: SyncUserOffDaysRequest = {
        users: [],
    };
    const userOffDaysPromise: Promise<any>[] = [];
    
    // Process each user
    for (const checkUserOffDay of newUserOffDays) {
        const keys = Object.keys(checkUserOffDay);
        if (!keys) continue;
        
        const userID = keys[0];
        const newDates = checkUserOffDay[keys[0]];
        
        // CREATE FLOW
        if (!userOffDay.some((obj) => obj.userID === userID)) {
            const newUserOffDay: UserOffdayMongo = {
                disabled: false,
                organizationID: ctx.user.organizationID,
                updatedAt: new Date().getTime(),
                updatedBy: ctx.user.userID,
                userID: userID,
                dates: newDates,
            };
 
            let userRequest: UserOffDaysDetail = {
                userID: userID,
                removedDates: [],
                addedDates: [],
            };
            
            if (!newUserOffDay.disabled) {
                userRequest.addedDates = newUserOffDay.dates;
                userRequest.isDisabled = false;
            } else {
                userRequest.isDisabled = true;
            }
            
            scheduleStatisticPubsubRequest.users.push(userRequest);
            userOffDaysPromise.push(this.userOffDayRepo.create(newUserOffDay));
        } else {
            // UPDATE FLOW
            const updatedUserOffdays = {
                disabled: false,
                updatedAt: new Date().getTime(),
                organizationID: ctx.user.organizationID,
                userID: userID,
                updatedBy: ctx.user.userID,
                dates: newDates,
            };
 
            const selectedUserOffDay = userOffDay.find(
                (obj) => obj.userID === userID
            );
 
            const userRequest: UserOffDaysDetail = {
                userID: userID,
                addedDates: [],
                removedDates: [],
            };
 
            const oldDates = selectedUserOffDay.dates;
 
            // Calculate date differences
            if (newDates && oldDates) {
                const addedDates = (newDates || []).filter(
                    (date) => !oldDates.includes(date)
                );
                const removedDates = oldDates.filter(
                    (date) => !(newDates || []).includes(date)
                );
                userRequest.addedDates = addedDates;
                userRequest.removedDates = removedDates;
            }
 
            scheduleStatisticPubsubRequest.users.push(userRequest);
 
            userOffDaysPromise.push(
                this.userOffDayRepo.update(
                    { organizationID, userID: userID },
                    { $set: updatedUserOffdays }
                )
            );
        }
    }
 
    // Execute all database operations
    if (userOffDaysPromise.length) {
        try {
            await Promise.all(userOffDaysPromise);
        } catch (error) {
            log.error(
                `[DEBUG] Error adding to userOffDays, errorMsg:${error}`
            );
        }
    }
    
    // Publish sync events
    if (scheduleStatisticPubsubRequest.users.length) {
        try {
            await this.pubsubRepo.publish(
                ctx,
                "scheduleStatistic:syncUserOffDays",
                scheduleStatisticPubsubRequest
            );
        } catch (error) {
            log.error(
                `[DEBUG] Error publishing to scheduleStatistic:syncUserOffDays`
            );
        }
    }
 
    return "SUCCESS";
}
3. GET /userOffDay/upcoming - Get Upcoming Schedules
flowchart TD
    A[GET Request to /userOffDay/upcoming] --> B[UserOffDayController.upcomingSchedules]
    B --> C[UserOffDayUsecase.upcomingSchedules]
    C --> D[Calculate Today's Date]
    D --> E[Query Today's Off Users]
    E --> F[Get User Display Names]
    F --> G[Calculate This Week Range]
    G --> H[Query This Week's Off Users]
    H --> I[Get User Display Names]
    I --> J[Calculate Next Week Range]
    J --> K[Query Next Week's Off Users]
    K --> L[Get User Display Names]
    L --> M[Aggregate Results by Period]
    M --> N[Return Upcoming Schedules]

Upcoming Schedules Implementation:

// Use Case: userOffDay.usecase.ts
public async upcomingSchedules(ctx: Context<UserAuth>): Promise<any> {
    const todayDate = new Date();
    const year = todayDate.getFullYear();
    const month = todayDate.getMonth() + 1;
    const day = todayDate.getDate();
    const formattedDateFilter = `${year}-${month
        .toString()
        .padStart(2, "0")}-${day.toString().padStart(2, "0")}`;
 
    // TODAY'S OFF USERS
    const userIDs: string[] = [];
    const users: string[] = [];
    
    // Find users off today
    const upcomingOffUserID = this.modelUserOffDays.find(
        { 
            organizationID: ctx.user.organizationID, 
            dates: formattedDateFilter 
        },
        { projection: { userID: 1, _id: 0 } }
    );
    
    await upcomingOffUserID.forEach((user: any) => {
        userIDs.push(user.userID);
    });
 
    // Get user display names
    const upcomingOffUser = this.modelUsers.find(
        { userID: { $in: userIDs } },
        { projection: { displayName: 1, _id: 0 } }
    );
    
    await upcomingOffUser.forEach((user: any) => {
        users.push(user.displayName);
    });
 
    // THIS WEEK'S OFF USERS
    const weekUserIDs: string[] = [];
    const weekUsers: string[] = [];
    const { sunday } = getMondayAndNextSunday();
    
    const upcomingWeekOffUserID = this.modelUserOffDays.find(
        {
            organizationID: ctx.user.organizationID,
            dates: {
                $elemMatch: {
                    $gte: formattedDateFilter,
                    $lte: sunday.toISOString().split("T")[0],
                },
            },
        },
        { projection: { userID: 1, _id: 0 } }
    );
    
    await upcomingWeekOffUserID.forEach((user: any) => {
        weekUserIDs.push(user.userID);
    });
 
    const upcomingWeekOffUser = this.modelUsers.find(
        { userID: { $in: weekUserIDs } },
        { projection: { displayName: 1, _id: 0 } }
    );
    
    await upcomingWeekOffUser.forEach((user: any) => {
        weekUsers.push(user.displayName);
    });
 
    // NEXT WEEK'S OFF USERS
    const nextWeekUserIDs: string[] = [];
    const nextWeekUsers: string[] = [];
    const { nextWeekStart, nextWeekEnd } = getNextWeekStartAndEnd();
    
    const upcomingNextWeekOffUserID = this.modelUserOffDays.find(
        {
            organizationID: ctx.user.organizationID,
            dates: {
                $elemMatch: {
                    $gte: nextWeekStart.toISOString().split("T")[0],
                    $lte: nextWeekEnd.toISOString().split("T")[0],
                },
            },
        },
        { projection: { userID: 1, _id: 0 } }
    );
    
    await upcomingNextWeekOffUserID.forEach((user: any) => {
        nextWeekUserIDs.push(user.userID);
    });
 
    const upcomingNextWeekOffUser = this.modelUsers.find(
        { userID: { $in: nextWeekUserIDs } },
        { projection: { displayName: 1, _id: 0 } }
    );
    
    await upcomingNextWeekOffUser.forEach((user: any) => {
        nextWeekUsers.push(user.displayName);
    });
 
    return { 
        today: users, 
        thisWeek: weekUsers, 
        nextWeek: nextWeekUsers 
    };
}

6.3. User Off Days Statistical Analysis

The system provides comprehensive statistical analysis across multiple time periods:

  1. Daily Statistics

    • Count of users off today
    • Real-time updates
  2. Weekly Statistics

    • Current week analysis (Monday-Sunday)
    • Trending patterns
  3. Monthly Statistics

    • Full month coverage
    • Department-wise breakdown
  4. Annual Statistics

    • Year-to-date analysis
    • Historical comparisons
4. GET /userOffDay/userOffDaysData - Advanced Query with Filters
flowchart TD
    A[GET Request with Query Params] --> B[UserOffDayController.userOffDaysData]
    B --> C[Extract Query Parameters]
    C --> D[UserOffDayUsecase.userOffDaysData]
    D --> E{Filter Type?}
    E -->|Date| F[Date-Based Aggregation]
    E -->|User| G[User-Based Aggregation]
    
    F --> H[Group Users by Date]
    G --> I[Group Dates by User]
    
    H --> J[Apply Department Filter]
    I --> J
    J --> K[Apply Site Filter]
    K --> L[Apply Date Range Filter]
    L --> M[Apply Search Filter]
    M --> N[Apply Pagination]
    N --> O[Sort Results]
    O --> P[Return Filtered Data]

Advanced Query Implementation:

// Use Case: userOffDay.usecase.ts (partial)
public async userOffDaysData(
    ctx: Context<UserAuth>,
    options: UserOffDayOptions
): Promise<any> {
    let pipeline = [];
    const site = options.site;
    const department = options.department;
 
    // Filter By Date - Groups users by each date
    if (options.type === "date") {
        pipeline = [
            {
                $match: {
                    organizationID: ctx.user.organizationID,
                },
            },
            {
                $unwind: "$dates",
            },
            {
                $group: {
                    _id: { date: "$dates" },
                    userID: { $addToSet: "$userID" },
                },
            },
        ];
 
        // Date range filter
        if (options.startDate && options.endDate) {
            pipeline.splice(2, 0, {
                $match: {
                    dates: {
                        $gte: options.startDate,
                        $lte: options.endDate,
                    },
                },
            });
        }
 
        // Specific dates filter
        if (options.dates && options.dates.length > 0) {
            pipeline.splice(2, 0, {
                $match: {
                    dates: { $in: options.dates },
                },
            });
        }
 
        // Enrich with user details
        pipeline.push({
            $lookup: {
                from: "users",
                localField: "userID",
                foreignField: "userID",
                as: "userDetails",
            },
        });
 
        // Apply department/site filters
        if (department && department.length > 0) {
            pipeline.push({
                $addFields: {
                    userDetails: {
                        $filter: {
                            input: "$userDetails",
                            as: "user",
                            cond: {
                                $in: ["$$user.department", department],
                            },
                        },
                    },
                },
            });
        }
 
        if (site && site.length > 0) {
            pipeline.push({
                $addFields: {
                    userDetails: {
                        $filter: {
                            input: "$userDetails",
                            as: "user",
                            cond: {
                                $in: ["$$user.site", site],
                            },
                        },
                    },
                },
            });
        }
 
        // Apply search filter
        if (options.search) {
            pipeline.push({
                $addFields: {
                    userDetails: {
                        $filter: {
                            input: "$userDetails",
                            as: "user",
                            cond: {
                                $regexMatch: {
                                    input: "$$user.displayName",
                                    regex: options.search,
                                    options: "i",
                                },
                            },
                        },
                    },
                },
            });
        }
 
        // Final projection
        pipeline.push({
            $project: {
                date: "$_id.date",
                users: {
                    $map: {
                        input: "$userDetails",
                        as: "user",
                        in: {
                            userID: "$$user.userID",
                            displayName: "$$user.displayName",
                        },
                    },
                },
                count: { $size: "$userDetails" },
            },
        });
 
        // Sort
        if (options.sortBy && options.order) {
            const sortOrder = options.order === "asc" ? 1 : -1;
            pipeline.push({
                $sort: { [options.sortBy]: sortOrder },
            });
        }
    }
 
    // Execute aggregation
    const results = await this.modelUserOffDays.aggregate(pipeline);
    
    // Apply pagination
    const page = options.page || 1;
    const limit = options.limit || 10;
    const startIndex = (page - 1) * limit;
    const endIndex = page * limit;
    
    const paginatedResults = results.slice(startIndex, endIndex);
    
    return {
        data: paginatedResults,
        total: results.length,
        page: page,
        limit: limit,
        totalPages: Math.ceil(results.length / limit),
    };
}
5. GET /userOffDay/userOffDaysPerMonth - Monthly User Off Days
flowchart TD
    A[GET Request with yearMonth] --> B[UserOffDayController.userOffDaysPerMonth]
    B --> C[Extract yearMonth Parameter]
    C --> D[UserOffDayUsecase.userOffDaysPerMonth]
    D --> E[Parse Year and Month]
    E --> F[Calculate Month Date Range]
    F --> G[Query Users with Off Days in Month]
    G --> H[Group by User]
    H --> I[Enrich with User Details]
    I --> J[Calculate Days Count per User]
    J --> K[Sort by Count]
    K --> L[Return Monthly Summary]

Monthly Analysis Implementation:

// Use Case: userOffDay.usecase.ts
public async userOffDaysPerMonth(
    ctx: Context<UserAuth>,
    yearMonth: string
): Promise<any> {
    // Parse year and month
    const [year, month] = yearMonth.split("-").map(Number);
    
    // Calculate month boundaries
    const startDate = new Date(year, month - 1, 1);
    const endDate = new Date(year, month, 0);
    
    const startDateStr = startDate.toISOString().split("T")[0];
    const endDateStr = endDate.toISOString().split("T")[0];
    
    // Aggregation pipeline
    const pipeline = [
        {
            $match: {
                organizationID: ctx.user.organizationID,
                dates: {
                    $exists: true,
                    $ne: [],
                },
            },
        },
        {
            $unwind: "$dates",
        },
        {
            $match: {
                dates: {
                    $gte: startDateStr,
                    $lte: endDateStr,
                },
            },
        },
        {
            $group: {
                _id: "$userID",
                dates: { $push: "$dates" },
                count: { $sum: 1 },
            },
        },
        {
            $lookup: {
                from: "users",
                localField: "_id",
                foreignField: "userID",
                as: "userInfo",
            },
        },
        {
            $unwind: {
                path: "$userInfo",
                preserveNullAndEmptyArrays: true,
            },
        },
        {
            $project: {
                userID: "$_id",
                displayName: "$userInfo.displayName",
                department: "$userInfo.department",
                site: "$userInfo.site",
                dates: 1,
                count: 1,
            },
        },
        {
            $sort: { count: -1 },
        },
    ];
    
    const results = await this.modelUserOffDays.aggregate(pipeline);
    
    // Calculate summary statistics
    const totalDaysInMonth = endDate.getDate();
    const workingDays = calculateWorkingDays(startDate, endDate);
    
    return {
        yearMonth: yearMonth,
        totalDaysInMonth: totalDaysInMonth,
        workingDays: workingDays,
        users: results,
        totalUsers: results.length,
        averageOffDays: results.length > 0 
            ? results.reduce((sum, user) => sum + user.count, 0) / results.length 
            : 0,
    };
}
6. GET /userOffDay - Get All User Off Days
flowchart TD
    A[GET Request to /userOffDay] --> B[UserOffDayController.getUserOffDay]
    B --> C[UserOffDayUsecase.getUserOffDay]
    C --> D[Query All Organization Off Days]
    D --> E[Transform to User-Dates Map]
    E --> F[Format Response Object]
    F --> G[Return Formatted Data]

Get All User Off Days Implementation:

// Use Case: userOffDay.usecase.ts
public async getUserOffDay(ctx: Context<UserAuth>): Promise<any> {
    // Fetch all user off days for the organization
    const userOffDay: UserOffdayMongo[] =
        await this.userOffDayRepo.findByOrganizationID(
            ctx.user.organizationID,
            {}
        );
 
    // Transform to required format
    let result = {
        userIDs: [],
    };
 
    if (userOffDay && userOffDay.length > 0) {
        for (const offDay of userOffDay) {
            const uID = offDay.userID;
            const dates = offDay.dates;
            // Create object with userID as key and dates as value
            result.userIDs.push({ [uID]: dates });
        }
    }
 
    return result;
}

6.4. Upcoming Schedules Algorithm

The upcoming schedules feature implements a multi-tier analysis:

// Conceptual implementation
async function analyzeUpcomingSchedules() {
    const timeFrames = {
        today: getCurrentDate(),
        thisWeek: getCurrentWeekRange(),
        nextWeek: getNextWeekRange()
    };
    
    for (const [period, range] of Object.entries(timeFrames)) {
        const offDayUsers = await findUsersWithOffDays(range);
        const userDetails = await enrichUserInformation(offDayUsers);
        results[period] = userDetails;
    }
    
    return results;
}

6.5. Helper Functions for Date Calculations

The system uses several utility functions for date calculations:

// utils.ts
export function getMondayAndNextSunday() {
    const today = new Date();
    const day = today.getDay();
    const diff = today.getDate() - day + (day === 0 ? -6 : 1);
    
    const monday = new Date(today.setDate(diff));
    monday.setHours(0, 0, 0, 0);
    
    const sunday = new Date(monday);
    sunday.setDate(monday.getDate() + 6);
    sunday.setHours(23, 59, 59, 999);
    
    return { monday, sunday };
}
 
export function getCurrentMonthStartAndEnd() {
    const today = new Date();
    const year = today.getFullYear();
    const month = today.getMonth();
    
    const startOfMonth = new Date(year, month, 1);
    const endOfMonth = new Date(year, month + 1, 0);
    
    return { startOfMonth, endOfMonth };
}
 
export function getCurrentYearStartAndEnd() {
    const today = new Date();
    const year = today.getFullYear();
    
    const start = new Date(year, 0, 1);
    const end = new Date(year, 11, 31);
    
    return { start, end };
}
 
export function getNextWeekStartAndEnd() {
    const { sunday } = getMondayAndNextSunday();
    
    const nextWeekStart = new Date(sunday);
    nextWeekStart.setDate(sunday.getDate() + 1);
    nextWeekStart.setHours(0, 0, 0, 0);
    
    const nextWeekEnd = new Date(nextWeekStart);
    nextWeekEnd.setDate(nextWeekStart.getDate() + 6);
    nextWeekEnd.setHours(23, 59, 59, 999);
    
    return { nextWeekStart, nextWeekEnd };
}

7. Error Handling Strategies

7.1. Error Classification

The system implements a comprehensive error handling hierarchy:

  1. Validation Errors (400 Bad Request)

    • Invalid input data
    • Missing required fields
    • Format violations
  2. Authentication Errors (401 Unauthorized)

    • Invalid JWT token
    • Expired token
    • Missing authentication
  3. Authorization Errors (403 Forbidden)

    • Insufficient permissions
    • Cross-organization access attempts
  4. Not Found Errors (404 Not Found)

    • Non-existent resources
    • Invalid IDs
  5. Internal Errors (500 Internal Server Error)

    • Database connection failures
    • Unexpected exceptions

7.2. Error Response Format

Standardized error responses ensure consistency:

{
    error: {
        code: "ERROR_CODE",
        message: "Human-readable error message",
        details: {
            // Additional context
        }
    },
    data: null
}

7.3. Error Recovery Mechanisms

  1. Automatic Retries

    • Database connection retries
    • PubSub publishing retries
    • External API call retries
  2. Circuit Breaker Pattern

    • Prevents cascade failures
    • Automatic service recovery
    • Fallback mechanisms
  3. Graceful Degradation

    • Cache fallback on database failure
    • Partial response on service unavailability

8. Conclusion

The Non-Operational Days System represents a robust, scalable solution for managing organizational off days. Through its microservices architecture, comprehensive API design, and integration capabilities, it provides a flexible foundation for workforce management. The system’s emphasis on real-time synchronization, performance optimization, and security ensures it can meet the demands of modern enterprise environments while maintaining data integrity and system reliability.

This documentation serves as a comprehensive guide for developers, architects, and operations teams working with the Non-Operational Days System, providing the necessary technical details for effective system understanding, maintenance, and enhancement.