Add fare import/exporter #194

This commit is contained in:
Nabeel Shahzad 2018-03-22 17:48:57 -05:00
parent a44204b185
commit 46d8fb125a
17 changed files with 322 additions and 58 deletions

View File

@ -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();

View File

@ -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);
}

View File

@ -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);

View File

@ -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,
]);
}
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,39 @@
<?php
namespace App\Services\ImportExport;
use App\Interfaces\ImportExport;
use App\Models\Fare;
/**
* The flight importer can be imported or export. Operates on rows
*
* @package App\Services\Import
*/
class FareExporter extends ImportExport
{
public $assetType = 'fare';
/**
* Set the current columns and other setup
*/
public function __construct()
{
self::$columns = FareImporter::$columns;
}
/**
* Import a flight, parse out the different rows
* @param Fare $fare
* @return array
*/
public function export(Fare $fare): array
{
$ret = [];
foreach(self::$columns as $column) {
$ret[$column] = $fare->{$column};
}
return $ret;
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace App\Services\ImportExport;
use App\Interfaces\ImportExport;
use App\Models\Fare;
/**
* Import aircraft
* @package App\Services\Import
*/
class FareImporter extends ImportExport
{
public $assetType = 'fare';
/**
* All of the columns that are in the CSV import
* Should match the database fields, for the most part
*/
public static $columns = [
'code',
'name',
'price',
'cost',
'capacity',
'notes',
'active',
];
/**
* Import a flight, parse out the different rows
* @param array $row
* @param int $index
* @return bool
*/
public function import(array $row, $index): bool
{
# Try to add or update
$fare = Fare::firstOrNew([
'code' => $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;
}
}

View File

@ -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);
}
}
}

View File

@ -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)
{

View File

@ -0,0 +1,5 @@
@extends('admin.app')
@section('title', 'Import Fares')
@section('content')
@include('admin.common.import', ['route' => 'admin.fares.import'])
@endsection

View File

@ -2,12 +2,9 @@
@section('title', 'Fares')
@section('actions')
<li>
<a href="{{ route('admin.fares.create') }}">
<i class="ti-plus"></i>
Add New
</a>
</li>
<li><a href="{{ route('admin.fares.export') }}"><i class="ti-plus"></i>Export to CSV</a></li>
<li><a href="{{ route('admin.fares.import') }}"><i class="ti-plus"></i>Import from CSV</a></li>
<li><a href="{{ route('admin.fares.create') }}"><i class="ti-plus"></i>Add New</a></li>
@endsection
@section('content')

View File

@ -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);
}
}

4
tests/data/fares.csv Normal file
View File

@ -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
1 code name price cost capacity notes active
2 Y Economy 100 0 200 This is the economy class 1
3 B Business 500 250 10 This is business class 0
4 F First-Class 800 350 5 1

View File

@ -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

1 airline type name fares
2 VMS A32X Airbus A320 Y;B?capacity=100&price=500