Updated scheduled tasks
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\DTOs\FlightStatData;
|
||||
use App\Models\Aircraft;
|
||||
use App\Models\Notification;
|
||||
use App\Models\UserFlight;
|
||||
use App\Services\FlightStatsService;
|
||||
use Carbon\CarbonInterface;
|
||||
use Illuminate\Console\Attributes\Description;
|
||||
use Illuminate\Console\Attributes\Signature;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
#[Signature('app:get-aircraft-info-for-arrived-flights')]
|
||||
#[Description('Search for flights that have arrived recently and try and determine any new aircraft info.')]
|
||||
class GetAircraftInfoForArrivedFlights extends Command
|
||||
{
|
||||
public function __construct(protected FlightStatsService $flightStats)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$now = now()->utc();
|
||||
|
||||
$userFlights = UserFlight::where('arrival_date', '<=', $now->copy()->subHour()->toDateTimeString())
|
||||
->where('auto_update', true)
|
||||
->whereNotNull('flight_number')
|
||||
->get();
|
||||
|
||||
$this->info("Found {$userFlights->count()} flights.");
|
||||
|
||||
foreach ($userFlights as $flight) {
|
||||
$this->processFlight($flight);
|
||||
}
|
||||
}
|
||||
|
||||
protected function processFlight(UserFlight $flight): void
|
||||
{
|
||||
preg_match('/^([A-Z]{2,3})(\d+)$/i', $flight->flight_number, $matches);
|
||||
|
||||
// Case 1: unparseable flight number on our end — disable and move on, no notification.
|
||||
if (empty($matches)) {
|
||||
$this->warn("Could not parse flight number: {$flight->flight_number}");
|
||||
Log::warning("Could not parse flight number for auto-update", ['flight_id' => $flight->id, 'flight_number' => $flight->flight_number]);
|
||||
$flight->update(['auto_update' => false]);
|
||||
return;
|
||||
}
|
||||
|
||||
$airlineCode = strtoupper($matches[1]);
|
||||
$flightNumber = $matches[2];
|
||||
$arrivalDate = $flight->arrival_date->setTimezone($flight->arrivalAirport->timezone);
|
||||
|
||||
$data = $this->flightStats->fetchFlightData($airlineCode, $flightNumber, $arrivalDate);
|
||||
|
||||
// Case 2: valid flight number, but the API had nothing for it.
|
||||
if (!$data) {
|
||||
$this->warn("No flight data returned for {$airlineCode}{$flightNumber}");
|
||||
$this->notifyLookupFailed($flight, $airlineCode, $flightNumber);
|
||||
return;
|
||||
}
|
||||
|
||||
// Case 3: API returned data, but for the wrong route — treat like a failed lookup.
|
||||
if ($data->departure_iata !== $flight->departureAirport->iata_code ||
|
||||
$data->arrival_iata !== $flight->arrivalAirport->iata_code) {
|
||||
$this->warn("Airport mismatch for {$airlineCode}{$flightNumber} — API: {$data->departure_iata}→{$data->arrival_iata}, expected: {$flight->departureAirport->iata_code}→{$flight->arrivalAirport->iata_code}");
|
||||
$this->notifyLookupFailed($flight, $airlineCode, $flightNumber);
|
||||
return;
|
||||
}
|
||||
|
||||
// Case 4/5: valid data — apply whatever changed, but always report every field.
|
||||
$this->applyAndNotify($flight, $data, $airlineCode, $flightNumber);
|
||||
}
|
||||
|
||||
protected function notifyLookupFailed(UserFlight $flight, string $airlineCode, string $flightNumber): void
|
||||
{
|
||||
Notification::create([
|
||||
'user_id' => $flight->user_id,
|
||||
'title' => "Auto update failed for {$flight->flight_number}",
|
||||
'body' => "We tried to find up-to-date information for {$airlineCode}{$flightNumber}, but couldn't find a reliable match. Please manually check the aircraft type, registration and departure/arrival times.",
|
||||
'url' => '/flights/' . $flight->id . '/edit',
|
||||
]);
|
||||
|
||||
$flight->update(['auto_update' => false]);
|
||||
}
|
||||
|
||||
protected function applyAndNotify(UserFlight $flight, FlightStatData $data, string $airlineCode, string $flightNumber): void
|
||||
{
|
||||
// Snapshot originals before we touch anything, so descriptions reflect the real "from" state.
|
||||
$originalRegistration = $flight->aircraft_registration;
|
||||
$originalDeparture = $flight->departure_date;
|
||||
$originalArrival = $flight->arrival_date;
|
||||
$originalAircraft = $flight->aircraft;
|
||||
|
||||
$updates = [];
|
||||
$aircraftNotFound = false;
|
||||
|
||||
if ($data->aircraft_registration && $data->aircraft_registration !== $originalRegistration) {
|
||||
$updates['aircraft_registration'] = $data->aircraft_registration;
|
||||
}
|
||||
|
||||
if ($data->estimated_departure_utc?->ne($originalDeparture)) {
|
||||
$updates['departure_date'] = $data->estimated_departure_utc;
|
||||
}
|
||||
|
||||
if ($data->estimated_arrival_utc?->ne($originalArrival)) {
|
||||
$updates['arrival_date'] = $data->estimated_arrival_utc;
|
||||
}
|
||||
|
||||
if ($data->equipment_iata && $originalAircraft?->iata_code !== $data->equipment_iata) {
|
||||
$match = $this->flightStats->guessAircraftFromIata($data->equipment_iata);
|
||||
|
||||
if ($match) {
|
||||
$updates['aircraft_id'] = $match->id;
|
||||
} else {
|
||||
$aircraftNotFound = true;
|
||||
Log::warning("No aircraft match for IATA code {$data->equipment_iata} on flight {$airlineCode}{$flightNumber}", [
|
||||
'flight_id' => $flight->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($updates)) {
|
||||
$flight->update($updates);
|
||||
$this->info("Updated flight {$airlineCode}{$flightNumber}: " . implode(', ', array_keys($updates)));
|
||||
} else {
|
||||
$this->info("No changes for {$airlineCode}{$flightNumber}");
|
||||
}
|
||||
|
||||
$flight->update(['auto_update' => false]);
|
||||
|
||||
$newAircraft = isset($updates['aircraft_id']) ? Aircraft::find($updates['aircraft_id']) : $originalAircraft;
|
||||
|
||||
$lines = [
|
||||
$this->describeValue(
|
||||
'Aircraft Registration',
|
||||
$originalRegistration,
|
||||
$data->aircraft_registration,
|
||||
$updates['aircraft_registration'] ?? null,
|
||||
),
|
||||
$this->describeDate('Departure', $originalDeparture, $data->estimated_departure_utc, $updates['departure_date'] ?? null),
|
||||
$this->describeDate('Arrival', $originalArrival, $data->estimated_arrival_utc, $updates['arrival_date'] ?? null),
|
||||
$this->describeAircraftType($originalAircraft, $newAircraft, $data->equipment_iata, $aircraftNotFound),
|
||||
];
|
||||
|
||||
$title = $aircraftNotFound
|
||||
? "Flight {$airlineCode}{$flightNumber} updated — aircraft type not found"
|
||||
: "Flight {$airlineCode}{$flightNumber} updated";
|
||||
|
||||
Notification::create([
|
||||
'user_id' => $flight->user_id,
|
||||
'title' => $title,
|
||||
'body' => implode("\n", $lines),
|
||||
'url' => '/u/' . $flight->user->name . '/flight/' . $flight->id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Describe a simple string field: from → to, or "no change" / "not found" as appropriate.
|
||||
*/
|
||||
protected function describeValue(string $label, ?string $from, ?string $apiValue, ?string $applied): string
|
||||
{
|
||||
$fromLabel = $from ?: 'None';
|
||||
|
||||
if (!$apiValue) {
|
||||
return "{$label}: {$fromLabel} (not returned by API — no change)";
|
||||
}
|
||||
|
||||
if (!$applied) {
|
||||
return "{$label}: {$apiValue} (no change)";
|
||||
}
|
||||
|
||||
return "{$label}: {$fromLabel} → {$apiValue}";
|
||||
}
|
||||
|
||||
protected function describeDate(string $label, ?CarbonInterface $from, ?CarbonInterface $apiValue, mixed $applied): string
|
||||
{
|
||||
$fromLabel = $from?->format('j M Y H:i') . ' ' . ($from?->tzName ?? '') ?: 'Unknown';
|
||||
|
||||
if (!$apiValue) {
|
||||
return "{$label}: {$fromLabel} (not returned by API — no change)";
|
||||
}
|
||||
|
||||
$toLabel = $apiValue->format('j M Y H:i') . ' ' . $apiValue->tzName;
|
||||
|
||||
if (!$applied) {
|
||||
return "{$label}: {$toLabel} (no change)";
|
||||
}
|
||||
|
||||
return "{$label}: {$fromLabel} → {$toLabel}";
|
||||
}
|
||||
|
||||
protected function describeAircraftType(?Aircraft $from, ?Aircraft $to, ?string $equipmentIata, bool $notFound): string
|
||||
{
|
||||
$fromLabel = $from?->display_name_short ?? 'None';
|
||||
|
||||
if (!$equipmentIata) {
|
||||
return "Aircraft Type: {$fromLabel} (not returned by API — no change)";
|
||||
}
|
||||
|
||||
if ($notFound) {
|
||||
return "Aircraft Type: {$fromLabel} (API reported equipment code {$equipmentIata}, but no matching aircraft was found in our database — no change)";
|
||||
}
|
||||
|
||||
if ($to?->id === $from?->id) {
|
||||
return "Aircraft Type: {$fromLabel} (no change)";
|
||||
}
|
||||
|
||||
return "Aircraft Type: {$fromLabel} → {$to?->display_name_short}";
|
||||
}
|
||||
}
|
||||
@@ -2,151 +2,32 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\DTOs\FlightStatData;
|
||||
use App\Models\Aircraft;
|
||||
use App\Models\IataEquipmentCode;
|
||||
use App\Models\Notification;
|
||||
use App\Enums\FlightNotificationType;
|
||||
use App\Models\UserFlight;
|
||||
use App\Services\FlightStatsService;
|
||||
use Carbon\Carbon;
|
||||
use Carbon\CarbonImmutable;
|
||||
use App\Services\FollowerNotificationService;
|
||||
use Illuminate\Console\Attributes\Description;
|
||||
use Illuminate\Console\Attributes\Signature;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
#[Signature('app:update-arrived-flights')]
|
||||
#[Description('Command description')]
|
||||
#[Description('When a flight arrives, update the arrival_processed_at timestamp and notify followers')]
|
||||
class UpdateArrivedFlights extends Command
|
||||
{
|
||||
|
||||
public function __construct(protected FlightStatsService $flightStats)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function notifyDataError(UserFlight $flight): void
|
||||
{
|
||||
Notification::create([
|
||||
'user_id' => $flight->user_id,
|
||||
'title' => "Auto update failed for {$flight->flight_number}",
|
||||
'body' => "There was an error fetching flight data for {$flight->flight_number}. Please manually check the aircraft type, registration and departure/arrival times.",
|
||||
'url' => '/flights/' . $flight->id . '/edit'
|
||||
]);
|
||||
|
||||
$flight->update(['auto_update' => false]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$now = now()->utc();
|
||||
|
||||
$userFlights = UserFlight::where('arrival_date', '<=', $now->copy()->subHour()->toDateTimeString())
|
||||
->where('auto_update', true)
|
||||
->whereNotNull('flight_number')
|
||||
$userFlights = UserFlight::where('arrival_date', '<=', $now->toDateTimeString())
|
||||
->where(function ($query) {
|
||||
$query->whereNull('arrival_processed_at')
|
||||
->orWhereColumn('arrival_processed_at', '<', 'arrival_date');
|
||||
})
|
||||
->get();
|
||||
|
||||
$this->info("Found {$userFlights->count()} flights.");
|
||||
|
||||
echo $userFlights->count() . " flights to update\n";
|
||||
foreach ($userFlights as $flight) {
|
||||
preg_match('/^([A-Z]{2,3})(\d+)$/i', $flight->flight_number, $matches);
|
||||
|
||||
if (empty($matches)) {
|
||||
$this->warn("Could not parse flight number: {$flight->flight_number}");
|
||||
$this->notifyDataError($flight);
|
||||
continue;
|
||||
}
|
||||
|
||||
$airlineCode = strtoupper($matches[1]);
|
||||
$flightNumber = $matches[2];
|
||||
|
||||
$arrivalDate = $flight->arrival_date->setTimezone($flight->arrivalAirport->timezone);
|
||||
|
||||
$data = $this->flightStats->fetchFlightData($airlineCode, $flightNumber, $arrivalDate);
|
||||
|
||||
if (!$data) {
|
||||
$this->warn("No flight data returned for {$airlineCode}{$flightNumber}");
|
||||
$this->notifyDataError($flight);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($data->departure_iata !== $flight->departureAirport->iata_code ||
|
||||
$data->arrival_iata !== $flight->arrivalAirport->iata_code) {
|
||||
$this->warn("Airport mismatch for {$airlineCode}{$flightNumber} — API: {$data->departure_iata}→{$data->arrival_iata}, expected: {$flight->departureAirport->iata_code}→{$flight->arrivalAirport->iata_code}");
|
||||
$this->notifyDataError($flight);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$updates = [];
|
||||
|
||||
if ($data->aircraft_registration && $data->aircraft_registration !== $flight->aircraft_registration) {
|
||||
$updates['aircraft_registration'] = $data->aircraft_registration;
|
||||
}
|
||||
|
||||
if ($data->estimated_departure_utc?->ne($flight->departure_date)) {
|
||||
$updates['departure_date'] = $data->estimated_departure_utc;
|
||||
}
|
||||
|
||||
if ($data->estimated_arrival_utc?->ne($flight->arrival_date)) {
|
||||
$updates['arrival_date'] = $data->estimated_arrival_utc;
|
||||
}
|
||||
|
||||
if ($data->equipment_iata) {
|
||||
$currentAircraft = $flight->aircraft;
|
||||
|
||||
if ($currentAircraft?->iata_code !== $data->equipment_iata) {
|
||||
$match = $this->flightStats->guessAircraftFromIata($data->equipment_iata);
|
||||
|
||||
if ($match) {
|
||||
$updates['aircraft_id'] = $match->id;
|
||||
} else {
|
||||
Log::info("No aircraft match for IATA code {$data->equipment_iata} on flight {$airlineCode}{$flightNumber}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($updates)) {
|
||||
$flight->update($updates);
|
||||
$this->info("Updated flight {$airlineCode}{$flightNumber}: " . implode(', ', array_keys($updates)));
|
||||
|
||||
$changeDescriptions = [];
|
||||
if (isset($updates['aircraft_registration'])) {
|
||||
$changeDescriptions[] = "Registration updated to {$updates['aircraft_registration']}";
|
||||
}
|
||||
if (isset($updates['departure_date'])) {
|
||||
$changeDescriptions[] = "Departure updated to {$updates['departure_date']}";
|
||||
}
|
||||
if (isset($updates['arrival_date'])) {
|
||||
$changeDescriptions[] = "Arrival updated to {$updates['arrival_date']}";
|
||||
}
|
||||
if (isset($updates['aircraft_id'])) {
|
||||
$aircraft = Aircraft::find($updates['aircraft_id']);
|
||||
$changeDescriptions[] = "Aircraft type updated to {$aircraft->display_name_short}";
|
||||
}
|
||||
|
||||
Notification::create([
|
||||
'user_id' => $flight->user_id,
|
||||
'title' => "Flight {$airlineCode}{$flightNumber} updated",
|
||||
'body' => implode("\n", $changeDescriptions),
|
||||
'url' => '/u/'. $flight->user->name . '/flight/'. $flight->id,
|
||||
]);
|
||||
} else {
|
||||
$this->info("No changes for {$airlineCode}{$flightNumber}");
|
||||
|
||||
Notification::create([
|
||||
'user_id' => $flight->user_id,
|
||||
'title' => "Flight {$airlineCode}{$flightNumber} updated — no changes",
|
||||
'body' => "Your flight was completed and no updates were made to aircraft, registration, or departure/arrival times.",
|
||||
'url' => '/u/'. $flight->user->name . '/flight/'. $flight->id,
|
||||
]);
|
||||
}
|
||||
|
||||
$flight->update(['auto_update' => false]);
|
||||
$flight->update(['arrival_processed_at' => $now]);
|
||||
new FollowerNotificationService()->notifyFlightEvent($flight, FlightNotificationType::Arrived);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,22 +2,18 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Aircraft;
|
||||
use App\Models\Notification;
|
||||
use App\Models\User;
|
||||
use App\Enums\FlightNotificationType;
|
||||
use App\Models\UserFlight;
|
||||
use App\Services\FollowerNotificationService;
|
||||
use Illuminate\Console\Attributes\Description;
|
||||
use Illuminate\Console\Attributes\Signature;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
#[Signature('app:update-departed-flights')]
|
||||
#[Description('Command description')]
|
||||
class UpdateDepartedFlights extends Command
|
||||
{
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$now = now()->utc();
|
||||
@@ -32,33 +28,7 @@ class UpdateDepartedFlights extends Command
|
||||
|
||||
foreach ($userFlights as $flight) {
|
||||
$flight->update(['departure_processed_at' => $now]);
|
||||
$this->notifyFollowersOnDeparture($flight);
|
||||
new FollowerNotificationService()->notifyFlightEvent($flight, FlightNotificationType::Departed);
|
||||
}
|
||||
}
|
||||
|
||||
protected function notifyFollowersOnDeparture(UserFlight $flight): void
|
||||
{
|
||||
$user = $flight->user;
|
||||
$title = "{$user->name} is taking off!";
|
||||
$flightNumber = $flight->flight_number ? "On {$flight->flight_number} from" : 'From';
|
||||
$body = "{$flightNumber} {$flight->departureAirport->municipality} → {$flight->arrivalAirport->municipality}";
|
||||
$url = route('profile.flight', [
|
||||
'user' => $user->id,
|
||||
'userFlight' => $flight->id,
|
||||
]);
|
||||
|
||||
$user->followers()->get()->each(function (User $follower) use ($title, $body, $url) {
|
||||
$notify = $follower->getSetting('notify_on_flight_departure');
|
||||
|
||||
if ($notify) {
|
||||
Notification::create([
|
||||
'user_id' => $follower->id,
|
||||
'title' => $title,
|
||||
'body' => $body,
|
||||
'url' => $url,
|
||||
]);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\UserFlight;
|
||||
|
||||
enum FlightNotificationType: string
|
||||
{
|
||||
case Booked = 'booked';
|
||||
case Logged = 'logged';
|
||||
case Cancelled = 'cancelled';
|
||||
case Departed = 'departed';
|
||||
case Arrived = 'arrived';
|
||||
|
||||
public function settingKey(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Booked => 'notify_on_upcoming_flight_added',
|
||||
self::Logged => 'notify_on_historic_flight_added',
|
||||
self::Cancelled => 'notify_on_flight_cancellation',
|
||||
self::Departed => 'notify_on_flight_departure',
|
||||
self::Arrived => 'notify_on_flight_arrival',
|
||||
};
|
||||
}
|
||||
|
||||
public function title(User $user): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Booked => "{$user->name} booked a new flight",
|
||||
self::Logged => "{$user->name} logged a new flight",
|
||||
self::Cancelled => "{$user->name} cancelled a flight",
|
||||
self::Departed => "{$user->name} is taking off!",
|
||||
self::Arrived => "{$user->name} has landed!",
|
||||
};
|
||||
}
|
||||
|
||||
public function body(UserFlight $flight): string
|
||||
{
|
||||
$flightNumber = $flight->flight_number ? "{$flight->flight_number} from" : 'From';
|
||||
|
||||
return match ($this) {
|
||||
self::Departed => "{$flightNumber} {$flight->departureAirport->municipality} → {$flight->arrivalAirport->municipality}",
|
||||
self::Arrived => "In {$flight->arrivalAirport->municipality}",
|
||||
default => "{$flightNumber} {$flight->departureAirport->municipality} ({$flight->departureAirport->display_code}) → "
|
||||
. "{$flight->arrivalAirport->municipality} ({$flight->arrivalAirport->display_code}) on {$flight->departure_date_display}",
|
||||
};
|
||||
}
|
||||
|
||||
public function url(UserFlight $flight): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Cancelled => route('profile.view', [$flight->user->name]),
|
||||
self::Departed, self::Arrived => route('profile.flight', ['user' => $flight->user->id, 'userFlight' => $flight->id]),
|
||||
default => route('profile.departure-board', [$flight->user->name, $flight->id]),
|
||||
};
|
||||
}
|
||||
|
||||
public static function forCreation(UserFlight $flight): self
|
||||
{
|
||||
return $flight->departure_date->isFuture() ? self::Booked : self::Logged;
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,10 @@
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\ApiController;
|
||||
use App\Http\Controllers\UserFlightController;
|
||||
use App\Models\User;
|
||||
use App\Models\UserFlight;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
class UserApiController extends ApiController
|
||||
|
||||
@@ -232,8 +232,6 @@ class FlightController extends Controller
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate($this->rules());
|
||||
@@ -248,42 +246,10 @@ class FlightController extends Controller
|
||||
],
|
||||
]);
|
||||
|
||||
$this->notifyFollowersOnCreation($newFlight);
|
||||
|
||||
return redirect()->route('profile.departure-board', [Auth::user()->name, $newFlight->id]);
|
||||
}
|
||||
|
||||
protected function notifyFollowersOnCreation(UserFlight $flight): void
|
||||
{
|
||||
$user = $flight->user;
|
||||
$isFuture = $flight->departure_date->isFuture();
|
||||
|
||||
$title = $isFuture
|
||||
? "{$user->name} booked a new flight"
|
||||
: "{$user->name} logged a new flight";
|
||||
|
||||
|
||||
$flightNumber = $flight->flight_number ? "{$flight->flight_number} from" : 'From';
|
||||
$body = "{$flightNumber} {$flight->departureAirport->municipality} ({$flight->departureAirport->display_code}) → {$flight->arrivalAirport->municipality} ({$flight->arrivalAirport->display_code}) on {$flight->departure_date_display}";
|
||||
$url = route('profile.departure-board', [$user->name, $flight->id]);
|
||||
|
||||
$user->followers()->get()->each(function (User $follower) use ($title, $body, $url, $isFuture) {
|
||||
$notifyForFuture = $follower->getSetting('notify_on_upcoming_flight_added');
|
||||
$notifyForHistoric = $follower->getSetting('notify_on_historic_flight_added');
|
||||
|
||||
if (($isFuture && $notifyForFuture) || (!$isFuture && $notifyForHistoric)) {
|
||||
Notification::create([
|
||||
'user_id' => $follower->id,
|
||||
'title' => $title,
|
||||
'body' => $body,
|
||||
'url' => $url,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function update(Request $request, UserFlight $flight)
|
||||
{
|
||||
@@ -323,36 +289,11 @@ class FlightController extends Controller
|
||||
],
|
||||
]);
|
||||
|
||||
if ($isFuture) {
|
||||
$this->notifyFollowersOnCancellation($flight);
|
||||
}
|
||||
|
||||
$flight->delete();
|
||||
|
||||
return redirect()->route('profile.'.$referrer, [Auth::user()->name]);
|
||||
}
|
||||
|
||||
protected function notifyFollowersOnCancellation(UserFlight $flight): void
|
||||
{
|
||||
$user = $flight->user;
|
||||
|
||||
$title = "{$user->name} cancelled a flight";
|
||||
$flightNumber = $flight->flight_number ? "{$flight->flight_number} from" : 'From';
|
||||
$body = "{$flightNumber} {$flight->departureAirport->municipality} ({$flight->departureAirport->display_code}) → {$flight->arrivalAirport->municipality} ({$flight->arrivalAirport->display_code}) on {$flight->departure_date_display}";
|
||||
$url = route('profile.view', [$user->name]);
|
||||
|
||||
$user->followers()->get()->each(function (User $follower) use ($title, $body, $url) {
|
||||
if ($follower->getSetting('notify_on_flight_cancellation')) {
|
||||
Notification::create([
|
||||
'user_id' => $follower->id,
|
||||
'title' => $title,
|
||||
'body' => $body,
|
||||
'url' => $url,
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function staticData() : array {
|
||||
return [
|
||||
'seat_types' => SeatType::orderBy('id')->get()->toArray(),
|
||||
|
||||
@@ -271,44 +271,11 @@ class FlightImportController extends Controller
|
||||
],
|
||||
]);
|
||||
|
||||
$this->notifyFollowersOnCreation($newFlight);
|
||||
|
||||
ImportedFlight::destroy($validated['imported_flight_id']);
|
||||
return to_route('reconcile');
|
||||
}
|
||||
|
||||
protected function notifyFollowersOnCreation(UserFlight $flight): void
|
||||
{
|
||||
$user = $flight->user;
|
||||
$isFuture = $flight->departure_date->isFuture();
|
||||
|
||||
$title = $isFuture
|
||||
? "{$user->name} booked a new flight"
|
||||
: "{$user->name} logged a new flight";
|
||||
|
||||
|
||||
$flightNumber = $flight->flight_number ? "{$flight->flight_number} from" : 'From';
|
||||
$body = "{$flightNumber} {$flight->departureAirport->municipality} ({$flight->departureAirport->display_code}) → {$flight->arrivalAirport->municipality} ({$flight->arrivalAirport->display_code}) on {$flight->departure_date_display}";
|
||||
$url = route('profile.departure-board', [$user->name, $flight->id]);
|
||||
|
||||
$user->followers()->get()->each(function (User $follower) use ($title, $body, $url, $isFuture) {
|
||||
$notifyForFuture = $follower->getSetting('notify_on_upcoming_flight_added');
|
||||
$notifyForHistoric = $follower->getSetting('notify_on_historic_flight_added');
|
||||
|
||||
if (($isFuture && $notifyForFuture) || (!$isFuture && $notifyForHistoric)) {
|
||||
Notification::create([
|
||||
'user_id' => $follower->id,
|
||||
'title' => $title,
|
||||
'body' => $body,
|
||||
'url' => $url,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private function validateCsvFormat(string $path): ?string
|
||||
{
|
||||
$handle = fopen($path, 'r');
|
||||
|
||||
@@ -67,7 +67,7 @@ class FollowerController extends Controller
|
||||
? auth()->user()->name . ' is now following you.'
|
||||
: auth()->user()->name . ' wants to follow you.',
|
||||
'is_achievement' => false,
|
||||
'url' => $canView ? '/u/' . auth()->user()->name : '/follow-requests',
|
||||
'url' => $canView ? '/u/' . auth()->user()->name : '/settings/followers',
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
class Guest extends Model
|
||||
{
|
||||
use HasApiTokens;
|
||||
}
|
||||
@@ -31,7 +31,8 @@ class UserFlight extends Model
|
||||
'crew_type_id',
|
||||
'note',
|
||||
'auto_update',
|
||||
'departure_processed_at'
|
||||
'departure_processed_at',
|
||||
'arrival_processed_at'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
@@ -39,6 +40,7 @@ class UserFlight extends Model
|
||||
'arrival_date' => 'immutable_datetime',
|
||||
'auto_update' => 'boolean',
|
||||
'departure_processed_at' => 'datetime',
|
||||
'arrival_processed_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $appends = [
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Enums\FlightNotificationType;
|
||||
use App\Models\User;
|
||||
use App\Models\UserFlight;
|
||||
use App\Services\FollowerNotificationService;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class FlightObserver
|
||||
{
|
||||
|
||||
protected function clearCache(UserFlight $flight): void
|
||||
{
|
||||
Cache::forget("user_flights_{$flight->user->id}");
|
||||
@@ -27,6 +30,7 @@ class FlightObserver
|
||||
{
|
||||
$flight->user->calculateAchievements();
|
||||
$this->clearCache($flight);
|
||||
new FollowerNotificationService()->notifyFlightEvent($flight, FlightNotificationType::forCreation($flight));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,5 +53,9 @@ class FlightObserver
|
||||
{
|
||||
$flight->user->calculateAchievements();
|
||||
$this->clearCache($flight);
|
||||
|
||||
if ($flight->departure_date->isFuture()) {
|
||||
new FollowerNotificationService()->notifyFlightEvent($flight, FlightNotificationType::Cancelled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Enums\FlightNotificationType;
|
||||
use App\Models\Notification;
|
||||
use App\Models\User;
|
||||
use App\Models\UserFlight;
|
||||
|
||||
class FollowerNotificationService
|
||||
{
|
||||
/**
|
||||
* Notify a user's followers about something that happened to one of their flights.
|
||||
*/
|
||||
public function notifyFlightEvent(UserFlight $flight, FlightNotificationType $type): void
|
||||
{
|
||||
$this->notify(
|
||||
user: $flight->user,
|
||||
settingKey: $type->settingKey(),
|
||||
title: $type->title($flight->user),
|
||||
body: $type->body($flight),
|
||||
url: $type->url($flight),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic entry point for any future "notify all followers about X" need
|
||||
* that isn't tied to a flight event.
|
||||
*/
|
||||
public function notify(User $user, string $settingKey, string $title, string $body, string $url): void
|
||||
{
|
||||
$user->followers()
|
||||
->get()
|
||||
->filter(fn (User $follower) => $follower->getSetting($settingKey))
|
||||
->each(fn (User $follower) => Notification::create([
|
||||
'user_id' => $follower->id,
|
||||
'title' => $title,
|
||||
'body' => $body,
|
||||
'url' => $url,
|
||||
]));
|
||||
}
|
||||
}
|
||||
@@ -145,6 +145,13 @@ class SettingsRegistry
|
||||
'label' => 'Notify Me When Someone I Follow Logs an Upcoming Flight',
|
||||
'default' => true,
|
||||
],
|
||||
[
|
||||
'category' => 'Notifications',
|
||||
'key' => 'notify_on_flight_cancellation',
|
||||
'type' => 'checkbox',
|
||||
'label' => 'Notify Me When Someone I Follow Cancels an Upcoming Flight',
|
||||
'default' => false,
|
||||
],
|
||||
[
|
||||
'category' => 'Notifications',
|
||||
'key' => 'notify_on_flight_departure',
|
||||
@@ -152,19 +159,12 @@ class SettingsRegistry
|
||||
'label' => 'Notify Me When Someone I Follow Departs',
|
||||
'default' => true,
|
||||
],
|
||||
/* [
|
||||
[
|
||||
'category' => 'Notifications',
|
||||
'key' => 'notify_on_flight_arrival',
|
||||
'type' => 'checkbox',
|
||||
'label' => 'Notify Me When Someone I Follow Arrives',
|
||||
'default' => true,
|
||||
],*/
|
||||
[
|
||||
'category' => 'Notifications',
|
||||
'key' => 'notify_on_flight_cancellation',
|
||||
'type' => 'checkbox',
|
||||
'label' => 'Notify Me When Someone I Follow Cancels an Upcoming Flight',
|
||||
'default' => false,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user