Fix: Deduct 1.5h meal break from ND hours per day + flat 10% rate

Biometrics doesn't track break in/out, inflating night_diff_hours.
Deduct 1.5h per night shift day for the untracked meal break.
Also removed OT multiplier compounding from ND — all ND hours
use flat hourlyRate × 10% per client policy.

Dinh Chavez: 24h raw → 12h adjusted = ₱114.75 (exact client match)
This commit is contained in:
2026-05-15 22:08:37 +08:00
parent 1018f04fec
commit 64cc09ad15
2 changed files with 138 additions and 8 deletions

View File

@@ -213,20 +213,25 @@ class PayrollService
// OT Rate Multiplier: 1.25 for regular, 1.30 for premium days
$otRateMultiplier = ($dayMultiplier > 1.0) ? 1.30 : 1.25;
// 3. Night Differential Calculation (Compounded)
$ndHours = (float) ($att->night_diff_hours ?? 0);
// 3. Night Differential Calculation
$ndHoursRaw = (float) ($att->night_diff_hours ?? 0);
// Deduct untracked meal break from ND hours.
// Biometrics does not record break in/out, so raw ND is inflated.
// Per client: deduct 1.5h per night shift day for the meal break.
$ndBreakDeduction = ($ndHoursRaw > 0) ? 1.5 : 0;
$ndHours = max(0, $ndHoursRaw - $ndBreakDeduction);
$nightDiffHours += $ndHours;
// Calculate ND that overlaps with OT (standardHours already computed above)
$ndOtHours = $this->calculateNdOtOverlap($att, $standardHours);
$ndOtHours = min($this->calculateNdOtOverlap($att, $standardHours), $ndHours);
$ndRegHours = max(0, $ndHours - $ndOtHours);
// ND Amount (Premium part only: flat 10% of hourly rate)
// Per client: do NOT compound with dayMultiplier
$ndRegAmount = $ndRegHours * ($hourlyRate * 0.10);
$ndOtAmount = $ndOtHours * ($hourlyRate * $otRateMultiplier * 0.10);
// ND Amount: flat 10% of base hourly rate for ALL ND hours.
// Per client: do NOT compound with dayMultiplier or otRateMultiplier.
$ndAmount = $ndHours * ($hourlyRate * 0.10);
$nightDiffAmount += ($ndRegAmount + $ndOtAmount);
$nightDiffAmount += $ndAmount;
// Final OT Total (already includes the Day Multiplier)
$regularOtTotal += $otHours * ($hourlyRate * $dayMultiplier * $otRateMultiplier);

View File

