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(); } } |