efcb7e8895
Due to an error on the settings check advanced fuel calculations was not working and defaulting to fuel used always.
623 lines
20 KiB
PHP
623 lines
20 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Finance;
|
|
|
|
use App\Contracts\Service;
|
|
use App\Events\Expenses as ExpensesEvent;
|
|
use App\Models\Aircraft;
|
|
use App\Models\Airport;
|
|
use App\Models\Enums\ExpenseType;
|
|
use App\Models\Enums\FareType;
|
|
use App\Models\Enums\PirepSource;
|
|
use App\Models\Expense;
|
|
use App\Models\Pirep;
|
|
use App\Models\Subfleet;
|
|
use App\Repositories\ExpenseRepository;
|
|
use App\Repositories\JournalRepository;
|
|
use App\Services\FareService;
|
|
use App\Services\FinanceService;
|
|
use App\Support\Math;
|
|
use App\Support\Money;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class PirepFinanceService extends Service
|
|
{
|
|
private $expenseRepo;
|
|
private $fareSvc;
|
|
private $financeSvc;
|
|
private $journalRepo;
|
|
|
|
/**
|
|
* FinanceService constructor.
|
|
*
|
|
* @param ExpenseRepository $expenseRepo
|
|
* @param FareService $fareSvc
|
|
* @param JournalRepository $journalRepo
|
|
* @param FinanceService $financeSvc
|
|
*/
|
|
public function __construct(
|
|
ExpenseRepository $expenseRepo,
|
|
FareService $fareSvc,
|
|
FinanceService $financeSvc,
|
|
JournalRepository $journalRepo
|
|
) {
|
|
$this->expenseRepo = $expenseRepo;
|
|
$this->fareSvc = $fareSvc;
|
|
$this->journalRepo = $journalRepo;
|
|
$this->financeSvc = $financeSvc;
|
|
}
|
|
|
|
/**
|
|
* Process all of the finances for a pilot report. This is called
|
|
* from a listener (FinanceEvents)
|
|
*
|
|
* @param Pirep $pirep
|
|
*
|
|
* @throws \UnexpectedValueException
|
|
* @throws \InvalidArgumentException
|
|
* @throws \Prettus\Validator\Exceptions\ValidatorException
|
|
* @throws \Exception
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function processFinancesForPirep(Pirep $pirep)
|
|
{
|
|
if (!$pirep->airline->journal) {
|
|
$pirep->airline->journal = $pirep->airline->initJournal(setting('units.currency', 'USD'));
|
|
}
|
|
|
|
if (!$pirep->user->journal) {
|
|
$pirep->user->journal = $pirep->user->initJournal(setting('units.currency', 'USD'));
|
|
}
|
|
|
|
// Clean out the expenses first
|
|
$this->deleteFinancesForPirep($pirep);
|
|
|
|
Log::info('Finance: Starting PIREP pay for '.$pirep->id);
|
|
|
|
// Now start and pay from scratch
|
|
$this->payFuelCosts($pirep);
|
|
$this->payFaresForPirep($pirep);
|
|
$this->payExpensesForSubfleet($pirep);
|
|
$this->payExpensesForPirep($pirep);
|
|
$this->payAirportExpensesForPirep($pirep);
|
|
$this->payExpensesEventsForPirep($pirep);
|
|
$this->payGroundHandlingForPirep($pirep);
|
|
$this->payPilotForPirep($pirep);
|
|
|
|
$pirep->airline->journal->refresh();
|
|
$pirep->user->journal->refresh();
|
|
|
|
// Recalculate balances...
|
|
$this->journalRepo->recalculateBalance($pirep->airline->journal);
|
|
$this->journalRepo->recalculateBalance($pirep->user->journal);
|
|
|
|
return $pirep;
|
|
}
|
|
|
|
/**
|
|
* @param Pirep $pirep
|
|
*/
|
|
public function deleteFinancesForPirep(Pirep $pirep): void
|
|
{
|
|
$this->journalRepo->deleteAllForObject($pirep);
|
|
}
|
|
|
|
/**
|
|
* Collect all of the fares and then post each fare class's profit and
|
|
* the costs for each seat and post it to the journal
|
|
*
|
|
* @param $pirep
|
|
*
|
|
* @throws \UnexpectedValueException
|
|
* @throws \InvalidArgumentException
|
|
* @throws \Prettus\Validator\Exceptions\ValidatorException
|
|
*/
|
|
public function payFaresForPirep($pirep): void
|
|
{
|
|
$fares = $this->getReconciledFaresForPirep($pirep);
|
|
|
|
/** @var \App\Models\Fare $fare */
|
|
foreach ($fares as $fare) {
|
|
Log::info('Finance: PIREP: '.$pirep->id.', Fare:', $fare->toArray());
|
|
|
|
$credit = Money::createFromAmount($fare->count * $fare->price);
|
|
$debit = Money::createFromAmount($fare->count * $fare->cost);
|
|
|
|
Log::info('Finance: Calculate: C='.$credit->toAmount().', D='.$debit->toAmount());
|
|
|
|
$memo = FareType::label($fare->type).' fare: '.$fare->code.': '.$fare->count
|
|
.'; price: '.$fare->price.', cost: '.$fare->cost;
|
|
|
|
$this->journalRepo->post(
|
|
$pirep->airline->journal,
|
|
$credit,
|
|
$debit,
|
|
$pirep,
|
|
$memo,
|
|
null,
|
|
'Fares',
|
|
'fare'
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate the fuel used by the PIREP and add those costs in
|
|
*
|
|
* @param Pirep $pirep
|
|
*
|
|
* @throws \Prettus\Validator\Exceptions\ValidatorException
|
|
*/
|
|
public function payFuelCosts(Pirep $pirep): void
|
|
{
|
|
$ap = $pirep->dpt_airport;
|
|
if (setting('pireps.advanced_fuel', false)) {
|
|
$ac = $pirep->aircraft;
|
|
// Reading second row by skip(1) to reach the previous accepted pirep. Current pirep is at the first row
|
|
$prev_flight = Pirep::where('aircraft_id', $ac->id)->where('state', 2)->where('status', 'ONB')->orderby('submitted_at', 'desc')->skip(1)->first();
|
|
if ($prev_flight) {
|
|
// If there is a pirep use its values to calculate the remaining fuel
|
|
// and calculate the uplifted fuel amount for this pirep
|
|
$fuel_amount = $pirep->block_fuel - ($prev_flight->block_fuel - $prev_flight->fuel_used);
|
|
// Aircraft has more than enough fuel in its tanks, no uplift necessary
|
|
if ($fuel_amount < 0) {
|
|
$fuel_amount = 0;
|
|
}
|
|
} else {
|
|
// No pirep found for aircraft, debit full block fuel
|
|
$fuel_amount = $pirep->block_fuel;
|
|
}
|
|
} else {
|
|
// Setting is false, switch back to basic calculation
|
|
$fuel_amount = $pirep->fuel_used;
|
|
}
|
|
|
|
$debit = Money::createFromAmount($fuel_amount * $ap->fuel_jeta_cost);
|
|
Log::info('Finance: Fuel cost, (fuel='.$fuel_amount.', cost='.$ap->fuel_jeta_cost.') D='
|
|
.$debit->getAmount());
|
|
|
|
$this->financeSvc->debitFromJournal(
|
|
$pirep->airline->journal,
|
|
$debit,
|
|
$pirep,
|
|
'Fuel Cost ('.$ap->fuel_jeta_cost.'/'.config('phpvms.internal_units.fuel').')',
|
|
'Fuel',
|
|
'fuel'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Calculate what the cost is for the operating an aircraft
|
|
* in this subfleet, as-per the block time
|
|
*
|
|
* @param Pirep $pirep
|
|
*
|
|
* @throws \UnexpectedValueException
|
|
* @throws \InvalidArgumentException
|
|
* @throws \Prettus\Validator\Exceptions\ValidatorException
|
|
*/
|
|
public function payExpensesForSubfleet(Pirep $pirep): void
|
|
{
|
|
$sf = $pirep->aircraft->subfleet;
|
|
|
|
// Haven't entered a cost
|
|
if (!filled($sf->cost_block_hour)) {
|
|
return;
|
|
}
|
|
|
|
// Convert to cost per-minute
|
|
$cost_per_min = round($sf->cost_block_hour / 60, 2);
|
|
|
|
// Time to use - use the block time if it's there, actual
|
|
// flight time if that hasn't been used
|
|
$block_time = $pirep->block_time;
|
|
if (!filled($block_time)) {
|
|
Log::info('Finance: No block time, using PIREP flight time');
|
|
$block_time = $pirep->flight_time;
|
|
}
|
|
|
|
$debit = Money::createFromAmount($cost_per_min * $block_time);
|
|
Log::info('Finance: Subfleet Block Hourly, D='.$debit->getAmount());
|
|
|
|
$this->financeSvc->debitFromJournal(
|
|
$pirep->airline->journal,
|
|
$debit,
|
|
$pirep,
|
|
'Subfleet '.$sf->type.': Block Time Cost',
|
|
'Subfleet '.$sf->type,
|
|
'subfleet'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Collect all of the expenses and apply those to the journal
|
|
*
|
|
* @param Pirep $pirep
|
|
*
|
|
* @throws \UnexpectedValueException
|
|
* @throws \InvalidArgumentException
|
|
*/
|
|
public function payExpensesForPirep(Pirep $pirep): void
|
|
{
|
|
$expenses = $this->expenseRepo->getAllForType(
|
|
ExpenseType::FLIGHT,
|
|
$pirep->airline_id
|
|
);
|
|
|
|
/*
|
|
* Go through the expenses and apply a mulitplier if present
|
|
*/
|
|
$expenses->map(function (Expense $expense, $i) use ($pirep) {
|
|
// Airport expenses are paid out separately
|
|
if ($expense->ref_model === Airport::class) {
|
|
return;
|
|
}
|
|
|
|
Log::info('Finance: PIREP: '.$pirep->id.', expense:', $expense->toArray());
|
|
|
|
// Check to see if there is a certain fleet or flight type set on this expense
|
|
// if there is and it doesn't match up the flight type for the PIREP, skip it
|
|
if ($expense->ref_model === Expense::class) {
|
|
if (is_array($expense->flight_type) && count($expense->flight_type) > 0) {
|
|
if (!in_array($pirep->flight_type, $expense->flight_type, true)) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get the transaction group name from the ref_model name
|
|
// This way it can be more dynamic and don't have to add special
|
|
// tables or specific expense calls to accomodate all of these
|
|
$klass = 'Expense';
|
|
if ($expense->ref_model) {
|
|
$ref = explode('\\', $expense->ref_model);
|
|
$klass = end($ref);
|
|
}
|
|
|
|
// Form the memo, with some specific ones depending on the group
|
|
if ($expense->ref_model === Subfleet::class
|
|
&& $expense->ref_model_id === $pirep->aircraft->subfleet->id
|
|
) {
|
|
$memo = "Subfleet Expense: {$expense->name} ({$pirep->aircraft->subfleet->name})";
|
|
$transaction_group = "Subfleet: {$expense->name} ({$pirep->aircraft->subfleet->name})";
|
|
} elseif ($expense->ref_model === Aircraft::class
|
|
&& $expense->ref_model_id === $pirep->aircraft->id
|
|
) {
|
|
$memo = "Aircraft Expense: {$expense->name} ({$pirep->aircraft->name})";
|
|
$transaction_group = "Aircraft: {$expense->name} "
|
|
."({$pirep->aircraft->name}-{$pirep->aircraft->registration})";
|
|
} else {
|
|
$memo = "Expense: {$expense->name}";
|
|
$transaction_group = "Expense: {$expense->name}";
|
|
}
|
|
|
|
$debit = Money::createFromAmount($expense->amount);
|
|
|
|
// If the expense is marked to charge it to a user (only applicable to Flight)
|
|
// then change the journal to the user's to debit there
|
|
$journal = $pirep->airline->journal;
|
|
if ($expense->charge_to_user) {
|
|
$journal = $pirep->user->journal;
|
|
}
|
|
|
|
$this->financeSvc->debitFromJournal(
|
|
$journal,
|
|
$debit,
|
|
$pirep,
|
|
$memo,
|
|
$transaction_group,
|
|
strtolower($klass)
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Pay the airport-specific expenses for a PIREP
|
|
*
|
|
* @param \App\Models\Pirep $pirep
|
|
*/
|
|
public function payAirportExpensesForPirep(Pirep $pirep): void
|
|
{
|
|
$expenses = $this->expenseRepo->getAllForType(
|
|
ExpenseType::FLIGHT,
|
|
$pirep->airline_id,
|
|
Airport::class,
|
|
$pirep->arr_airport_id
|
|
);
|
|
|
|
/*
|
|
* Go through the expenses and apply a mulitplier if present
|
|
*/
|
|
$expenses->map(function (Expense $expense, $i) use ($pirep) {
|
|
Log::info('Finance: PIREP: '.$pirep->id.', airport expense:', $expense->toArray());
|
|
|
|
$memo = "Airport Expense: {$expense->name} ({$expense->ref_model_id})";
|
|
$transaction_group = "Airport: {$expense->ref_model_id}";
|
|
|
|
$debit = Money::createFromAmount($expense->amount);
|
|
|
|
// Charge to the airlines journal
|
|
$journal = $pirep->airline->journal;
|
|
$this->financeSvc->debitFromJournal(
|
|
$journal,
|
|
$debit,
|
|
$pirep,
|
|
$memo,
|
|
$transaction_group,
|
|
'airport'
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Collect all of the expenses from the listeners and apply those to the journal
|
|
*
|
|
* @param Pirep $pirep
|
|
*
|
|
* @throws \UnexpectedValueException
|
|
* @throws \InvalidArgumentException
|
|
* @throws \Prettus\Validator\Exceptions\ValidatorException
|
|
*/
|
|
public function payExpensesEventsForPirep(Pirep $pirep): void
|
|
{
|
|
/**
|
|
* Throw an event and collect any expenses returned from it
|
|
*/
|
|
$gathered_expenses = event(new ExpensesEvent($pirep));
|
|
if (!\is_array($gathered_expenses)) {
|
|
return;
|
|
}
|
|
|
|
foreach ($gathered_expenses as $event_expense) {
|
|
if (!\is_array($event_expense)) {
|
|
continue;
|
|
}
|
|
|
|
foreach ($event_expense as $expense) {
|
|
// Make sure it's of type expense Model
|
|
if (!($expense instanceof Expense)) {
|
|
continue;
|
|
}
|
|
|
|
Log::info('Finance: Expense from listener, N="'
|
|
.$expense->name.'", A='.$expense->amount);
|
|
|
|
// If an airline_id is filled, then see if it matches
|
|
/* @noinspection NotOptimalIfConditionsInspection */
|
|
if (filled($expense->airline_id) && $expense->airline_id !== $pirep->airline_id) {
|
|
Log::info('Finance: Expense has an airline ID and it doesn\'t match, skipping');
|
|
continue;
|
|
}
|
|
|
|
$debit = Money::createFromAmount($expense->amount);
|
|
|
|
$this->financeSvc->debitFromJournal(
|
|
$pirep->airline->journal,
|
|
$debit,
|
|
$pirep,
|
|
'Expense: '.$expense->name,
|
|
$expense->transaction_group ?? 'Expenses',
|
|
'expense'
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collect and apply the ground handling cost
|
|
*
|
|
* @param Pirep $pirep
|
|
*
|
|
* @throws \UnexpectedValueException
|
|
* @throws \InvalidArgumentException
|
|
* @throws \Prettus\Validator\Exceptions\ValidatorException
|
|
*/
|
|
public function payGroundHandlingForPirep(Pirep $pirep): void
|
|
{
|
|
$ground_handling_cost = $this->getGroundHandlingCost($pirep);
|
|
Log::info('Finance: PIREP: '.$pirep->id.'; ground handling: '.$ground_handling_cost);
|
|
|
|
$this->financeSvc->debitFromJournal(
|
|
$pirep->airline->journal,
|
|
Money::createFromAmount($ground_handling_cost),
|
|
$pirep,
|
|
'Ground Handling',
|
|
'Ground Handling',
|
|
'ground_handling'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Figure out what the pilot pay is. Debit it from the airline journal
|
|
* But also reference the PIREP
|
|
*
|
|
* @param Pirep $pirep
|
|
*
|
|
* @throws \UnexpectedValueException
|
|
* @throws \InvalidArgumentException
|
|
* @throws \Prettus\Validator\Exceptions\ValidatorException
|
|
*/
|
|
public function payPilotForPirep(Pirep $pirep): void
|
|
{
|
|
$pilot_pay = $this->getPilotPay($pirep);
|
|
|
|
if ($pirep->flight && !empty($pirep->flight->pilot_pay)) {
|
|
$memo = 'Pilot fixed payment for flight: '.$pirep->flight->pilot_pay;
|
|
Log::info('Finance: PIREP: '.$pirep->id
|
|
.'; pilot pay: fixed for flight='.$pirep->flight->pilot_pay.', total: '.$pilot_pay);
|
|
} else {
|
|
$pilot_pay_rate = $this->getPilotPayRateForPirep($pirep);
|
|
$memo = 'Pilot Payment @ '.$pilot_pay_rate;
|
|
|
|
Log::info('Finance: PIREP: '.$pirep->id
|
|
.'; pilot pay: '.$pilot_pay_rate.', total: '.$pilot_pay);
|
|
}
|
|
|
|
$this->financeSvc->debitFromJournal(
|
|
$pirep->airline->journal,
|
|
$pilot_pay,
|
|
$pirep,
|
|
$memo,
|
|
'Pilot Pay',
|
|
'pilot_pay'
|
|
);
|
|
|
|
$this->financeSvc->creditToJournal(
|
|
$pirep->user->journal,
|
|
$pilot_pay,
|
|
$pirep,
|
|
$memo,
|
|
'Pilot Pay',
|
|
'pilot_pay'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Return all of the fares for the PIREP. Reconcile the list;
|
|
* Get the fares that have been filled out for the PIREP, and
|
|
* then get the fares for the flight and subfleet. Then merge
|
|
* them together, and return the final list of:
|
|
* count = number of pax
|
|
* price = how much each pax unit paid
|
|
* capacity = max number of pax units
|
|
*
|
|
* If count > capacity, count will be adjusted to capacity
|
|
*
|
|
* @param $pirep
|
|
*
|
|
* @return \Illuminate\Support\Collection
|
|
*/
|
|
public function getReconciledFaresForPirep($pirep)
|
|
{
|
|
// Collect all of the fares and prices
|
|
$flight_fares = $this->fareSvc->getForPirep($pirep);
|
|
Log::info('Finance: PIREP: '.$pirep->id.', flight fares: ', $flight_fares->toArray());
|
|
|
|
$all_fares = $this->fareSvc->getAllFares($pirep->flight, $pirep->aircraft->subfleet);
|
|
|
|
$fares = $all_fares->map(function ($fare, $i) use ($flight_fares, $pirep) {
|
|
$fare_count = $flight_fares
|
|
->where('fare_id', $fare->id)
|
|
->first();
|
|
|
|
if ($fare_count) {
|
|
Log::info('Finance: PIREP: '.$pirep->id.', fare count: '.$fare_count);
|
|
|
|
// If the count is greater than capacity, then just set it
|
|
// to the maximum amount
|
|
if ($fare_count->count > $fare->capacity) {
|
|
$fare->count = $fare->capacity;
|
|
} else {
|
|
$fare->count = $fare_count->count;
|
|
}
|
|
} else {
|
|
Log::info('Finance: PIREP: '.$pirep->id.', no fare count found', $fare->toArray());
|
|
}
|
|
|
|
return $fare;
|
|
});
|
|
|
|
return $fares;
|
|
}
|
|
|
|
/**
|
|
* Return the costs for the ground handling, with the multiplier
|
|
* being applied from the subfleet
|
|
*
|
|
* @param Pirep $pirep
|
|
*
|
|
* @return float|null
|
|
*/
|
|
public function getGroundHandlingCost(Pirep $pirep)
|
|
{
|
|
if (filled($pirep->aircraft->subfleet->ground_handling_multiplier)) {
|
|
// force into percent mode
|
|
$multiplier = $pirep->aircraft->subfleet->ground_handling_multiplier.'%';
|
|
|
|
return Math::applyAmountOrPercent(
|
|
$pirep->arr_airport->ground_handling_cost,
|
|
$multiplier
|
|
);
|
|
}
|
|
|
|
return $pirep->arr_airport->ground_handling_cost;
|
|
}
|
|
|
|
/**
|
|
* Return the pilot's hourly pay for the given PIREP
|
|
*
|
|
* @param Pirep $pirep
|
|
*
|
|
* @throws \InvalidArgumentException
|
|
*
|
|
* @return float
|
|
*/
|
|
public function getPilotPayRateForPirep(Pirep $pirep)
|
|
{
|
|
// Get the base rate for the rank
|
|
$rank = $pirep->user->rank;
|
|
$subfleet_id = $pirep->aircraft->subfleet_id;
|
|
|
|
// find the right subfleet
|
|
$override_rate = $rank->subfleets()
|
|
->where('subfleet_id', $subfleet_id)
|
|
->first();
|
|
|
|
if ($override_rate) {
|
|
$override_rate = $override_rate->pivot;
|
|
}
|
|
|
|
if ($pirep->source === PirepSource::ACARS) {
|
|
Log::debug('Source is ACARS');
|
|
$base_rate = $rank->acars_base_pay_rate;
|
|
|
|
if ($override_rate) {
|
|
$override_rate = $override_rate->acars_pay;
|
|
}
|
|
} else {
|
|
Log::debug('Source is Manual');
|
|
$base_rate = $rank->manual_base_pay_rate;
|
|
|
|
if ($override_rate) {
|
|
$override_rate = $override_rate->manual_pay;
|
|
}
|
|
}
|
|
|
|
Log::debug('pilot pay: base rate='.$base_rate.', override='.$override_rate);
|
|
|
|
return Math::applyAmountOrPercent(
|
|
$base_rate,
|
|
$override_rate
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get the user's payment amount for a PIREP
|
|
*
|
|
* @param Pirep $pirep
|
|
*
|
|
* @throws \UnexpectedValueException
|
|
* @throws \InvalidArgumentException
|
|
*
|
|
* @return Money
|
|
*/
|
|
public function getPilotPay(Pirep $pirep)
|
|
{
|
|
// If there is a fixed price for this flight, return that amount
|
|
$flight = $pirep->flight;
|
|
if ($flight && !empty($flight->pilot_pay)) {
|
|
return new Money(Money::convertToSubunit($flight->pilot_pay));
|
|
}
|
|
|
|
// Divided by 60 to get the rate per minute
|
|
$pilot_rate = $this->getPilotPayRateForPirep($pirep) / 60;
|
|
$payment = round($pirep->flight_time * $pilot_rate, 2);
|
|
|
|
Log::info('Pilot Payment: rate='.$pilot_rate);
|
|
$payment = Money::convertToSubunit($payment);
|
|
|
|
return new Money($payment);
|
|
}
|
|
}
|