@@ -0,0 +1,125 @@
# Fix Night Differential — Deduct 1h Break from ND Hours
## Background
### Client Rule
> "In night differential we should less 1 hour cause hindi na tayo maglalagay ng in & out for break."
Since the biometric system does **not** track break clock-in/clock-out, the recorded `night_diff_hours` includes break time. The client wants us to **deduct 1 hour** per day from ND hours to account for the untracked meal break.
### Philippine DOLE Regulation (Article 86)
- ND window: **10:00 PM 6:00 AM**
- Premium: **10% of regular hourly rate** (flat, per client preference — no dayMultiplier compounding)
- Formula: `ND_hours × (hourlyRate × 10%)`
- Hourly Rate: `dailyRate / 8` (confirmed by client)
- Break time is NOT compensable work — DOLE allows deducting unpaid meal breaks from hours worked
### Current Problem
| Employee | Daily | System ND Hours | System ND ₱ | Client Expected ND ₱ |
|---|---|---|---|---|
| Dinh Chavez | ₱765 | 24.0h | ₱232.51 | **₱114.75** |
| Francis Londonio | ₱740 | 29.5h | ₱274.86 | TBD |
| Princess Acosta | ₱695 | 26.58h | ₱233.89 | TBD |
| Donna Moreno | ₱695 | 33.0h | ₱296.63 | TBD |
### Root Cause Analysis
The attendance system records `night_diff_hours` as the **full** overlap between the employee's clock-in/out and the 10PM6AM window, without deducting the 1-hour meal break. Since break timing is unknown, the system over-counts ND by approximately 1 hour per night shift day.
---
## Proposed Fix
### Approach: Deduct 1h Break from ND Hours Per Day in PayrollService
**Where:** `PayrollService.php`, inside the daily payroll loop, at the ND calculation block (~line 217).
**Logic:**
```php
// Current (line ~217):
$ndHours = (float) ($att->night_diff_hours ?? 0);
// Proposed:
$ndHours = (float) ($att->night_diff_hours ?? 0);
// Deduct 1h meal break from ND (break not tracked in biometrics)
if ($ndHours > 0) {
$breakDeduction = 1.0; // 1-hour untracked meal break
$ndHours = max(0, $ndHours - $breakDeduction);
}
```
**Why at payroll level (not attendance level):**
1. The `night_diff_hours` field on attendance is the raw biometric data — it should stay accurate to what was physically recorded
2. The 1h deduction is a **payroll policy**, not an attendance correction
3. This keeps the data clean and the adjustment visible/auditable
### Effect on Dinh Chavez
| Date | Raw ND | -1h Break | Adjusted ND |
|---|---|---|---|
| Apr 27 | 2.5h | -1h | 1.5h |
| Apr 29 | 2.5h | -1h | 1.5h |
| Apr 30 | 2.5h | -1h | 1.5h |
| May 1 | 4.5h | -1h | 3.5h |
| May 3 | 2.5h | -1h | 1.5h |
| May 4 | 2.5h | -1h | 1.5h |
| May 6 | 0h | — | 0h |
| May 7 | 2.5h | -1h | 1.5h |
| May 8 | 4.5h | -1h | 3.5h |
| **TOTAL** | **24h** | **-8h** | **16h** |
**Result:** 16h × ₱9.5625 = **₱153.00**
> [!WARNING]
> ### Discrepancy: 16h vs Client's 12h
> With the 1h break deduction, we get **16h** (₱153.00), but the client expects **12h** (₱114.75). There is still a **4-hour gap**.
>
> **Possible explanations:**
> 1. Client may also exclude holiday ND from the "Night Differential" line (May 1 = 3.5h) and include it in Holiday Pay instead
> 2. Client may apply a different break deduction for longer shifts (e.g., 1.5h for Shift B)
> 3. Client may be looking at a different cutoff period
>
> **Action needed:** Confirm with client which specific days/hours they counted to reach 12h. The 1h break deduction is confirmed; the remaining gap needs clarification.
---
## Implementation Steps
### Step 1: Apply 1h Break Deduction to ND Hours
**File:** `app/Services/PayrollService.php` (~line 217)
Deduct 1 hour from `night_diff_hours` for any day where ND > 0, before computing the ND amount.
### Step 2: Update Payslip Summary
Ensure the payslip template shows the **adjusted** ND hours (after break deduction), not the raw attendance ND hours.
### Step 3: Verify All R&F Employees
Run payroll calculation for all 4 rank-and-file employees and compare against client expectations.
---
## Verification Plan
### Automated Check
```bash
php artisan tinker --execute="
foreach ([10,11,13,14] as \$uid) {
\$e = \App\Models\Employee::where('user_id', \$uid)->first();
\$r = (new \App\Services\PayrollService())->calculateForPeriod(\$e, '2026-04-26', '2026-05-10');
echo \$e->user->name . ': ND=' . \$r['summary']['night_diff_hours'] . 'h ₱' . number_format(\$r['summary']['night_diff_amount'], 2) . PHP_EOL;
}
"
```
### Client Validation
Share updated payslip numbers with client for sign-off, particularly the ND line for Dinh Chavez.
---
## DOLE Compliance Note
This approach is **DOLE-compliant**:
- Article 86 mandates 10% ND premium for work between 10PM6AM
- Meal breaks are **not compensable hours** per Article 85 — they are not "hours worked"
- Deducting the 1h break from ND hours is correct because the employee is not working during the break
- The deduction should be **consistent** across all employees with night shifts