@@ -149,7 +149,7 @@ public AttendanceResponse upsertAttendance(AttendanceUpsertRequest request) {
149149
150150 Attendance saved = attendanceRepository .save (attendance );
151151
152- if (assignment != null && shouldMarkAssignmentCompleted (saved . getStatus () )) {
152+ if (assignment != null && shouldMarkAssignmentCompleted (saved )) {
153153 assignment .setStatus ("COMPLETED" );
154154 assignmentRepository .save (assignment );
155155 }
@@ -269,14 +269,28 @@ public PayrollSummaryResponse buildPayrollSummary(String month,
269269 BigDecimal shiftHours = resolveWorkedHours (assignment .getWorkShift (), attendance );
270270 BigDecimal regularHours = shiftHours .min (BigDecimal .valueOf (8 ));
271271 BigDecimal overtimeHours = shiftHours .subtract (regularHours ).max (BigDecimal .ZERO );
272+ BigDecimal bonusFactor = resolveShiftBonusFactor (assignment .getWorkShift ());
273+ BigDecimal overtimeFactor = resolveShiftOvertimeFactor (assignment .getWorkShift ());
272274
273275 acc .workedHours = acc .workedHours .add (shiftHours );
274276 acc .overtimeHours = acc .overtimeHours .add (overtimeHours );
277+ acc .adjustedRegularHours = acc .adjustedRegularHours .add (regularHours .multiply (bonusFactor ));
278+ acc .adjustedOvertimeHours = acc .adjustedOvertimeHours .add (overtimeHours .multiply (bonusFactor ).multiply (overtimeFactor ));
275279 }
276280 }
277281
282+ Map <String , LocalDateTime > paidAtMonthMap = paidPayrolls .stream ()
283+ .filter (item -> "PAID" .equalsIgnoreCase (Optional .ofNullable (item .getStatus ()).orElse ("" )))
284+ .filter (item -> item .getUser () != null && item .getUser ().getId () != null )
285+ .filter (item -> item .getPayPeriodStart () != null )
286+ .filter (item -> item .getPaidAt () != null )
287+ .collect (Collectors .toMap (
288+ item -> key (item .getUser ().getId (), item .getPayPeriodStart ()),
289+ PayrollCalculation ::getPaidAt ,
290+ (first , second ) -> first ));
291+
278292 List <PayrollSummaryResponse .Row > rows = accumulators .values ().stream ()
279- .map (acc -> toPayrollRow (acc , hourlyRateOverride ))
293+ .map (acc -> toPayrollRow (acc , hourlyRateOverride , endDate , paidMonthMap , paidAtMonthMap ))
280294 .sorted (Comparator .comparing (PayrollSummaryResponse .Row ::getFullName , Comparator .nullsLast (String ::compareToIgnoreCase )))
281295 .collect (Collectors .toList ());
282296
@@ -447,7 +461,11 @@ public Map<String, Object> buildPayrollPaymentStatus(
447461 : (isPaid ? "Đã thanh toán lương tháng" : "Chưa thanh toán lương tháng" ));
448462 }
449463
450- private PayrollSummaryResponse .Row toPayrollRow (PayrollAccumulator acc , BigDecimal hourlyRateOverride ) {
464+ private PayrollSummaryResponse .Row toPayrollRow (PayrollAccumulator acc ,
465+ BigDecimal hourlyRateOverride ,
466+ LocalDate periodEnd ,
467+ Map <String , Boolean > paidMonthMap ,
468+ Map <String , LocalDateTime > paidAtMonthMap ) {
451469 SalaryProfile salaryProfile = resolveSalaryProfile (acc .user .getId (), hourlyRateOverride );
452470
453471 BigDecimal hourlyRate = salaryProfile .hourlyRate ;
@@ -459,11 +477,10 @@ private PayrollSummaryResponse.Row toPayrollRow(PayrollAccumulator acc, BigDecim
459477 boolean eligibleForMonthlySalary = true ;
460478
461479 if (salaryProfile .salaryType == SalaryType .HOURLY ) {
462- regularPay = acc .workedHours .subtract (acc .overtimeHours ).max (BigDecimal .ZERO )
480+ regularPay = acc .adjustedRegularHours .max (BigDecimal .ZERO )
481+ .multiply (hourlyRate );
482+ overtimePay = acc .adjustedOvertimeHours .max (BigDecimal .ZERO )
463483 .multiply (hourlyRate );
464- overtimePay = acc .overtimeHours
465- .multiply (hourlyRate )
466- .multiply (BigDecimal .valueOf (1.5 ));
467484 grossPay = regularPay .add (overtimePay );
468485 deductions = BigDecimal .valueOf (acc .absentShifts )
469486 .multiply (hourlyRate )
@@ -482,6 +499,14 @@ private PayrollSummaryResponse.Row toPayrollRow(PayrollAccumulator acc, BigDecim
482499 netPay = grossPay ;
483500 }
484501
502+ String paidKey = key (acc .user .getId (), periodEnd .withDayOfMonth (1 ));
503+ boolean isPaid = Boolean .TRUE .equals (paidMonthMap .get (paidKey ));
504+ LocalDate dueDate = periodEnd .plusDays (5 );
505+ int overdueDays = !isPaid && LocalDate .now ().isAfter (dueDate )
506+ ? (int ) ChronoUnit .DAYS .between (dueDate , LocalDate .now ())
507+ : 0 ;
508+ boolean attendanceFlag = acc .absentShifts > 0 || acc .lateShifts > 0 ;
509+
485510 return PayrollSummaryResponse .Row .builder ()
486511 .userId (acc .user .getId ())
487512 .fullName (acc .user .getFullName ())
@@ -499,9 +524,40 @@ private PayrollSummaryResponse.Row toPayrollRow(PayrollAccumulator acc, BigDecim
499524 .grossPay (grossPay .setScale (2 , RoundingMode .HALF_UP ))
500525 .deductions (deductions .setScale (2 , RoundingMode .HALF_UP ))
501526 .netPay (netPay .setScale (2 , RoundingMode .HALF_UP ))
527+ .isPaid (isPaid )
528+ .paidAt (paidAtMonthMap .get (paidKey ))
529+ .overdueDays (overdueDays )
530+ .attendanceFlag (attendanceFlag )
502531 .build ();
503532 }
504533
534+ private BigDecimal resolveShiftBonusFactor (WorkShift shift ) {
535+ if (shift == null ) {
536+ return BigDecimal .ONE ;
537+ }
538+
539+ BigDecimal totalBonusPercent = BigDecimal .ZERO ;
540+ if (shift .getNightShiftBonus () != null ) {
541+ totalBonusPercent = totalBonusPercent .add (shift .getNightShiftBonus ());
542+ }
543+ if (shift .getWeekendBonus () != null ) {
544+ totalBonusPercent = totalBonusPercent .add (shift .getWeekendBonus ());
545+ }
546+ if (shift .getHolidayBonus () != null ) {
547+ totalBonusPercent = totalBonusPercent .add (shift .getHolidayBonus ());
548+ }
549+
550+ return BigDecimal .ONE .add (totalBonusPercent .divide (BigDecimal .valueOf (100 ), 4 , RoundingMode .HALF_UP ));
551+ }
552+
553+ private BigDecimal resolveShiftOvertimeFactor (WorkShift shift ) {
554+ if (shift == null || shift .getOvertimeMultiplier () == null || shift .getOvertimeMultiplier ().compareTo (BigDecimal .ONE ) < 0 ) {
555+ return BigDecimal .valueOf (1.5 );
556+ }
557+
558+ return shift .getOvertimeMultiplier ();
559+ }
560+
505561 private SalaryProfile resolveSalaryProfile (Integer userId , BigDecimal hourlyRateOverride ) {
506562 if (hourlyRateOverride != null && hourlyRateOverride .compareTo (BigDecimal .ZERO ) > 0 ) {
507563 return new SalaryProfile (
@@ -567,6 +623,10 @@ private BigDecimal resolveWorkedHours(WorkShift shift, Attendance attendance) {
567623 return BigDecimal .valueOf (minutes ).divide (BigDecimal .valueOf (60 ), 2 , RoundingMode .HALF_UP );
568624 }
569625
626+ if (attendance != null && attendance .getTimeIn () != null && attendance .getTimeOut () == null ) {
627+ return BigDecimal .ZERO ;
628+ }
629+
570630 if (attendance != null && attendance .getShiftWorkingMinutesSnapshot () != null ) {
571631 return BigDecimal .valueOf (attendance .getShiftWorkingMinutesSnapshot ())
572632 .divide (BigDecimal .valueOf (60 ), 2 , RoundingMode .HALF_UP );
@@ -619,8 +679,12 @@ private String normalizeStatus(String status) {
619679 return status .trim ().toUpperCase ();
620680 }
621681
622- private boolean shouldMarkAssignmentCompleted (String attendanceStatus ) {
623- String normalized = Optional .ofNullable (attendanceStatus ).orElse ("" ).trim ().toUpperCase ();
682+ private boolean shouldMarkAssignmentCompleted (Attendance attendance ) {
683+ if (attendance == null || attendance .getTimeOut () == null ) {
684+ return false ;
685+ }
686+
687+ String normalized = Optional .ofNullable (attendance .getStatus ()).orElse ("" ).trim ().toUpperCase ();
624688 return "PRESENT" .equals (normalized ) || "LATE" .equals (normalized );
625689 }
626690
@@ -657,20 +721,19 @@ private boolean hasShiftEndedWithoutCheckIn(LocalTime checkIn,
657721 }
658722
659723 LocalDate shiftDate = assignment .getShiftDate ();
660- if (shiftDate .isBefore (today )) {
661- return true ;
662- }
663-
664- if (!shiftDate .isEqual (today )) {
665- return false ;
666- }
667-
668724 WorkShift shift = assignment .getWorkShift ();
669725 if (shift == null || shift .getEndTime () == null ) {
670726 return false ;
671727 }
672728
673- return !now .isBefore (shift .getEndTime ());
729+ LocalDateTime shiftEndDateTime = LocalDateTime .of (shiftDate , shift .getEndTime ());
730+ if (shift .getStartTime () != null && !shift .getEndTime ().isAfter (shift .getStartTime ())) {
731+ shiftEndDateTime = shiftEndDateTime .plusDays (1 );
732+ }
733+
734+ LocalDateTime nowDateTime = LocalDateTime .of (today , now );
735+
736+ return !nowDateTime .isBefore (shiftEndDateTime );
674737 }
675738
676739 private static class PayrollAccumulator {
@@ -682,6 +745,8 @@ private static class PayrollAccumulator {
682745 private int absentShifts ;
683746 private BigDecimal workedHours = BigDecimal .ZERO ;
684747 private BigDecimal overtimeHours = BigDecimal .ZERO ;
748+ private BigDecimal adjustedRegularHours = BigDecimal .ZERO ;
749+ private BigDecimal adjustedOvertimeHours = BigDecimal .ZERO ;
685750
686751 private PayrollAccumulator (User user ) {
687752 this .user = user ;
0 commit comments