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:
@@ -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);
|
||||
|
||||
125
docs/PLAN-nd-break-deduction.md
Normal file
125
docs/PLAN-nd-break-deduction.md
Normal 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 10PM–6AM 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 10PM–6AM
|
||||
- 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
|
||||
Reference in New Issue
Block a user