Skip to content

Commit

Permalink
✨ create api endpoint for creating trips manually [closed-beta] (#2145)
Browse files Browse the repository at this point in the history
  • Loading branch information
MrKrisKrisu authored Nov 23, 2023
1 parent 55bb49c commit 5990031
Show file tree
Hide file tree
Showing 15 changed files with 370 additions and 62 deletions.
68 changes: 68 additions & 0 deletions app/Http/Controllers/API/v1/TripController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

namespace App\Http\Controllers\API\v1;

use App\Enum\HafasTravelType;
use App\Http\Controllers\Backend\Transport\ManualTripCreator;
use App\Http\Resources\HafasTripResource;
use App\Models\HafasOperator;
use App\Models\HafasTrip;
use App\Models\TrainStation;
use App\Models\TrainStopover;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rules\Enum;

class TripController extends Controller
{

/**
* Undocumented beta endpoint - only specific users have access
*
* @param Request $request
*
* @return HafasTripResource
*
* @todo add stopovers
* @todo currently the stations need to be in the database. We need to add a fallback to HAFAS.
* -> later solve the problem for non-existing stations
*/
public function createTrip(Request $request): HafasTripResource {
if (!auth()->user()?->hasRole('closed-beta')) {
abort(403, 'this endpoint is currently only available for beta users');
}

$validated = $request->validate([
'category' => ['required', new Enum(HafasTravelType::class)],
'lineName' => ['required'],
'journey_number' => ['nullable', 'numeric', 'min:1'],
'operator_id' => ['nullable', 'numeric', 'exists:hafas_operators,id'],
'originId' => ['required', 'exists:train_stations,ibnr'],
'originDeparturePlanned' => ['required', 'date'],
'destinationId' => ['required', 'exists:train_stations,ibnr'],
'destinationArrivalPlanned' => ['required', 'date'],
]);

DB::beginTransaction();

$creator = new ManualTripCreator();

$creator->category = HafasTravelType::from($validated['category']);
$creator->lineName = $validated['lineName'];
$creator->journeyNumber = $validated['journey_number'];
$creator->operator = HafasOperator::find($validated['operator_id']);
$creator->origin = TrainStation::where('ibnr', $validated['originId'])->firstOrFail();
$creator->originDeparturePlanned = Carbon::parse($validated['originDeparturePlanned']);
$creator->destination = TrainStation::where('ibnr', $validated['destinationId'])->firstOrFail();
$creator->destinationArrivalPlanned = Carbon::parse($validated['destinationArrivalPlanned']);

$trip = $creator->createTrip();
$creator->createOriginStopover();
$creator->createDestinationStopover();

DB::commit();

return new HafasTripResource($trip);
}
}
79 changes: 79 additions & 0 deletions app/Http/Controllers/Backend/Transport/ManualTripCreator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

namespace App\Http\Controllers\Backend\Transport;

use App\Enum\HafasTravelType;
use App\Enum\TripSource;
use App\Http\Controllers\Backend\Transport\ManualTripCreator as TripBackend;
use App\Http\Controllers\Controller;
use App\Models\HafasOperator;
use App\Models\HafasTrip;
use App\Models\TrainStation;
use App\Models\TrainStopover;
use Carbon\Carbon;
use Illuminate\Support\Str;

class ManualTripCreator extends Controller
{

private ?HafasTrip $trip;
//
public HafasTravelType $category;
public string $lineName;
public ?int $journeyNumber;
public ?HafasOperator $operator;
public TrainStation $origin;
public Carbon $originDeparturePlanned;
public TrainStation $destination;
public Carbon $destinationArrivalPlanned;

public function createTrip(): HafasTrip {
$this->trip = HafasTrip::create([
'trip_id' => TripBackend::generateUniqueTripId(),
'category' => $this->category,
'number' => $this->lineName,
'linename' => $this->lineName,
'journey_number' => $this->journeyNumber,
'operator_id' => $this->operator->id ?? null,
'origin' => $this->origin->ibnr,
'destination' => $this->destination->ibnr,
'departure' => $this->originDeparturePlanned,
'arrival' => $this->destinationArrivalPlanned,
'source' => TripSource::USER,
'user_id' => auth()->user()?->id ?? null,
]);
return $this->trip;
}

public function createOriginStopover(): TrainStopover {
if ($this->trip === null) {
throw new \InvalidArgumentException('Cannot create stopover without trip');
}
return TrainStopover::create([
'trip_id' => $this->trip->trip_id,
'train_station_id' => $this->origin->id,
'arrival_planned' => $this->originDeparturePlanned,
'departure_planned' => $this->originDeparturePlanned,
]);
}

public function createDestinationStopover(): TrainStopover {
if ($this->trip === null) {
throw new \InvalidArgumentException('Cannot create stopover without trip');
}
return TrainStopover::create([
'trip_id' => $this->trip->trip_id,
'train_station_id' => $this->destination->id,
'arrival_planned' => $this->destinationArrivalPlanned,
'departure_planned' => $this->destinationArrivalPlanned,
]);
}

public static function generateUniqueTripId(): string {
$tripId = Str::uuid();
while (HafasTrip::where('trip_id', $tripId)->exists()) {
return self::generateUniqueTripId();
}
return $tripId;
}
}
69 changes: 20 additions & 49 deletions app/Http/Controllers/Frontend/Admin/TripController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
namespace App\Http\Controllers\Frontend\Admin;

use App\Enum\HafasTravelType;
use App\Http\Controllers\Backend\Transport\ManualTripCreator;
use App\Http\Controllers\Backend\Transport\ManualTripCreator as TripBackend;
use App\Models\HafasOperator;
use App\Models\HafasTrip;
use App\Models\TrainStation;
use App\Models\TrainStopover;
use Carbon\Carbon;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rules\Enum;
Expand All @@ -23,19 +27,6 @@ public function renderTrip(string $id): View {
]);
}

