From 46d8fb125a6b6463a531b53e89663de3f0eb6a8d Mon Sep 17 00:00:00 2001 From: Nabeel Shahzad Date: Thu, 22 Mar 2018 17:48:57 -0500 Subject: [PATCH] Add fare import/exporter #194 --- .../Controllers/Admin/AircraftController.php | 9 +- .../Controllers/Admin/AirportController.php | 6 +- .../Controllers/Admin/ExpenseController.php | 4 +- app/Http/Controllers/Admin/FareController.php | 91 +++++++++++++++---- .../Controllers/Admin/FlightController.php | 4 +- .../Controllers/Admin/SubfleetController.php | 5 +- app/Routes/admin.php | 2 + app/Services/ExportService.php | 13 +++ app/Services/ImportExport/FareExporter.php | 39 ++++++++ app/Services/ImportExport/FareImporter.php | 53 +++++++++++ .../ImportExport/SubfleetImporter.php | 34 +++++++ app/Services/ImportService.php | 33 ++++++- resources/views/admin/fares/import.blade.php | 5 + resources/views/admin/fares/index.blade.php | 9 +- tests/ImporterTest.php | 65 +++++++++++-- tests/data/fares.csv | 4 + tests/data/subfleets.csv | 4 +- 17 files changed, 322 insertions(+), 58 deletions(-) create mode 100644 app/Services/ImportExport/FareExporter.php create mode 100644 app/Services/ImportExport/FareImporter.php create mode 100644 resources/views/admin/fares/import.blade.php create mode 100644 tests/data/fares.csv diff --git a/app/Http/Controllers/Admin/AircraftController.php b/app/Http/Controllers/Admin/AircraftController.php index 45810666..cc06d0c7 100644 --- a/app/Http/Controllers/Admin/AircraftController.php +++ b/app/Http/Controllers/Admin/AircraftController.php @@ -174,10 +174,8 @@ class AircraftController extends Controller } /** - * * @param Request $request * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View - * @throws \League\Csv\Exception * @throws \Illuminate\Validation\ValidationException */ public function import(Request $request) @@ -189,13 +187,12 @@ class AircraftController extends Controller if ($request->isMethod('post')) { ImportRequest::validate($request); - $path = Storage::putFileAs( - 'import', $request->file('csv_file'), 'aircraft' + 'import', $request->file('csv_file'), 'import_aircraft.csv' ); $path = storage_path('app/'.$path); - Log::info('Uploaded flights import file to '.$path); + Log::info('Uploaded aircraft import file to '.$path); $logs = $this->importSvc->importAircraft($path); } @@ -208,7 +205,7 @@ class AircraftController extends Controller * @param Aircraft|null $aircraft * @return mixed */ - protected function return_expenses_view(?Aircraft $aircraft) + protected function return_expenses_view(Aircraft $aircraft) { $aircraft->refresh(); diff --git a/app/Http/Controllers/Admin/AirportController.php b/app/Http/Controllers/Admin/AirportController.php index 402e2778..15bc3dcb 100644 --- a/app/Http/Controllers/Admin/AirportController.php +++ b/app/Http/Controllers/Admin/AirportController.php @@ -198,7 +198,6 @@ class AirportController extends Controller * * @param Request $request * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View - * @throws \League\Csv\Exception * @throws \Illuminate\Validation\ValidationException */ public function import(Request $request) @@ -210,13 +209,12 @@ class AirportController extends Controller if ($request->isMethod('post')) { ImportRequest::validate($request); - $path = Storage::putFileAs( - 'import', $request->file('csv_file'), 'airports' + 'import', $request->file('csv_file'), 'import_airports.csv' ); $path = storage_path('app/'.$path); - Log::info('Uploaded airports import file to '.$path); + Log::info('Uploaded airport import file to '.$path); $logs = $this->importSvc->importAirports($path); } diff --git a/app/Http/Controllers/Admin/ExpenseController.php b/app/Http/Controllers/Admin/ExpenseController.php index e6385957..b252d3f7 100644 --- a/app/Http/Controllers/Admin/ExpenseController.php +++ b/app/Http/Controllers/Admin/ExpenseController.php @@ -198,7 +198,6 @@ class ExpenseController extends Controller * * @param Request $request * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View - * @throws \League\Csv\Exception * @throws \Illuminate\Validation\ValidationException */ public function import(Request $request) @@ -210,9 +209,8 @@ class ExpenseController extends Controller if ($request->isMethod('post')) { ImportRequest::validate($request); - $path = Storage::putFileAs( - 'import', $request->file('csv_file'), 'expenses' + 'import', $request->file('csv_file'), 'import_expenses.csv' ); $path = storage_path('app/'.$path); diff --git a/app/Http/Controllers/Admin/FareController.php b/app/Http/Controllers/Admin/FareController.php index ac952753..721d0b9f 100644 --- a/app/Http/Controllers/Admin/FareController.php +++ b/app/Http/Controllers/Admin/FareController.php @@ -3,13 +3,18 @@ namespace App\Http\Controllers\Admin; use App\Http\Requests\CreateFareRequest; +use App\Http\Requests\ImportRequest; use App\Http\Requests\UpdateFareRequest; use App\Interfaces\Controller; use App\Repositories\FareRepository; +use App\Services\ExportService; +use App\Services\ImportService; use Flash; use Illuminate\Http\Request; +use Log; use Prettus\Repository\Criteria\RequestCriteria; use Response; +use Storage; /** * Class FareController @@ -17,16 +22,20 @@ use Response; */ class FareController extends Controller { - private $fareRepository; + private $fareRepo, + $importSvc; /** * FareController constructor. * @param FareRepository $fareRepo + * @param ImportService $importSvc */ public function __construct( - FareRepository $fareRepo + FareRepository $fareRepo, + ImportService $importSvc ) { - $this->fareRepository = $fareRepo; + $this->fareRepo = $fareRepo; + $this->importSvc = $importSvc; } /** @@ -37,8 +46,8 @@ class FareController extends Controller */ public function index(Request $request) { - $this->fareRepository->pushCriteria(new RequestCriteria($request)); - $fares = $this->fareRepository->all(); + $this->fareRepo->pushCriteria(new RequestCriteria($request)); + $fares = $this->fareRepo->all(); return view('admin.fares.index') ->with('fares', $fares); @@ -63,9 +72,9 @@ class FareController extends Controller public function store(CreateFareRequest $request) { $input = $request->all(); - $fare = $this->fareRepository->create($input); - Flash::success('Fare saved successfully.'); + $fare = $this->fareRepo->create($input); + Flash::success('Fare saved successfully.'); return redirect(route('admin.fares.index')); } @@ -76,10 +85,9 @@ class FareController extends Controller */ public function show($id) { - $fare = $this->fareRepository->findWithoutFail($id); + $fare = $this->fareRepo->findWithoutFail($id); if (empty($fare)) { Flash::error('Fare not found'); - return redirect(route('admin.fares.index')); } @@ -93,10 +101,9 @@ class FareController extends Controller */ public function edit($id) { - $fare = $this->fareRepository->findWithoutFail($id); + $fare = $this->fareRepo->findWithoutFail($id); if (empty($fare)) { Flash::error('Fare not found'); - return redirect(route('admin.fares.index')); } @@ -112,16 +119,15 @@ class FareController extends Controller */ public function update($id, UpdateFareRequest $request) { - $fare = $this->fareRepository->findWithoutFail($id); + $fare = $this->fareRepo->findWithoutFail($id); if (empty($fare)) { Flash::error('Fare not found'); - return redirect(route('admin.fares.index')); } - $fare = $this->fareRepository->update($request->all(), $id); - Flash::success('Fare updated successfully.'); + $fare = $this->fareRepo->update($request->all(), $id); + Flash::success('Fare updated successfully.'); return redirect(route('admin.fares.index')); } @@ -132,16 +138,63 @@ class FareController extends Controller */ public function destroy($id) { - $fare = $this->fareRepository->findWithoutFail($id); + $fare = $this->fareRepo->findWithoutFail($id); if (empty($fare)) { Flash::error('Fare not found'); - return redirect(route('admin.fares.index')); } - $this->fareRepository->delete($id); - Flash::success('Fare deleted successfully.'); + $this->fareRepo->delete($id); + Flash::success('Fare deleted successfully.'); return redirect(route('admin.fares.index')); } + + /** + * Run the aircraft exporter + * @param Request $request + * @return \Symfony\Component\HttpFoundation\BinaryFileResponse + * @throws \League\Csv\Exception + */ + public function export(Request $request) + { + $exporter = app(ExportService::class); + $fares = $this->fareRepo->all(); + + $path = $exporter->exportFares($fares); + return response() + ->download($path, 'fares.csv', [ + 'content-type' => 'text/csv', + ]) + ->deleteFileAfterSend(true); + } + + /** + * + * @param Request $request + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + * @throws \Illuminate\Validation\ValidationException + */ + public function import(Request $request) + { + $logs = [ + 'success' => [], + 'failed' => [], + ]; + + if ($request->isMethod('post')) { + ImportRequest::validate($request); + $path = Storage::putFileAs( + 'import', $request->file('csv_file'), 'import_fares.csv' + ); + + $path = storage_path('app/'.$path); + Log::info('Uploaded fares import file to '.$path); + $logs = $this->importSvc->importFares($path); + } + + return view('admin.fares.import', [ + 'logs' => $logs, + ]); + } } diff --git a/app/Http/Controllers/Admin/FlightController.php b/app/Http/Controllers/Admin/FlightController.php index a3af877d..052082e0 100644 --- a/app/Http/Controllers/Admin/FlightController.php +++ b/app/Http/Controllers/Admin/FlightController.php @@ -334,7 +334,7 @@ class FlightController extends Controller * * @param Request $request * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View - * @throws \League\Csv\Exception + * @throws \Illuminate\Validation\ValidationException */ public function import(Request $request) { @@ -345,7 +345,7 @@ class FlightController extends Controller if ($request->isMethod('post')) { $path = Storage::putFileAs( - 'import', $request->file('csv_file'), 'flights' + 'import', $request->file('csv_file'), 'import_flights.csv' ); $path = storage_path('app/'.$path); diff --git a/app/Http/Controllers/Admin/SubfleetController.php b/app/Http/Controllers/Admin/SubfleetController.php index 6b46578a..4506be05 100644 --- a/app/Http/Controllers/Admin/SubfleetController.php +++ b/app/Http/Controllers/Admin/SubfleetController.php @@ -245,7 +245,6 @@ class SubfleetController extends Controller * * @param Request $request * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View - * @throws \League\Csv\Exception * @throws \Illuminate\Validation\ValidationException */ public function import(Request $request) @@ -259,11 +258,11 @@ class SubfleetController extends Controller ImportRequest::validate($request); $path = Storage::putFileAs( - 'import', $request->file('csv_file'), 'subfleets' + 'import', $request->file('csv_file'), 'import_subfleets.csv' ); $path = storage_path('app/'.$path); - Log::info('Uploaded flights import file to '.$path); + Log::info('Uploaded subfleets import file to '.$path); $logs = $this->importSvc->importSubfleets($path); } diff --git a/app/Routes/admin.php b/app/Routes/admin.php index 104b910b..214feb8f 100644 --- a/app/Routes/admin.php +++ b/app/Routes/admin.php @@ -30,6 +30,8 @@ Route::group([ Route::resource('expenses', 'ExpenseController'); # fares + Route::get('fares/export', 'FareController@export')->name('fares.export'); + Route::match(['get', 'post'], 'fares/import', 'FareController@import')->name('fares.import'); Route::resource('fares', 'FareController'); # finances diff --git a/app/Services/ExportService.php b/app/Services/ExportService.php index 066e180d..cbcf3545 100644 --- a/app/Services/ExportService.php +++ b/app/Services/ExportService.php @@ -7,6 +7,7 @@ use App\Interfaces\Service; use App\Services\ImportExport\AircraftExporter; use App\Services\ImportExport\AirportExporter; use App\Services\ImportExport\ExpenseExporter; +use App\Services\ImportExport\FareExporter; use App\Services\ImportExport\FlightExporter; use Illuminate\Support\Collection; use League\Csv\CharsetConverter; @@ -97,6 +98,18 @@ class ExportService extends Service return $this->runExport($expenses, $exporter); } + /** + * Export all of the fares + * @param Collection $fares + * @return mixed + * @throws \League\Csv\CannotInsertRecord + */ + public function exportFares($fares) + { + $exporter = new FareExporter(); + return $this->runExport($fares, $exporter); + } + /** * Export all of the flights * @param Collection $flights diff --git a/app/Services/ImportExport/FareExporter.php b/app/Services/ImportExport/FareExporter.php new file mode 100644 index 00000000..b3bef9f9 --- /dev/null +++ b/app/Services/ImportExport/FareExporter.php @@ -0,0 +1,39 @@ +{$column}; + } + + return $ret; + } +} diff --git a/app/Services/ImportExport/FareImporter.php b/app/Services/ImportExport/FareImporter.php new file mode 100644 index 00000000..d57767c6 --- /dev/null +++ b/app/Services/ImportExport/FareImporter.php @@ -0,0 +1,53 @@ + $row['code'], + ], $row); + + try { + $fare->save(); + } catch(\Exception $e) { + $this->errorLog('Error in row '.$index.': '.$e->getMessage()); + return false; + } + + $this->log('Imported '.$row['code'].' '.$row['name']); + return true; + } +} diff --git a/app/Services/ImportExport/SubfleetImporter.php b/app/Services/ImportExport/SubfleetImporter.php index b383e909..0f242ef6 100644 --- a/app/Services/ImportExport/SubfleetImporter.php +++ b/app/Services/ImportExport/SubfleetImporter.php @@ -3,7 +3,9 @@ namespace App\Services\ImportExport; use App\Interfaces\ImportExport; +use App\Models\Fare; use App\Models\Subfleet; +use App\Services\FareService; /** * Import subfleets @@ -21,8 +23,19 @@ class SubfleetImporter extends ImportExport 'airline', 'type', 'name', + 'fares', ]; + private $fareSvc; + + /** + * FlightImportExporter constructor. + */ + public function __construct() + { + $this->fareSvc = app(FareService::class); + } + /** * Import a flight, parse out the different rows * @param array $row @@ -45,7 +58,28 @@ class SubfleetImporter extends ImportExport return false; } + $this->processFares($subfleet, $row['fares']); + $this->log('Imported '.$row['type']); return true; } + + /** + * Parse all of the fares in the multi-format + * @param Subfleet $subfleet + * @param $col + */ + protected function processFares(Subfleet &$subfleet, $col): void + { + $fares = $this->parseMultiColumnValues($col); + foreach ($fares as $fare_code => $fare_attributes) { + if (\is_int($fare_code)) { + $fare_code = $fare_attributes; + $fare_attributes = []; + } + + $fare = Fare::firstOrCreate(['code' => $fare_code], ['name' => $fare_code]); + $this->fareSvc->setForSubfleet($subfleet, $fare, $fare_attributes); + } + } } diff --git a/app/Services/ImportService.php b/app/Services/ImportService.php index 5989f3f5..587ea0eb 100644 --- a/app/Services/ImportService.php +++ b/app/Services/ImportService.php @@ -10,6 +10,7 @@ use App\Repositories\FlightRepository; use App\Services\ImportExport\AircraftImporter; use App\Services\ImportExport\AirportImporter; use App\Services\ImportExport\ExpenseImporter; +use App\Services\ImportExport\FareImporter; use App\Services\ImportExport\FlightImporter; use App\Services\ImportExport\SubfleetImporter; use Illuminate\Validation\ValidationException; @@ -112,7 +113,6 @@ class ImportService extends Service * @param string $csv_file * @param bool $delete_previous * @return mixed - * @throws \League\Csv\Exception * @throws ValidationException */ public function importAircraft($csv_file, bool $delete_previous = true) @@ -135,7 +135,7 @@ class ImportService extends Service * @param string $csv_file * @param bool $delete_previous * @return mixed - * @throws \League\Csv\Exception + * @throws ValidationException */ public function importAirports($csv_file, bool $delete_previous = true) { @@ -157,7 +157,7 @@ class ImportService extends Service * @param string $csv_file * @param bool $delete_previous * @return mixed - * @throws \League\Csv\Exception + * @throws ValidationException */ public function importExpenses($csv_file, bool $delete_previous = true) { @@ -174,12 +174,35 @@ class ImportService extends Service return $this->runImport($reader, $importer); } + /** + * Import fares + * @param string $csv_file + * @param bool $delete_previous + * @return mixed + * @throws ValidationException + */ + public function importFares($csv_file, bool $delete_previous = true) + { + if ($delete_previous) { + # TODO: Delete all from: fares + } + + $reader = $this->openCsv($csv_file); + if (!$reader) { + # TODO: Throw an error + return false; + } + + $importer = new FareImporter(); + return $this->runImport($reader, $importer); + } + /** * Import flights * @param string $csv_file * @param bool $delete_previous * @return mixed - * @throws \League\Csv\Exception + * @throws ValidationException */ public function importFlights($csv_file, bool $delete_previous = true) { @@ -202,7 +225,7 @@ class ImportService extends Service * @param string $csv_file * @param bool $delete_previous * @return mixed - * @throws \League\Csv\Exception + * @throws ValidationException */ public function importSubfleets($csv_file, bool $delete_previous = true) { diff --git a/resources/views/admin/fares/import.blade.php b/resources/views/admin/fares/import.blade.php new file mode 100644 index 00000000..523612ad --- /dev/null +++ b/resources/views/admin/fares/import.blade.php @@ -0,0 +1,5 @@ +@extends('admin.app') +@section('title', 'Import Fares') +@section('content') + @include('admin.common.import', ['route' => 'admin.fares.import']) +@endsection diff --git a/resources/views/admin/fares/index.blade.php b/resources/views/admin/fares/index.blade.php index 6b007351..f0599165 100644 --- a/resources/views/admin/fares/index.blade.php +++ b/resources/views/admin/fares/index.blade.php @@ -2,12 +2,9 @@ @section('title', 'Fares') @section('actions') -
  • - - - Add New - -
  • +
  • Export to CSV
  • +
  • Import from CSV
  • +
  • Add New
  • @endsection @section('content') diff --git a/tests/ImporterTest.php b/tests/ImporterTest.php index 36f8b6c4..124f1ad4 100644 --- a/tests/ImporterTest.php +++ b/tests/ImporterTest.php @@ -40,8 +40,8 @@ class ImporterTest extends TestCase $fare_economy = factory(App\Models\Fare::class)->create(['code' => 'Y', 'capacity' => 150]); $fare_svc->setForSubfleet($subfleet, $fare_economy); - $fare_economy = factory(App\Models\Fare::class)->create(['code' => 'B', 'capacity' => 20]); - $fare_svc->setForSubfleet($subfleet, $fare_economy); + $fare_business = factory(App\Models\Fare::class)->create(['code' => 'B', 'capacity' => 20]); + $fare_svc->setForSubfleet($subfleet, $fare_business); # Add first class $fare_first = factory(App\Models\Fare::class)->create(['code' => 'F', 'capacity' => 10]); @@ -265,7 +265,6 @@ class ImporterTest extends TestCase /** * Try importing the aicraft in the airports. Should fail * @expectedException \Illuminate\Validation\ValidationException - * @throws \League\Csv\Exception */ public function testInvalidFileImport(): void { @@ -275,7 +274,7 @@ class ImporterTest extends TestCase /** * Test the importing of expenses - * @throws \League\Csv\Exception + * @throws \Illuminate\Validation\ValidationException */ public function testExpenseImporter(): void { @@ -310,9 +309,44 @@ class ImporterTest extends TestCase $this->assertEquals($aircraft->id, $mnt->ref_class_id); } + /** + * @throws \Illuminate\Validation\ValidationException + */ + public function testFareImporter(): void + { + $file_path = base_path('tests/data/fares.csv'); + $this->importSvc->importFares($file_path); + + $fares = \App\Models\Fare::all(); + + $y_class = $fares->where('code', 'Y')->first(); + $this->assertEquals('Economy', $y_class->name); + $this->assertEquals(100, $y_class->price); + $this->assertEquals(0, $y_class->cost); + $this->assertEquals(200, $y_class->capacity); + $this->assertEquals(true, $y_class->active); + $this->assertEquals('This is the economy class', $y_class->notes); + + $b_class = $fares->where('code', 'B')->first(); + $this->assertEquals('Business', $b_class->name); + $this->assertEquals(500, $b_class->price); + $this->assertEquals(250, $b_class->cost); + $this->assertEquals(10, $b_class->capacity); + $this->assertEquals('This is business class', $b_class->notes); + $this->assertEquals(false, $b_class->active); + + $f_class = $fares->where('code', 'F')->first(); + $this->assertEquals('First-Class', $f_class->name); + $this->assertEquals(800, $f_class->price); + $this->assertEquals(350, $f_class->cost); + $this->assertEquals(5, $f_class->capacity); + $this->assertEquals('', $f_class->notes); + $this->assertEquals(true, $f_class->active); + } + /** * Test the flight importer - * @throws \League\Csv\Exception + * @throws \Illuminate\Validation\ValidationException */ public function testFlightImporter(): void { @@ -374,7 +408,7 @@ class ImporterTest extends TestCase } /** - * @throws \League\Csv\Exception + * @throws \Illuminate\Validation\ValidationException */ public function testAircraftImporter(): void { @@ -395,7 +429,7 @@ class ImporterTest extends TestCase } /** - * @throws \League\Csv\Exception + * @throws \Illuminate\Validation\ValidationException */ public function testAirportImporter(): void { @@ -415,10 +449,12 @@ class ImporterTest extends TestCase /** * Test importing the subfleets - * @throws \League\Csv\Exception + * @throws \Illuminate\Validation\ValidationException */ public function testSubfleetImporter(): void { + $fare_economy = factory(App\Models\Fare::class)->create(['code' => 'Y', 'capacity' => 150]); + $fare_business = factory(App\Models\Fare::class)->create(['code' => 'B', 'capacity' => 20]); $airline = factory(App\Models\Airline::class)->create(['icao' => 'VMS']); $file_path = base_path('tests/data/subfleets.csv'); @@ -433,5 +469,18 @@ class ImporterTest extends TestCase $this->assertEquals($airline->id, $subfleet->id); $this->assertEquals('A32X', $subfleet->type); $this->assertEquals('Airbus A320', $subfleet->name); + + // get the fares + $fares = $this->fareSvc->getForSubfleet($subfleet); + + $eco = $fares->where('code', 'Y')->first(); + $this->assertEquals($fare_economy->capacity, $eco->capacity); + $this->assertEquals($fare_economy->price, $eco->price); + $this->assertEquals($fare_economy->cost, $eco->cost); + + $busi = $fares->where('code', 'B')->first(); + $this->assertEquals(100, $busi->capacity); + $this->assertEquals(500, $busi->price); + $this->assertEquals($fare_business->cost, $busi->cost); } } diff --git a/tests/data/fares.csv b/tests/data/fares.csv new file mode 100644 index 00000000..391a9dbb --- /dev/null +++ b/tests/data/fares.csv @@ -0,0 +1,4 @@ +code,name,price,cost,capacity,notes,active +Y,Economy,100,0,200,This is the economy class,1 +B,Business,500,250,10,"This is business class",0 +F,First-Class,800,350,5,,1 diff --git a/tests/data/subfleets.csv b/tests/data/subfleets.csv index ce3d74b1..d8f99c05 100644 --- a/tests/data/subfleets.csv +++ b/tests/data/subfleets.csv @@ -1,2 +1,2 @@ -airline,type,name -VMS,A32X,Airbus A320 +airline,type,name,fares +VMS,A32X,Airbus A320,Y;B?capacity=100&price=500