Skip to content

Instantly share code, notes, and snippets.

@illmatix
Last active November 30, 2020 20:25
Show Gist options
  • Select an option

  • Save illmatix/f9e24a20446a5b95fb13f628c136246b to your computer and use it in GitHub Desktop.

Select an option

Save illmatix/f9e24a20446a5b95fb13f628c136246b to your computer and use it in GitHub Desktop.
<?php
<?php
namespace App\Observers\CalculatedTimesheetHours;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use App\Models\CalculatedTimesheetHours;
use App\Models\EmployeeBillingRates;
use App\Models\OvertimeRules;
use App\Repositories\CalculatedTimesheetHours\CalculatedTimesheetHoursRepositoryInterface;
use App\Repositories\OvertimeRules\OvertimeRulesRepositoryInterface;
use App\Repositories\PayPeriods\PayPeriodsRepositoryInterface;
use App\Repositories\SpecialOvertimeRules\SpecialOvertimeRulesRepositoryInterface;
/**
* Class CalculatedTimesheetHoursUpdate
* @package App\Observers\CalculatedTimesheetHours
*/
class CalculatedTimesheetHoursUpdate
{
/**
* @var string $fieldTicketVoidedApplicationStatusName
*/
protected $fieldTicketVoidedApplicationStatusName = 'Void';
// Repository Interfaces.
private $payPeriods;
private $overtimeRules;
private $specialOvertimeRules;
private $calculatedTimesheetHours;
// References.
private $currentPayPeriod;
private $currentTimesheetEntry;
// Operation from observer
private $op = 'create';
// Counters for comparing historic data.
private $dailyHours = 0;
private $weeklyHours = 0;
private $monthlyHours = 0;
// Standard and overtime hours.
private $standardHours = [];
private $overtimeHours = [];
// Entries to be created.
private $entries = [];
/**
* CalculatedTimesheetHoursUpdate constructor.
*
* @param PayPeriodsRepositoryInterface $payPeriods
* @param OvertimeRulesRepositoryInterface $overtimeRules
* @param SpecialOvertimeRulesRepositoryInterface $specialOvertimeRules
* @param CalculatedTimesheetHoursRepositoryInterface $calculatedTimesheetHours
*/
public function __construct(
PayPeriodsRepositoryInterface $payPeriods,
OvertimeRulesRepositoryInterface $overtimeRules,
SpecialOvertimeRulesRepositoryInterface $specialOvertimeRules,
CalculatedTimesheetHoursRepositoryInterface $calculatedTimesheetHours
) {
$this->payPeriods = $payPeriods;
$this->overtimeRules = $overtimeRules->getAllActive();
$this->specialOvertimeRules = $specialOvertimeRules
->selectAllForSubscription()
->with('employeeRoles')
->first();
$this->calculatedTimesheetHours = $calculatedTimesheetHours;
}
/**
* @param $timesheetEntry
* @param string $op
*/
public function processTimesheetEntry($timesheetEntry, $op = 'create')
{
$this->op = $op;
// Set current timesheet and pay period so other methods can use it.
$this->currentTimesheetEntry = $timesheetEntry;
$this->currentPayPeriod = $this->payPeriods->getWithinDate($this->currentTimesheetEntry->entry_date);
// Parse the date with carbon to allow switching format.
$fieldTicketEntryDate = Carbon::parse($this->currentTimesheetEntry->entry_date);
// 1 based day of the week value.
$fieldTicketWeekDay = $fieldTicketEntryDate->format('w') - 1;
// 0 based day of the week value
$weekStartDay = $this->specialOvertimeRules->first()['week_start'];
// Get historic data to determine current overtime and standard time amounts.
$entriesInMonth = $this->getMonthData();
$lastDate = null;
foreach ($entriesInMonth as $entryInMonth) {
$entryDate = Carbon::parse($entryInMonth->date);
// 1 based day of the week value.
$entryWeekDay = $entryDate->format('w') - 1;
// Determine if historic item is on the first day of the week.
if ($entryWeekDay == $weekStartDay) {
$this->weeklyHours = 0;
}
// Only reset the day counter if checking a new date.
// This handles overtime and standard time on the same date.
if (empty($lastDate) == false && $entryDate->format('Y-m-d') !== $lastDate) {
$this->dailyHours = 0;
}
// Skip items after ticket date. processing of these items will happen later.
if ($entryDate > $fieldTicketEntryDate) {
continue;
}
$this->checkHoursAgainstOvertimeRules((integer)$entryInMonth->hours, $entryInMonth->date, true);
$lastDate = $entryDate->format('Y-m-d');
}
$this->checkHoursAgainstOvertimeRules((integer)$this->currentTimesheetEntry->quantity, $this->currentTimesheetEntry->entry_date);
$this->recalculateExistingEntries();
$this->handleTransaction();
}
/**
* Calculated standard hours and over time hours from a single timesheet entry.
*
* @param int $hours
* @param $date
* @param bool $isExistingEntry
*/
private function checkHoursAgainstOvertimeRules(int $hours, $date, bool $isExistingEntry = false)
{
// increment hour counts for each period that may have a rule.
$this->monthlyHours += $hours;
$this->weeklyHours += $hours;
$this->dailyHours = $hours;
// If the item isn't historic overtime determine which rules the hours fall into.
if ($isExistingEntry == false) {
// initialize the standard hours array
if (isset($this->standardHours[$date]) == false) {
$this->standardHours[$date] = 0;
}
$this->overtimeHours[$date] = [];
foreach ($this->overtimeRules as $overtimeRule) {
// Set default value for this overtime rate multiplier.
$this->overtimeHours[$date][$overtimeRule->overtime_rates] = !empty($this->overtimeHours[$overtimeRule->overtime_rates])
? $this->overtimeHours[$overtimeRule->overtime_rates]
: 0;
// Determine if monthly rule is set and at limit.
if (empty($overtimeRule->monthly_hours) == false && $this->monthlyHours >= $overtimeRule->monthly_hours) {
$this->overtimeHours[$date][$overtimeRule->overtime_rates] += $hours;
continue;
}
// Determine if weekly rule is set and at limit.
if (empty($overtimeRule->weekly_hours) == false && $this->weeklyHours >= $overtimeRule->weekly_hours) {
$this->overtimeHours[$date][$overtimeRule->overtime_rates] += $hours;
continue;
}
// Determine if daily rule is set and at limit.
if (empty($overtimeRule->daily_hours) == false && $this->dailyHours >= $overtimeRule->daily_hours) {
$this->overtimeHours[$date][$overtimeRule->overtime_rates] += $hours - $overtimeRule->daily_hours;
$this->standardHours[$date] += $overtimeRule->daily_hours;
continue;
}
if ($hours <= $overtimeRule->daily_hours) {
$this->standardHours[$date] += $hours;
}
}
}
}
/**
* Recalculated existing entries after the field ticket date to conform with the overtime rules.
*/
private function recalculateExistingEntries()
{
$fieldTicketDate = Carbon::parse($this->currentTimesheetEntry->entry_date);
$existingEntries = CalculatedTimesheetHours
::where('date', '>', $fieldTicketDate)
->where('employees_id', '=', $this->currentTimesheetEntry->employeeTimesheet->employees_id)
->get();
foreach ($this->overtimeRules as $overtimeRule) {
foreach ($existingEntries as $existingEntry) {
$existingEntry->is_ot = false;
$existingEntry->overtime_rate = '1.00';
if ((empty($overtimeRule->monthly_hours) == false && $this->monthlyHours >= $overtimeRule->monthly_hours)
|| (empty($overtimeRule->weekly_hours) == false && $this->weeklyHours >= $overtimeRule->weekly_hours)
|| (empty($overtimeRule->daily_hours) == false && $this->dailyHours >= $overtimeRule->daily_hours)) {
$existingEntry->is_ot = true;
$existingEntry->overtime_rate = $overtimeRule->overtime_rates;
}
$existingEntry->save();
}
}
}
/**
* Create entries ready for inserting into the database.
*/
private function handleTransaction()
{
if ($this->op == 'update') {
$this->update();
} elseif ($this->op == 'delete') {
$this->delete();
} else {
$this->create();
}
}
/**
* Update existing item with new values.
*/
private function update()
{
// Get changed Attributes.
$changedAttributes = $this->currentTimesheetEntry->getDirty();
// Find the existing Entries for the date.
$entryDate = Carbon::parse($this->currentTimesheetEntry->entry_date);
$entryDateYmd = $entryDate->format('Y-m-d');
$entryDatedMY = $entryDate->format('d-M-Y');
$existingEntries = CalculatedTimesheetHours
::where('date', '=', $entryDateYmd)
->where('employees_id', '=', $this->currentTimesheetEntry->employeeTimesheet->employees_id);
foreach ($existingEntries->get() as $existingEntry) {
if (array_key_exists('quantity', $changedAttributes)) {
foreach ($this->overtimeRules as $overtimeRule) {
if ((empty($overtimeRule->monthly_hours) == false && $this->monthlyHours >= $overtimeRule->monthly_hours)
|| (empty($overtimeRule->weekly_hours) == false && $this->weeklyHours >= $overtimeRule->weekly_hours)
|| (empty($overtimeRule->daily_hours) == false && $this->dailyHours >= $overtimeRule->daily_hours)) {
$existingEntry->hours = $this->currentTimesheetEntry->quantity;
} else {
if ($existingEntry->is_ot == false && $existingEntry->hours !== $this->standardHours[$entryDatedMY]) {
$existingEntry->hours = $this->standardHours[$entryDatedMY];
}
if ($existingEntry->is_ot == true
&& array_key_exists($existingEntry->overtime_rate, $this->overtimeHours[$entryDatedMY])
&& $existingEntry->hours !== $this->overtimeHours[$entryDatedMY][$existingEntry->overtime_rate]) {
$existingEntry->hours = $this->overtimeHours[$entryDatedMY][$existingEntry->overtime_rate];
}
}
}
}
if (array_key_exists('employee_roles_id', $changedAttributes)) {
$existingEntry->employee_roles_id = $this->hasSeparatedEmployeeRoles($changedAttributes['employee_roles_id']);
}
if (array_key_exists('employee_billing_rates_id', $changedAttributes)) {
$existingEntry->employee_billing_rates_id = $changedAttributes['employee_billing_rates_id'];
}
if (array_key_exists('pay_rate', $changedAttributes)) {
$existingEntry->pay_rate = $changedAttributes['pay_rate'];
}
$this->saveEntry($existingEntry);
}
}
/**
* Delete existing timesheet entries.
*/
private function delete()
{
// fill in placeholder
}
/**
* Create a new timesheet entry.
*/
private function create()
{
// Pull current billing rate for employee using the data attached to the timesheet.
$employeeBillingRates = $this->getEmployeeBillingRates();
// Throw error if no billing rates can be found.
if (empty($employeeBillingRates) == true) {
$message = 'Cannot find employee billing rates by charge_units_id, employee_roles_id, pay_rate ';
$message .= '[' . $this->currentTimesheetEntry->charge_units_id
. ', ' . $this->currentTimesheetEntry->employee_roles_id
. ', ' . $this->currentTimesheetEntry->pay_rate . ']';
throw new ModelNotFoundException($message);
}
$employeeBillingRates = $employeeBillingRates->first();
// If there are standard hours to track populate the entry data.
foreach ($this->standardHours as $dateKey => $quantity) {
if ($quantity == 0) {
continue;
}
$date = Carbon::parse($dateKey);
$this->entries[] = $this->populateNewEntry(
$employeeBillingRates->employee_billing_rates_id,
$date,
$quantity,
$this->currentTimesheetEntry->pay_rate,
1,
false
);
}
// If there are overtime hours to track populate the entry data.
foreach ($this->overtimeHours as $dateKey => $overtimeGroup) {
$date = Carbon::parse($dateKey);
foreach ($overtimeGroup as $overtimeRate => $quantity) {
if ($quantity == 0) {
continue;
}
$this->entries[] = $this->populateNewEntry(
$employeeBillingRates->employee_billing_rates_id,
$date,
$quantity,
$this->currentTimesheetEntry->pay_rate,
$overtimeRate,
true
);
}
}
// Save new entries that have been created.
foreach ($this->entries as $entry) {
$this->saveEntry($entry);
}
}
/**
* @param $employee_billing_rates_id
* @param $date
* @param $hours
* @param $pay_rate
* @param $overtime_rate
* @param bool $is_ot
*
* @return CalculatedTimesheetHours
*/
private function populateNewEntry($employee_billing_rates_id, $date, $hours, $pay_rate, $overtime_rate, $is_ot = false): CalculatedTimesheetHours
{
$calculatedTimesheetHours = new CalculatedTimesheetHours();
$calculatedTimesheetHours->employees_id = $this->currentTimesheetEntry->employeeTimesheet->employees_id;
$calculatedTimesheetHours->employee_roles_id = $this->hasSeparatedEmployeeRoles($this->currentTimesheetEntry->employee_roles_id);
$calculatedTimesheetHours->employee_billing_rates_id = $employee_billing_rates_id;
$calculatedTimesheetHours->pay_periods_id = $this->currentPayPeriod->pay_periods_id;
$calculatedTimesheetHours->date = $date;
$calculatedTimesheetHours->hours = $hours;
$calculatedTimesheetHours->pay_rate = $pay_rate;
$calculatedTimesheetHours->overtime_rate = $overtime_rate;
$calculatedTimesheetHours->is_ot = $is_ot;
return $calculatedTimesheetHours;
}
/**
* @param CalculatedTimesheetHours $calculatedTimesheetHours
*/
private function saveEntry(CalculatedTimesheetHours $calculatedTimesheetHours)
{
$calculatedTimesheetHours->save();
}
/**
* @return mixed
*/
private function getMonthData()
{
$employee_id = $this->currentTimesheetEntry->employeeTimesheet->employees_id;
// Determine the month and year from the field ticket.
$fieldTicketEntryDate = Carbon::createFromFormat('d-M-Y', $this->currentTimesheetEntry->entry_date);
$fieldTicketMonth = $fieldTicketEntryDate->format('M');
$fieldTicketYear = $fieldTicketEntryDate->format('Y');
// Create new date string from ticket date using the subscription defined start of month.
$monthStartString = $fieldTicketMonth . ' ' . ($this->specialOvertimeRules->first()['month_start'] + 1) . ' ' . $fieldTicketYear;
$monthStart = Carbon::CreateFromFormat('M d Y', $monthStartString);
// Define the range for the subscription defined "month".
$from = $monthStart;
$to = $monthStart->copy()->addMonth()->subDay();
$getByEmployeeIdInMonth = $this->calculatedTimesheetHours->getByEmployeeIdInMonth($employee_id, $from, $to);
return $getByEmployeeIdInMonth->get();
}
/**
* Get active billing rate for specific role, and pay rate.
* Pay rate will be synced with the role on the timesheet by this point.
*
* @return Collection|EmployeeBillingRates[]
*/
private function getEmployeeBillingRates()
{
$employeeBillingRates = EmployeeBillingRates
::where('employee_billing_rates.charge_units_id', $this->currentTimesheetEntry->charge_units_id)
->where('employee_roles_id', $this->currentTimesheetEntry->employee_roles_id)
->where('pay_rate', $this->currentTimesheetEntry->pay_rate)
->get();
return $employeeBillingRates;
}
/**
* @param $employee_roles_id
*
* @return int|null
*/
private function hasSeparatedEmployeeRoles($employee_roles_id)
{
$check = $this->specialOvertimeRules
->employeeRoles
->where('employee_roles_id', '=', $employee_roles_id);
return $check->isNotEmpty() ? $check->first()->employee_roles_id : null;
}
/**
* Check if the field ticket is void.
*
* @param FieldTickets $timesheetEntry
*
* @return bool
*/
public function fieldTicketActive($timesheetEntry): bool
{
$applicationStatus = $timesheetEntry->fieldTicket->fieldTicketStatus->applicationStatus;
if ($applicationStatus->is_void) {
return false;
}
if ($applicationStatus->name == $this->fieldTicketVoidedApplicationStatusName) {
return false;
}
return true;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment