All files / src/domains/hr/attendance/capture capture.service.ts

0% Statements 0/32
0% Branches 0/26
0% Functions 0/3
0% Lines 0/29

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108                                                                                                                                                                                                                       
import { Injectable, BadRequestException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { AttendancePunch, GeofenceValidationLog } from '../schemas';
import { RecordPunchDto } from './dto/capture.dto';
// Note: In real app, we'd inject PolicyService, LocationService for validations
// import { AttendancePolicyService } from '../policy/attendance-policy.service';
 
@Injectable()
export class CaptureService {
  constructor(
    @InjectModel(AttendancePunch.name)
    private punchModel: Model<AttendancePunch>,
    @InjectModel(GeofenceValidationLog.name)
    private geofenceLogModel: Model<GeofenceValidationLog>,
    // private policyService: AttendancePolicyService,
  ) {}
 
  async recordPunch(
    tenantId: string,
    employeeId: string,
    punchDto: RecordPunchDto,
    userId: string,
    ipAddress: string,
  ) {
    // 1. Check idempotency
    if (punchDto.idempotencyKey) {
      const existing = await this.punchModel.findOne({
        tenantId,
        idempotencyKey: punchDto.idempotencyKey,
      });
      if (existing) return existing;
    }
 
    // 2. Resolve Policy & Shift (omitted complex resolution for brevity)
    // const policy = await this.policyService.resolvePolicyForEmployee(tenantId, employeeId);
 
    // 3. Geofence Validation if coordinates provided and policy requires it
    let geofenceStatus = 'unavailable';
    if (punchDto.latitude && punchDto.longitude) {
      // Stub: in real world, compare with Branch/Location coordinates
      const distance = 0; // calculate distance
      geofenceStatus = distance <= 50 ? 'inside' : 'outside';
 
      // Log validation
      await this.geofenceLogModel.create({
        tenantId,
        employeeId,
        timestamp: new Date(), // Server time
        latitude: punchDto.latitude,
        longitude: punchDto.longitude,
        accuracy: punchDto.accuracy || 0,
        status: geofenceStatus,
        distanceMeters: distance,
      });
    }
 
    // 4. Server authoritative timestamp override for Web/Mobile
    // Never trust client timestamp completely for standard punches unless Offline Sync
    let finalTimestamp = punchDto.timestamp;
    if (punchDto.source === 'Web' || punchDto.source === 'Mobile') {
      const serverTime = new Date();
      const clientTime = new Date(punchDto.timestamp);
      // If diff is > 5 minutes, reject or flag (depending on strictness)
      const diffMinutes =
        Math.abs(serverTime.getTime() - clientTime.getTime()) / 60000;
      if (diffMinutes > 5) {
        // Flag it but use Server Time
        finalTimestamp = serverTime;
        punchDto.metadata = {
          ...punchDto.metadata,
          timeMismatchWarning: `Client time off by ${diffMinutes.toFixed(2)} mins. Used server time.`,
        };
      } else {
        // Close enough, we can use server time to be strictly fair
        finalTimestamp = serverTime;
      }
    }
 
    // 5. Determine the logical attendance date based on Shift bounds
    // Simple approach: just use local date string
    const dateObj = new Date(finalTimestamp);
    // Rough local date offset simulation - in real app use proper timezone lib like moment-timezone or luxon
    const attendanceDate = dateObj.toISOString().split('T')[0];
 
    // 6. Save the Punch
    const created = new this.punchModel({
      ...punchDto,
      tenantId,
      employeeId,
      timestamp: finalTimestamp,
      attendanceDate,
      IPAddress: ipAddress,
      geofenceStatus,
      createdBy: userId,
    });
 
    return created.save();
  }
 
  async getEmployeePunches(tenantId: string, employeeId: string, date: string) {
    return this.punchModel
      .find({ tenantId, employeeId, attendanceDate: date })
      .sort({ timestamp: 1 })
      .exec();
  }
}