From 96e8fbfa7891b21c39338c03de7b85f5b271648e Mon Sep 17 00:00:00 2001 From: Nabeel Shahzad Date: Mon, 5 Mar 2018 12:21:38 -0600 Subject: [PATCH] Add subfleet specific expenses, fixed bug for pirep fares #130 #136 --- .../factories/SubfleetExpenseFactory.php | 11 +++ ...17_06_23_011011_create_subfleet_tables.php | 6 +- app/Database/seeds/sample.yml | 87 ++++++++++++------- .../Controllers/Admin/SubfleetController.php | 56 +++++++++++- app/Models/Subfleet.php | 11 +++ app/Models/SubfleetExpense.php | 45 ++++++++++ app/Routes/admin.php | 1 + app/Services/FinanceService.php | 64 ++++++++++++-- .../views/admin/subfleets/edit.blade.php | 6 ++ .../views/admin/subfleets/expenses.blade.php | 60 +++++++++++++ .../views/admin/subfleets/script.blade.php | 22 +++++ tests/FinanceTest.php | 32 ++++++- 12 files changed, 357 insertions(+), 44 deletions(-) create mode 100644 app/Database/factories/SubfleetExpenseFactory.php create mode 100644 app/Models/SubfleetExpense.php create mode 100644 resources/views/admin/subfleets/expenses.blade.php diff --git a/app/Database/factories/SubfleetExpenseFactory.php b/app/Database/factories/SubfleetExpenseFactory.php new file mode 100644 index 000000000..9ece17960 --- /dev/null +++ b/app/Database/factories/SubfleetExpenseFactory.php @@ -0,0 +1,11 @@ +define(App\Models\SubfleetExpense::class, function (Faker $faker) { + return [ + 'subfleet_id' => null, + 'name' => $faker->text(20), + 'amount' => $faker->randomFloat(2, 100, 1000), + ]; +}); diff --git a/app/Database/migrations/2017_06_23_011011_create_subfleet_tables.php b/app/Database/migrations/2017_06_23_011011_create_subfleet_tables.php index 16ae2174d..dd2ce40f2 100644 --- a/app/Database/migrations/2017_06_23_011011_create_subfleet_tables.php +++ b/app/Database/migrations/2017_06_23_011011_create_subfleet_tables.php @@ -27,11 +27,13 @@ public function up() }); Schema::create('subfleet_expenses', function(Blueprint $table) { + $table->increments('id'); $table->unsignedBigInteger('subfleet_id'); $table->string('name', 50); - $table->unsignedDecimal('cost'); + $table->unsignedDecimal('amount'); + $table->timestamps(); - $table->primary(['subfleet_id', 'name']); + $table->index('subfleet_id'); }); Schema::create('subfleet_fare', function (Blueprint $table) { diff --git a/app/Database/seeds/sample.yml b/app/Database/seeds/sample.yml index 8bd74dd4a..2a7337ce4 100644 --- a/app/Database/seeds/sample.yml +++ b/app/Database/seeds/sample.yml @@ -238,6 +238,14 @@ subfleets: type: 772-36ER-GE90-115B ground_handling_multiplier: 150 +subfleet_expenses: + - id: 1 + subfleet_id: 1 + name: Catering + amount: 1000 + created_at: now + updated_at: now + # add a few mods to aircraft and fares subfleet_fare: @@ -433,20 +441,20 @@ pirep_comments: journals: - id: '1' ledger_id: null - balance: '7970000' + balance: '15840000' currency: USD morphed_type: App\Models\Airline morphed_id: '1' - created_at: '2018-03-02 23:50:01' - updated_at: '2018-03-02 23:50:01' + created_at: now + updated_at: now - id: '2' ledger_id: null - balance: '15000' + balance: '30000' currency: USD morphed_type: App\Models\User morphed_id: '1' - created_at: '2018-03-02 23:50:01' - updated_at: '2018-03-02 23:50:01' + created_at: now + updated_at: now journal_transactions: - id: 81e9d86c-fede-467d-befd-887e046d9c48 @@ -458,9 +466,9 @@ journal_transactions: memo: 'Fares Y300; price:200, cost: 0' ref_class: App\Models\Pirep ref_class_id: pirepid_1 - created_at: '2018-03-02 23:50:01' - updated_at: '2018-03-02 23:50:01' - post_date: '2018-03-02 23:50:01' + created_at: now + updated_at: now + post_date: now deleted_at: null - id: b12a81a9-1273-4413-a46a-96b2925cfefb transaction_group: fares @@ -471,9 +479,9 @@ journal_transactions: memo: 'Fares B10; price:1100, cost: 0' ref_class: App\Models\Pirep ref_class_id: pirepid_1 - created_at: '2018-03-02 23:50:01' - updated_at: '2018-03-02 23:50:01' - post_date: '2018-03-02 23:50:01' + created_at: now + updated_at: now + post_date: now deleted_at: null - id: 8688ff40-aed4-4d60-90b7-0c5a88c12fbc transaction_group: fares @@ -484,9 +492,9 @@ journal_transactions: memo: 'Fares F10; price:1000, cost: 0' ref_class: App\Models\Pirep ref_class_id: pirepid_1 - created_at: '2018-03-02 23:50:01' - updated_at: '2018-03-02 23:50:01' - post_date: '2018-03-02 23:50:01' + created_at: now + updated_at: now + post_date: now deleted_at: null - id: d34a9d1e-0d54-4191-bf9f-0043062c04c9 transaction_group: expenses @@ -497,9 +505,9 @@ journal_transactions: memo: 'Expense: Per-Flight (no muliplier)' ref_class: App\Models\Pirep ref_class_id: pirepid_1 - created_at: '2018-03-02 23:50:01' - updated_at: '2018-03-02 23:50:01' - post_date: '2018-03-02 23:50:01' + created_at: now + updated_at: now + post_date: now deleted_at: null - id: bdc9d50d-ac3d-4334-997c-8f13b8328ab8 transaction_group: expenses @@ -510,9 +518,9 @@ journal_transactions: memo: 'Expense: Per-Flight (multiplier)' ref_class: App\Models\Pirep ref_class_id: pirepid_1 - created_at: '2018-03-02 23:50:01' - updated_at: '2018-03-02 23:50:01' - post_date: '2018-03-02 23:50:01' + created_at: now + updated_at: now + post_date: now deleted_at: null - id: b5c45ad5-af73-4d7c-9352-3dfb8de292a0 transaction_group: expenses @@ -523,9 +531,9 @@ journal_transactions: memo: 'Expense: Per-Flight (multiplier, on airline)' ref_class: App\Models\Pirep ref_class_id: pirepid_1 - created_at: '2018-03-02 23:50:01' - updated_at: '2018-03-02 23:50:01' - post_date: '2018-03-02 23:50:01' + created_at: now + updated_at: now + post_date: now deleted_at: null - id: e65083f9-23c3-4e98-8d63-cd7f35732f7b transaction_group: ground_handling @@ -536,9 +544,9 @@ journal_transactions: memo: 'Ground handling' ref_class: App\Models\Pirep ref_class_id: pirepid_1 - created_at: '2018-03-02 23:50:01' - updated_at: '2018-03-02 23:50:01' - post_date: '2018-03-02 23:50:01' + created_at: now + updated_at: now + post_date: now deleted_at: null - id: 9825a96e-58b5-465f-8fb8-4c8e1e5567eb transaction_group: pilot_pay @@ -549,9 +557,9 @@ journal_transactions: memo: 'Pilot Payment @ 50' ref_class: App\Models\Pirep ref_class_id: pirepid_1 - created_at: '2018-03-02 23:50:01' - updated_at: '2018-03-02 23:50:01' - post_date: '2018-03-02 23:50:01' + created_at: now + updated_at: now + post_date: now deleted_at: null - id: 2e3118b3-c98f-41d1-b2b6-ccb4f34e86b0 transaction_group: pilot_pay @@ -562,7 +570,20 @@ journal_transactions: memo: 'Pilot Payment @ 50' ref_class: App\Models\Pirep ref_class_id: pirepid_1 - created_at: '2018-03-02 23:50:01' - updated_at: '2018-03-02 23:50:01' - post_date: '2018-03-02 23:50:01' + created_at: now + updated_at: now + post_date: now + deleted_at: null + - id: b98a837a-aa59-4630-a547-5a9d90b5b541 + transaction_group: subfleet_expense + journal_id: 1 + credit: null + debit: 100000 + currency: USD + memo: 'Subfleet Expense: Catering' + ref_class: App\Models\Pirep + ref_class_id: pirepid_1 + created_at: now + updated_at: now + post_date: now deleted_at: null diff --git a/app/Http/Controllers/Admin/SubfleetController.php b/app/Http/Controllers/Admin/SubfleetController.php index c7f048e59..f5d21ac18 100644 --- a/app/Http/Controllers/Admin/SubfleetController.php +++ b/app/Http/Controllers/Admin/SubfleetController.php @@ -7,6 +7,7 @@ use App\Models\Airline; use App\Models\Enums\FuelType; use App\Models\Subfleet; +use App\Models\SubfleetExpense; use App\Repositories\AircraftRepository; use App\Repositories\FareRepository; use App\Repositories\RankRepository; @@ -232,7 +233,7 @@ public function destroy($id) * @param Subfleet $subfleet * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View */ - protected function return_ranks_view(Subfleet $subfleet) + protected function return_ranks_view(?Subfleet $subfleet) { $subfleet->refresh(); @@ -248,7 +249,7 @@ protected function return_ranks_view(Subfleet $subfleet) * @param Subfleet $subfleet * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View */ - protected function return_fares_view(Subfleet $subfleet) + protected function return_fares_view(?Subfleet $subfleet) { $subfleet->refresh(); @@ -303,6 +304,57 @@ public function ranks($id, Request $request) return $this->return_ranks_view($subfleet); } + /** + * @param Subfleet $subfleet + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + protected function return_expenses_view(?Subfleet $subfleet) + { + $subfleet->refresh(); + return view('admin.subfleets.expenses', [ + 'subfleet' => $subfleet, + ]); + } + + /** + * Operations for associating ranks to the subfleet + * @param $id + * @param Request $request + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + * @throws \Exception + */ + public function expenses($id, Request $request) + { + $subfleet = $this->subfleetRepo->findWithoutFail($id); + if (empty($subfleet)) { + return $this->return_expenses_view($subfleet); + } + + if ($request->isMethod('get')) { + return $this->return_expenses_view($subfleet); + } + + /** + * update specific rank data + */ + if ($request->isMethod('post')) { + $expense = new SubfleetExpense($request->post()); + $expense->subfleet_id = $subfleet->id; + $expense->save(); + $subfleet->refresh(); + } elseif ($request->isMethod('put')) { + $expense = SubfleetExpense::findOrFail($request->input('expense_id')); + $expense->{$request->name} = $request->value; + $expense->save(); + } // dissassociate fare from teh aircraft + elseif ($request->isMethod('delete')) { + $expense = SubfleetExpense::findOrFail($request->input('expense_id')); + $expense->delete(); + } + + return $this->return_expenses_view($subfleet); + } + /** * Operations on fares to the subfleet * @param $id diff --git a/app/Models/Subfleet.php b/app/Models/Subfleet.php index db04ad97e..ec45c2829 100644 --- a/app/Models/Subfleet.php +++ b/app/Models/Subfleet.php @@ -74,6 +74,9 @@ public static function boot() * Relationships */ + /** + * @return $this + */ public function aircraft() { return $this->hasMany(Aircraft::class, 'subfleet_id') @@ -85,6 +88,14 @@ public function airline() return $this->belongsTo(Airline::class, 'airline_id'); } + /** + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function expenses() + { + return $this->hasMany(SubfleetExpense::class, 'subfleet_id'); + } + public function fares() { return $this->belongsToMany(Fare::class, 'subfleet_fare') diff --git a/app/Models/SubfleetExpense.php b/app/Models/SubfleetExpense.php new file mode 100644 index 000000000..a2812e869 --- /dev/null +++ b/app/Models/SubfleetExpense.php @@ -0,0 +1,45 @@ + 'float', + ]; + + public static $rules = [ + 'name' => 'required', + 'amount' => 'required|numeric', + ]; + + /** + * Relationships + */ + + /** + * Has a subfleet + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function subfleet() + { + return $this->belongsTo(Subfleet::class, 'subfleet_id'); + } +} diff --git a/app/Routes/admin.php b/app/Routes/admin.php index 775bf06e0..c8cb49a6d 100644 --- a/app/Routes/admin.php +++ b/app/Routes/admin.php @@ -46,6 +46,7 @@ # subfleet Route::resource('subfleets', 'SubfleetController'); + Route::match(['get', 'post', 'put', 'delete'], 'subfleets/{id}/expenses', 'SubfleetController@expenses'); Route::match(['get', 'post', 'put', 'delete'], 'subfleets/{id}/fares', 'SubfleetController@fares'); Route::match(['get', 'post', 'put', 'delete'], 'subfleets/{id}/ranks', 'SubfleetController@ranks'); diff --git a/app/Services/FinanceService.php b/app/Services/FinanceService.php index fa3f9eba7..14beb8496 100644 --- a/app/Services/FinanceService.php +++ b/app/Services/FinanceService.php @@ -7,6 +7,7 @@ use App\Models\Enums\PirepSource; use App\Models\Expense; use App\Models\Pirep; +use App\Models\SubfleetExpense; use App\Repositories\ExpenseRepository; use App\Repositories\JournalRepository; use App\Support\Math; @@ -85,8 +86,13 @@ public function processFinancesForPirep(Pirep $pirep) $pirep->user->journal = $pirep->user->initJournal(config('phpvms.currency')); } + # Clean out the expenses first + $this->deleteFinancesForPirep($pirep); + + # Now start and pay from scratch $this->payFaresForPirep($pirep); $this->payExpensesForPirep($pirep); + $this->paySubfleetExpenses($pirep); $this->payGroundHandlingForPirep($pirep); $this->payPilotForPirep($pirep); @@ -118,7 +124,7 @@ public function payFaresForPirep($pirep): void /** @var \App\Models\Fare $fare */ foreach ($fares as $fare) { - Log::info('Finance: PIREP: ' . $pirep->id . ', fare:', $fare->toArray()); + Log::info('Finance: PIREP: '.$pirep->id.', fare:', $fare->toArray()); $credit = Money::createFromAmount($fare->count * $fare->price); $debit = Money::createFromAmount($fare->count * $fare->cost); @@ -129,7 +135,7 @@ public function payFaresForPirep($pirep): void $debit, $pirep, 'Fares ' . $fare->code . $fare->count - . '; price:' . $fare->price . ', cost: ' . $fare->cost, + .'; price: '.$fare->price.', cost: '.$fare->cost, null, 'fares' ); @@ -164,6 +170,38 @@ public function payExpensesForPirep(Pirep $pirep): void } } + /** + * Pay out the expenses for the subfleet + * @param Pirep $pirep + * @throws \Prettus\Validator\Exceptions\ValidatorException + */ + public function paySubfleetExpenses(Pirep $pirep) + { + $subfleet_expenses = SubfleetExpense::where([ + 'subfleet_id' => $pirep->aircraft->subfleet_id, + ])->get(); + + if(!$subfleet_expenses) { + return; + } + + foreach ($subfleet_expenses as $expense) { + + Log::info('Finance: PIREP: '.$pirep->id + .'; subfleet expense: "'.$expense->name.'", cost: "'.$expense->amount); + + $this->journalRepo->post( + $pirep->airline->journal, + null, + Money::createFromAmount($expense->amount), + $pirep, + 'Subfleet Expense: '.$expense->name, + null, + 'subfleet_expense' + ); + } + } + /** * Collect and apply the ground handling cost * @param Pirep $pirep @@ -174,12 +212,13 @@ public function payExpensesForPirep(Pirep $pirep): void public function payGroundHandlingForPirep(Pirep $pirep) { $ground_handling_cost = $this->getGroundHandlingCost($pirep); + Log::info('Finance: PIREP: '.$pirep->id.'; ground handling: '.$ground_handling_cost); $this->journalRepo->post( $pirep->airline->journal, null, Money::createFromAmount($ground_handling_cost), $pirep, - 'Ground handling', + 'Ground Handling', null, 'ground_handling' ); @@ -199,6 +238,9 @@ public function payPilotForPirep(Pirep $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->journalRepo->post( $pirep->airline->journal, null, @@ -239,13 +281,20 @@ 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($flight, $pirep->aircraft->subfleet); - $fares = $all_fares->map(function($fare, $i) use ($flight_fares) { + $fares = $all_fares->map(function($fare, $i) use ($flight_fares, $pirep) { - $fare_count = $flight_fares->whereStrict('id', $fare->id)->first(); + $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) { @@ -253,6 +302,8 @@ public function getReconciledFaresForPirep($pirep) } else { $fare->count = $fare_count->count; } + } else { + Log::info('Finance: PIREP: ' . $pirep->id . ', no fare count found', $fare->toArray()); } return $fare; @@ -307,6 +358,9 @@ public function getExpenses(Pirep $pirep) return $expense; }); + /** + * Throw an event and collect any expenses returned from it + */ $gathered_expenses = event(new ExpensesEvent($pirep)); if (!\is_array($gathered_expenses)) { return $expenses; diff --git a/resources/views/admin/subfleets/edit.blade.php b/resources/views/admin/subfleets/edit.blade.php index 64d1db330..fd6e66a56 100644 --- a/resources/views/admin/subfleets/edit.blade.php +++ b/resources/views/admin/subfleets/edit.blade.php @@ -21,5 +21,11 @@ @include('admin.subfleets.fares') + +
+
+ @include('admin.subfleets.expenses') +
+
@endsection @include('admin.subfleets.script') diff --git a/resources/views/admin/subfleets/expenses.blade.php b/resources/views/admin/subfleets/expenses.blade.php new file mode 100644 index 000000000..b89870724 --- /dev/null +++ b/resources/views/admin/subfleets/expenses.blade.php @@ -0,0 +1,60 @@ +
+
+

