gte:newDate(now.getTime()-24*60*60*1000),// Within last 24 hours
},
},
});
if(alreadyNotified)continue;
// Notify admins and super admins
constadminUsers=awaitthis.prisma.user.findMany({
where:{
role:{in:['SUPER_ADMIN','ADMIN']},
status:'ACTIVE',
deletedAt:null,
},
select:{id:true},
});
for(constadminofadminUsers){
try{
awaitthis.notificationsService.create({
userId:admin.id,
type:'IMPORTANT',
category:'SYSTEM',
title:`Contract Expiring in ${daysOut} Days: ${contract.user.firstName}${contract.user.lastName}`,
message:`The contract for ${contract.user.firstName}${contract.user.lastName} expires on ${contract.endDate?.toISOString().split('T')[0]}.${daysOut}daysremaining.Pleasereviewandtakeaction.`,
reviewNotes:`Auto-applied: contractor did not respond within the ${responseWindowHours}-hour response window.`,
},
});
// Push HUD update
try{
awaitthis.hudService.pushDeductionApplied(
deduction.userId,
deduction.subCategory,
deduction.amountPiasters,
);
}catch(hudErr){
this.logger.warn(`Failed to push HUD for auto-applied deduction: ${hudErr.message}`);
}
// Notify the contractor
try{
awaitthis.notificationsService.create({
userId:deduction.userId,
type:'IMPORTANT',
category:'DEDUCTION',
title:'Deduction Auto-Applied',
message:`Deduction (${deduction.subCategory}) of ${deduction.amountPiasters} piasters was auto-applied because no response was submitted within ${responseWindowHours} hours.`,
actionUrl:`/salary`,
entityType:'deduction',
entityId:deduction.id,
});
}catch(notifErr){
this.logger.warn(`Failed to send auto-apply notification: ${notifErr.message}`);
message:`Contractor has reached ${percentage.toFixed(1)}% deduction threshold (${totalAmount} / ${actualSalary} piasters). PIP or termination review required within 5 business days.`,
actionUrl:`/admin/contractors/${userId}`,
isBlocking:true,
entityType:'user',
entityId:userId,
});
}catch{/* non-critical */}
}
// Notify the contractor
try{
awaitthis.notificationsService.create({
userId,
type:'BLOCKING',
category:'DEDUCTION',
title:'Critical Deduction Threshold Reached',
message:`Your total deductions this month have reached ${percentage.toFixed(1)}% of your salary. This triggers an automatic performance review.`,
isBlocking:true,
});
}catch{/* non-critical */}
}
}catch(err){
this.logger.error(`Failed to check deduction threshold for ${userId}: ${err.message}`);
message:`Reminder: "${meeting.title}" is scheduled for tomorrow at ${meeting.startTime.toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit'})}.`,
actionUrl:'/meetings',
entityType:'meeting',
entityId:meeting.id,
});
remindersSent++;
}catch{/* non-critical */}
}
awaitmeetingModel.update({
where:{id:meeting.id},
data:{reminderSent24h:true},
});
}
}catch(err){
if(err.message?.includes('does not exist')||err.message?.includes('not found')||err.code==='P2021'){
this.logger.debug('Meeting table does not exist yet. Skipping reminders.');
message:`Monthly payroll for ${month}/${year} has been auto-calculated for ${contractorsProcessed} contractors. Total net: ${totalNetPiasters} piasters. Please review and submit for approval.`,
actionUrl:'/admin/payroll',
entityType:'payroll',
entityId:result.id,
});
}catch{
// Non-critical
}
}
this.logger.log(
`Payroll for ${month}/${year} auto-calculated: ${contractorsProcessed} contractors, ${totalNetPiasters} piasters net`,
);
return{contractorsProcessed,totalNetPiasters};
}catch(err){
this.logger.error(`Payroll auto-calculation failed for ${month}/${year}: ${err.message}`);
message:`You have an unreported working day. No report was submitted and no unavailability was logged for ${yesterday.toISOString().split('T')[0]}. A deduction of ${dailyRate} piasters has been initiated.`,
actionUrl:'/salary',
isBlocking:true,
entityType:'deduction',
entityId:deduction.id,
});
this.logger.log(
`Unreported day detected: ${contractor.firstName}${contractor.lastName} on ${yesterday.toISOString().split('T')[0]} — deduction of ${dailyRate} piasters`,
);
}catch(err){
this.logger.error(
`Failed to create B2 deduction for ${contractor.id}: ${err.message}`,
);
}
// Check for consecutive unreported days (3+ triggers escalation)
awaitthis.checkConsecutiveUnreported(contractor);
}catch(err){
this.logger.error(
`Error checking unreported day for contractor ${contractor.id}: ${err.message}`,