/**
* This form is currently for testing purposes only.
* Admins can create a trip with manually entered data.
* Users can check in to this trip.
* It should be tested if the trip is created correctly and all data required for the trip is present,
* so no (500) errors occur.
*
* @return View
*/
public function renderForm(): View {
return view('admin.trip.create');
}

public function createTrip(Request $request): RedirectResponse {
$validated = $request->validate([
'origin' => ['required', 'numeric', 'exists:train_stations,ibnr'],
Expand All @@ -49,49 +40,29 @@ public function createTrip(Request $request): RedirectResponse {
'journey_number' => ['required', 'numeric'],
]);

$departure = str_contains($validated['departure'], '+') && str_contains($validated['departure'], '-')
? $validated['departure'] : $validated['departure'] . '+00:00';
$arrival = str_contains($validated['arrival'], '+') && str_contains($validated['arrival'], '-')
? $validated['arrival'] : $validated['arrival'] . '+00:00';
$creator = new ManualTripCreator();

$originStation = TrainStation::where('ibnr', $validated['origin'])->firstOrFail();
$destinationStation = TrainStation::where('ibnr', $validated['destination'])->firstOrFail();
$creator->category = HafasTravelType::from($validated['category']);
$creator->lineName = $validated['linename'];
$creator->journeyNumber = $validated['journey_number'];
$creator->operator = HafasOperator::find($validated['operator_id']);
$creator->origin = TrainStation::where('ibnr', $validated['origin'])->firstOrFail();
$creator->originDeparturePlanned = Carbon::parse(str_contains($validated['departure'], '+') && str_contains($validated['departure'], '-')
? $validated['departure'] : $validated['departure'] . '+00:00');
$creator->destination = TrainStation::where('ibnr', $validated['destination'])->firstOrFail();
$creator->destinationArrivalPlanned = Carbon::parse(str_contains($validated['arrival'], '+') && str_contains($validated['arrival'], '-')
? $validated['arrival'] : $validated['arrival'] . '+00:00');

$trip = HafasTrip::create([
'trip_id' => strtr('manual-userId-uuid', [
'userId' => auth()->id(),
'uuid' => uniqid('', true),
]),
'category' => $validated['category'],
'number' => $validated['number'],
'linename' => $validated['linename'],
'journey_number' => $validated['journey_number'],
'operator_id' => $validated['operator_id'],
'origin' => $validated['origin'],
'destination' => $validated['destination'],
'departure' => $departure,
'arrival' => $arrival,
]);
//Origin stopover
TrainStopover::create([
'trip_id' => $trip->trip_id,
'train_station_id' => $originStation->id,
'arrival_planned' => $departure,
'departure_planned' => $departure,
]);
//Destination stopover
TrainStopover::create([
'trip_id' => $trip->trip_id,
'train_station_id' => $destinationStation->id,
'arrival_planned' => $arrival,
'departure_planned' => $arrival,
]);
$trip = $creator->createTrip();
$creator->createOriginStopover();
$creator->createDestinationStopover();
$trip->refresh();

return redirect()->route('trains.trip', [
'tripID' => $trip->trip_id,
'lineName' => $trip->linename,
'start' => $trip->origin,
'departure' => $departure,
'departure' => $trip->departure->toIso8601String(),
]);
}
}
3 changes: 3 additions & 0 deletions app/Models/HafasOperator.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