expenses

+ @component('admin.components.info') + These expenses are only applied to this subfleet + @endcomponent +
+
+ + @if(count($subfleet->expenses)) + + + + + + @endif + + @foreach($subfleet->expenses as $expense) + + + + + + @endforeach + +
NameCost {!! currency(config('phpvms.currency')) !!}
+

+ {!! $expense->name !!} +

+
+

+ {!! $expense->amount !!} +

+
+ {!! Form::open(['url' => url('/admin/subfleets/'.$subfleet->id.'/expenses'), + 'method' => 'delete', 'class' => 'modify_expense form-inline']) !!} + {!! Form::hidden('expense_id', $expense->id) !!} + {!! Form::button('', ['type' => 'submit', + 'class' => 'btn btn-sm btn-danger btn-icon', + 'onclick' => "return confirm('Are you sure?')", + ]) !!} + {!! Form::close() !!} +
+
+
+
+
+ {!! Form::open(['url' => url('/admin/subfleets/'.$subfleet->id.'/expenses'), + 'method' => 'post', 'class' => 'modify_expense form-inline']) !!} + {!! Form::input('text', 'name', null, ['class' => 'form-control input-sm']) !!} + {!! Form::number('cost', null, ['class' => 'form-control input-sm']) !!} + {!! Form::button(' Add', ['type' => 'submit', + 'class' => 'btn btn-success btn-small']) !!} + {!! Form::close() !!} +
+
+
+
diff --git a/resources/views/admin/subfleets/script.blade.php b/resources/views/admin/subfleets/script.blade.php index aa2aeb9e0..818586a6a 100644 --- a/resources/views/admin/subfleets/script.blade.php +++ b/resources/views/admin/subfleets/script.blade.php @@ -32,6 +32,22 @@ function setEditable() { } } }); + + $('#subfleet-expenses a').editable({ + type: 'text', + mode: 'inline', + emptytext: '0', + url: '{!! url('/admin/subfleets/'.$subfleet->id.'/expenses') !!}', + title: 'Enter override value', + ajaxOptions: {'type': 'put'}, + params: function (params) { + return { + expense_id: params.pk, + name: params.name, + value: params.value + } + } + }); } $(document).ready(function() { @@ -51,6 +67,12 @@ function setEditable() { $.pjax.submit(event, '#subfleet_ranks_wrapper', {push: false}); }); + $(document).on('submit', 'form.modify_expense', function (event) { + event.preventDefault(); + console.log(event); + $.pjax.submit(event, '#subfleet-expenses-wrapper', {push: false}); + }); + $(document).on('pjax:complete', function() { $(".select2").select2(); setEditable(); diff --git a/tests/FinanceTest.php b/tests/FinanceTest.php index 79465732b..ce3520661 100644 --- a/tests/FinanceTest.php +++ b/tests/FinanceTest.php @@ -91,6 +91,18 @@ public function createFullPirep() $this->fareSvc->setForSubfleet($subfleet['subfleet'], $fare); } + # Add an expense + factory(App\Models\Expense::class)->create([ + 'airline_id' => null, + 'amount' => 100 + ]); + + # Add a subfleet expense + factory(App\Models\SubfleetExpense::class)->create([ + 'subfleet_id' => $subfleet['subfleet']->id, + 'amount' => 200 + ]); + $pirep = $this->pirepSvc->create($pirep, []); return [$user, $pirep, $fares]; @@ -614,8 +626,24 @@ public function testPirepFinances() $transactions = $journalRepo->getAllForObject($pirep); - $this->assertCount(6, $transactions['transactions']); + $this->assertCount(8, $transactions['transactions']); $this->assertEquals(3020, $transactions['credits']->getValue()); - $this->assertEquals(1540, $transactions['debits']->getValue()); + $this->assertEquals(1840, $transactions['debits']->getValue()); + + # Check that all the different transaction types are there + $transaction_types = [ + 'expenses' => 1, + 'fares' => 3, + 'ground_handling' => 1, + 'pilot_pay' => 2, # debit on the airline, credit to the pilot + 'subfleet_expense' => 1, + ]; + + foreach($transaction_types as $type => $count) { + $find = $transactions['transactions'] + ->where('transaction_group', $type); + + $this->assertEquals($count, $find->count()); + } } }