Files
phpvms/app/Services/Finance/PirepFinanceService.php
Nabeel S 073e48c396 7.0.0-beta3 Release (#541)
* 391 Notification refactorings (#441)

* Refactor notifications to allow easier plugins

* Notification refactoring

* Formatting

* Move news to NewsService; cleanup of events

* More refactoring; added send email out for news item and the template

* Formatting

* Formatting

* Fix missing newsRepo (#445)

* Refactor and add importer to Installer module #443 (#444)

* Refactor and add importer to Installer module #443

* Refactor for finances to use in import

* Import groups into roles

* Formatting

* Formatting

* Add interface in installer for import

* Notes about importing

* Check for installer folder

* Formatting

* Fix pirep->user mapping

* Unused import

* Formatting

* Replace importer with AJAX powered; better error handling #443 (#447)

* Replace importer with AJAX powered; better error handling #443

* Formatting

* Fix command line importer

* Remove bootstrap cache (#448)

* Cleanup the bootstrap/cache directory when packaging

* Fix removal of bootstrap cache

* Formatting

* Stricter checks on ACARS API data (#451)

* Stricter checks on ACARS API data

* More checks

* Fix for flight_number check forcing to exist

* Allow nullable on flight_id

* Avoid proc_open use #455 (#456)

* Use PhpExecutableFinder() closes #457 #458 (#460)

* Use DateTimeZone instead of int for creating datetime

closes #461

* Fix CSV imports giving Storage class not found #454 (#462)

* Fix CSV imports giving Storage class not found #454

* Update yarn files for security alert

* Add PHP 7.4 support (#464)

* Add PHP 7.4 to build matrix

* DB fix

* YAML parser fix in test data

* Show versions

* Package updates

* Track used ICAOs

* 7.4 METAR parsing fix

* METAR parser fix

* Formatting

* Add meters to response units

* Call instance for unit conversion

* Return value

* Catch exception for unknown quantity

* Comment fix

* Formatting

* METAR parsing fixes on PHP 7.4

* Package updates

* More random airport ID

* More random airport ID

* Properly disable toolbar

* Semver written out to version file

* Use dev as default identifier

* Fix BindingResolutionError when debug toolbar isn't present (#465)

* Fix BindingResolutionError when debug toolbar isn't present

* Formatting

* Split the importer module out from the installer module (#468)

* Split the importer module out from the installer module

* Cleanup of unused imports

* Move updater into separate module #453

* Remove unused imports/formatting

* Disable the install and importer modules at the end of the setup

* Unused imports; update IJ style

* test explicit stage for php+mysql

* add more to matrix

* Add different MariaDB versions

* undo

* Cleanup Model doc

* Pilots cannot use the dashboard or flights without admin rights (#481)

* Use auth middleware instead of specific groups for logged in state

* Auth check for admin access

* Check user admin access for updates

* Formatting

* Allow nullable field and calculate distance if nulled for flight import #478 (#482)

* Check for no roles being attached #480 (#483)

* Return the flight fares if there are no subfleet fares #488 (#489)

* Return the flight fares if there are no subfleet fares #488

* Formatting

* Formatting

* Account for units when entering fuel amounts #493

* Search for ICAO not working properly (#496)

* /flights and /flights/search direct to the same endpoint

* Properly set the distance/planned_distance on save (#497)

* 491 Installation Error (#495)

* Disable CSRF token

* Add error handling around looking up the theme and set a default

* Note about logs in issue template

* Formatting

* Fix GeoService errors when viewing PIREP #498 (#499)

* Add new command to export a specific PIREP for debugging (#501)

* Set a default model value for airports on PIREP (#500)

* Set a default model value for airports on PIREP

* Fix airport icao reference

* Default airport models

* Catch broader exception writing out config files #491

* style

* Add reference to docs on doc site (#502)

* Properly create/update rows importing #486 (#503)

* Add base Dockerfile for Dockerhub builds (#504)

* New subfleet not being attached to an airline on import #479 (#505)

* Fix subfleet not being attached to an airline on creation in import #479

* Call airline name with optional() around subfleet

* Minor cleanup

* Search flights by subfleet #484 (#506)

* API level search of flights #484

* Add Subfleet to flights page for search

* Make the fuel used optional (#512)

* Add make to Docker container

* Add getRootDomain() to Utils (#514)

* Show admin dropdown for admin-access ability (#515)

* Show admin dropdown for admin-access ability closes #509

* Formatting

* Check user permissions on the routes #508 (#516)

* Check user permissions on the routes #508

* Formatting

* Return default value on exception for setting()

* Correct text for no subfleets #507 (#518)

* Add a public_url() helper #513 (#519)

* Reduce number of queries for update check (#520)

* Try to clear caches before updating (#522)

* Try to clear caches before updating

* Add clear-compiled to maintenance cache list

* Formatting

* Set PIREPs page to public (#526)

Set PIREPs page to public

* Fix live and route map errors #527 (#528)

* Add menu bar for mobile (#529)

* Format all blade templates to 2 spaces #530 (#531)

* Fix PIREP edit endpoint closes #533 (#534)

* Fix import during flight cron #532 (#535)

* PIREPS resource except for show (#536)

* Use optional() around the airport fields (#537)

* Use optional() around the airport fields

* Add null-coalesce around full_name

* Add link to download ACARS config from profile (#539)

* Add link to download ACARS config from profile

* Formatting

* Update xml config file template (#540)
2020-02-08 13:29:34 -05:00

533 lines
17 KiB
PHP

<?php
namespace App\Services\Finance;
use App\Contracts\Service;
use App\Events\Expenses as ExpensesEvent;
use App\Models\Enums\ExpenseType;
use App\Models\Enums\PirepSource;
use App\Models\Expense;
use App\Models\Pirep;
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(config('phpvms.currency'));
}
if (!$pirep->user->journal) {
$pirep->user->journal = $pirep->user->initJournal(config('phpvms.currency'));
}
// 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->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());
$this->journalRepo->post(
$pirep->airline->journal,
$credit,
$debit,
$pirep,
'Fares '.$fare->code.$fare->count
.'; price: '.$fare->price.', cost: '.$fare->cost,
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;
$fuel_used = $pirep->block_fuel;
$debit = Money::createFromAmount($fuel_used * $ap->fuel_jeta_cost);
Log::info('Finance: Fuel cost, (fuel='.$fuel_used.', 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, $i) use ($pirep) {
/*if ($expense->multiplier) {
# TODO: Modify the amount
}*/
Log::info('Finance: PIREP: '.$pirep->id.', expense:', $expense->toArray());
// 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 ($klass === 'Airport') {
$memo = "Airport Expense: {$expense->name} ({$expense->ref_model_id})";
$transaction_group = "Airport: {$expense->ref_model_id}";
} elseif ($klass === 'Subfleet') {
$memo = "Subfleet Expense: {$expense->name} ({$pirep->aircraft->subfleet->name})";
$transaction_group = "Subfleet: {$expense->name} ({$pirep->aircraft->subfleet->name})";
} elseif ($klass === 'Aircraft') {
$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)
);
});
}
/**
* 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);
$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)
{
$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);
}
}