/**
* @todo rename table only to "Operator" (or "TransportOperator", ..., but not HAFAS)
*/
class HafasOperator extends Model
{
use HasFactory;
Expand Down
36 changes: 31 additions & 5 deletions app/Models/HafasTrip.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,28 @@
use Illuminate\Database\Eloquent\Relations\HasOne;

/**
* @property $stopovers
* @property PolyLine $polyLine
* @property PolyLine $polyline
* @property $linename
* @property int $id
* @property string $trip_id
* @property HafasTravelType $category
* @property string $number
* @property string $linename
* @property string $journey_number
* @property int $operator_id
* @property int $origin
* @property int $destination
* @property int $polyline_id
* @property UTCDateTime $departure
* @property UTCDateTime $arrival
* @property UTCDateTime $last_refreshed
* @property TripSource $source
* @property int $user_id
* @property $stopovers
* @property PolyLine $polyLine
*
* @todo rename table only to "Trip" (without Hafas)
* @todo rename "linename" to "line_name" (or something else, but not "linename")
* @todo migrate origin & destination to use "id" instead of "ibnr" and rename to "origin_id" & "destination_id"
* @todo is "delay" still needed? We save planned and real in the stopovers. check.
*/
class HafasTrip extends Model
{
Expand All @@ -24,7 +42,7 @@ class HafasTrip extends Model

protected $fillable = [
'trip_id', 'category', 'number', 'linename', 'journey_number', 'operator_id', 'origin', 'destination',
'polyline_id', 'departure', 'arrival', 'delay', 'source', 'last_refreshed',
'polyline_id', 'departure', 'arrival', 'delay', 'source', 'user_id', 'last_refreshed',
];
protected $hidden = ['created_at', 'updated_at'];
protected $casts = [
Expand All @@ -40,6 +58,7 @@ class HafasTrip extends Model
'arrival' => UTCDateTime::class,
'last_refreshed' => 'datetime',
'source' => TripSource::class,
'user_id' => 'integer',
];

public function polyline(): HasOne {
Expand Down Expand Up @@ -67,4 +86,11 @@ public function stopovers(): HasMany {
public function checkins(): HasMany {
return $this->hasMany(TrainCheckin::class, 'trip_id', 'trip_id');
}

/**
* If this trip was created by a user, this model belongs to the user, so they can edit and delete it.
*/
public function user(): BelongsTo {
return $this->belongsTo(User::class, 'user_id', 'id');
}
}
11 changes: 7 additions & 4 deletions app/Models/Status.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,19 @@
* @property int event_id
* @property StatusVisibility visibility
* @property TrainCheckin $trainCheckin
*
* @todo merge model with "TrainCheckin" (later only "Checkin") because the difference between trip sources (HAFAS,
* User, and future sources) should be handled in the Trip model.
*/
class Status extends Model
{

use HasFactory;

protected $fillable = ['user_id', 'body', 'business', 'visibility', 'event_id', 'tweet_id', 'mastodon_post_id'];
protected $hidden = ['user_id', 'business'];
protected $appends = ['favorited', 'socialText', 'statusInvisibleToMe', 'description'];
protected $casts = [
protected $fillable = ['user_id', 'body', 'business', 'visibility', 'event_id', 'tweet_id', 'mastodon_post_id'];
protected $hidden = ['user_id', 'business'];
protected $appends = ['favorited', 'socialText', 'statusInvisibleToMe', 'description'];
protected $casts = [
'id' => 'integer',
'user_id' => 'integer',
'business' => Business::class,
Expand Down
4 changes: 4 additions & 0 deletions app/Models/TrainCheckin.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
* @property TrainStopover $destination_stopover
* @property TrainStation $originStation
* @property TrainStation $destinationStation
*
* @todo rename table to "Checkin" (without Train - we have more than just trains)
* @todo merge model with "Status" because the difference between trip sources (HAFAS,
* User, and future sources) should be handled in the Trip model.
*/
class TrainCheckin extends Model
{
Expand Down
1 change: 1 addition & 0 deletions app/Models/TrainStation.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* @property Event[] $events
* @property Carbon $created_at
* @property Carbon $updated_at
* @todo rename table to "Station" (without Train - we have more than just trains)
*/
class TrainStation extends Model
{
Expand Down
6 changes: 5 additions & 1 deletion app/Models/TrainStopover.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

/**
* @todo rename table to "Stopover" (without Train - we have more than just trains)
* @todo rename "train_station_id" to "station_id" - we have more than just trains.
* @todo rename "cancelled" to "is_cancelled" - or split into "is_arrival_cancelled" and "is_departure_cancelled"? need to think about this.
*/
class TrainStopover extends Model
{
use HasFactory;
Expand Down
6 changes: 6 additions & 0 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@
* @property string language
* @property Carbon last_login
* @property Status[] $statuses
*
* @todo replace "role" with an explicit permission system - e.g. spatie/laravel-permission
* @todo replace "experimental" also with an explicit permission system - user can add self to "experimental" group
* @todo rename home_id to home_station_id
* @todo rename mapprovider to map_provider
* @todo remove "twitterUrl" (Twitter isn't used by traewelling anymore)
*/
class User extends Authenticatable implements MustVerifyEmail
{
Expand Down
Loading

0 comments on commit 5990031

Please sign in to comment.