postgres fixes

This commit is contained in:
Nabeel Shahzad 2026-06-01 18:38:58 -05:00
parent 625082fdae
commit 67c244240a
No known key found for this signature in database
GPG Key ID: 08C44114D2BF3047
50 changed files with 508 additions and 378 deletions

View File

@ -27,3 +27,6 @@ indent_size = 4
[Makefile]
indent_style = tab
[justfile]
indent_size = 2

View File

@ -1,36 +0,0 @@
#---
#
# Run Laravel Pint
# https://laravel.com/docs/11.x/pint#running-tests-on-github-actions
#
# name: Check Code Style
#
# on:
# pull_request:
# push:
# branches:
# - main
# - dev
# - "feature/**"
# - "release/**"
# - "hotfix/**"
#
# jobs:
# lint:
# runs-on: ubuntu-latest
# strategy:
# fail-fast: true
# matrix:
# php: [8.4]
#
# steps:
# - name: Checkout code
# uses: actions/checkout@v4
#
# - name: Setup PHP
# uses: shivammathur/setup-php@v2
# with:
# php-version: ${{ matrix.php }}
# extensions: json, dom, curl, libxml, mbstring
# coverage: none

2
.gitignore vendored
View File

@ -30,6 +30,7 @@ storage/*.sqlite
.envrc
.env
.github/scripts/env.test
.mise.local.toml
.vagrant
.docker
#Homestead.yaml
@ -108,3 +109,4 @@ modules_statuses.json
/phpvms
.env.testing
/intellij_style.xml
/justfile

100
.mise.toml Normal file
View File

@ -0,0 +1,100 @@
[env]
BUILDKIT_HOST = "docker-container://buildkit"
PHPRC = "resources/mise"
_.php = {
prebuilt_static = true,
prebuilt_static_flavor = "bulk",
extra_configure_options = "--with-pdo-mysql --with-pdo-pgsql",
pie_extensions = "redis/phpredis xdebug/xdebug"
}
[tools]
php = "8.5.6"
#bun = "system"
[plugins]
php = "https://github.com/verzly/mise-php#latest"
# -----------------------------------------------
# Local tasks
# mise run setup
# -----------------------------------------------
[tasks.setup]
run = [
"mise install",
"mise run run-buildkit-container || true"
]
[tasks.run-buildkit-container]
run = "docker run --rm --privileged -d --name buildkit -e BUILDKIT_DEBUG=1 moby/buildkit:latest"
[tasks.railpack]
run = "railpack build ."
# Run the github workflows locally using act
[tasks.gh-actions]
env = {
DOCKER_HOST = 'ssh://twilight.zsh.ad'
}
run = '''
act \
--container-architecture linux/amd64 pull_request \
--action-offline-mode
'''
# -----------------------------------------------
# Test against real databases
# -----------------------------------------------
[tasks.test-db-up]
description = "Start MySQL and PostgreSQL for testing"
run = "docker compose -f compose.test.yml up -d --wait"
[tasks.test-db-down]
description = "Stop test databases and remove volumes"
run = "docker compose -f compose.test.yml down -v"
[tasks.'tests:mysql']
description = "Run tests against MySQL (port 13306)"
#depends = ["test-db-up"]
#depends_post = ["test-db-down"]
env = {
DB_CONNECTION = 'mysql',
DB_HOST = '127.0.0.1',
DB_PORT = "13306",
DB_DATABASE = "phpvms_test",
DB_USERNAME = "phpvms",
DB_PASSWORD = "password"
}
run = [
"docker compose -f compose.test.yml up mysql -d --wait",
"vendor/bin/pest --compact"
]
[tasks.'tests:pgsql']
description = "Run tests against PostgreSQL (port 15432)"
#depends = ["test-db-up"]
#depends_post = ["test-db-down"]
env = {
DB_CONNECTION = 'pgsql',
DB_HOST = '127.0.0.1',
DB_PORT = "15432",
DB_DATABASE = "phpvms_test",
DB_USERNAME = "phpvms",
DB_PASSWORD = "password"
}
run = [
"docker compose -f compose.test.yml up pgsql -d --wait",
"vendor/bin/pest --compact"
]
[tasks.test-db]
description = "Run tests against MySQL and PostgreSQL"
depends = ["test-mysql", "test-pgsql"]
run = '''
echo ""
echo "All database tests passed."
'''

View File

@ -5,6 +5,7 @@
"modules/**",
"resources/js/dist/**",
".github/skills/*",
".github/copilot-instructions.md"
".github/copilot-instructions.md",
"*.toml"
]
}

View File

@ -1,16 +1,12 @@
# phpVMS <sup>8</sup>
# phpVMS 8 - Build, Fly, Extend
[![Build](https://github.com/phpvms/phpvms/actions/workflows/build.yml/badge.svg)](https://github.com/phpvms/phpvms/actions/workflows/build.yml) ![StyleCI](https://github.styleci.io/repos/93688482/shield?branch=dev) [![License](https://poser.pugx.org/nabeel/phpvms/license)](https://packagist.org/packages/nabeel/phpvms)
![Build](https://github.com/phpvms/phpvms/actions/workflows/build.yml/badge.svg)![StyleCI](https://github.styleci.io/repos/93688482/shield?branch=dev)![License](https://poser.pugx.org/nabeel/phpvms/license)
phpVMS is a PHP application to run and simulate an airline. It allowed users to register,
view flight schedules that you create, and file flight reports, built on the Laravel framework.
The latest documentation, with installation instructions is available on the
[phpVMS documentation](https://docs.phpvms.net/) site.
phpVMS is a PHP application to run and simulate an airline. It allowed users to register, view flight schedules that you create, and file flight reports, built on the Laravel framework. The latest documentation, with installation instructions is available on the [phpVMS documentation](https://docs.phpvms.net/) site.
## Installation
A full distribution, with all the composer dependencies, is available at this
[GitHub Releases](https://github.com/nabeelio/phpvms/releases) link.
A full distribution, with all the composer dependencies, is available at this [GitHub Releases](https://github.com/nabeelio/phpvms/releases) link.
### Requirements
@ -24,41 +20,28 @@ A full distribution, with all the composer dependencies, is available at this
- tokenizer
- intl
- zip
- Database:
- MySQL 8.0+ (or MySQL variant, including MariaDB 10.2+ and Percona 8.0+)
- PostgreSQL 16+
- MySQL 8.0+
- MariaDB 10.6+
- Redis 6.0+ (optional, recommended)
[View more details on requirements](https://docs.phpvms.net/requirements)
### Installer
1. Upload to your server
2. Visit the site, and follow the link to the installer
[View installation details](https://docs.phpvms.net/installation)
## Production Deployment with Docker
The reference production stack is `compose.deploy.yml`. It runs the official
phpVMS image (built from the repo `Dockerfile`, based on
`serversideup/php:8.5-frankenphp`) with Laravel Octane worker mode enabled,
plus MariaDB and Redis. No separate web-server sidecar is needed — FrankenPHP
serves HTTP/HTTPS directly.
The reference production stack is `compose.deploy.yml`. It runs the official phpvms image (built from the repo `Dockerfile`, based on `serversideup/php:8.5-frankenphp`) with Laravel Octane worker mode enabled, plus PostgreSQL and Redis. No separate web-server sidecar is needed — FrankenPHP serves HTTP/HTTPS directly.
```bash
```shellscript
docker compose -f compose.deploy.yml up -d
```
### Octane worker mode
The image itself ships with serversideup's default classic FrankenPHP entry
point (one PHP worker per request). `compose.deploy.yml` opts into Laravel
Octane worker mode via a `command:` override on the `app` service — the
[pattern documented upstream](https://serversideup.net/open-source/docker-php/docs/framework-guides/laravel/octane).
The image itself ships with serversideup's default classic FrankenPHP entry point (one PHP worker per request). `compose.deploy.yml` opts into Laravel Octane worker mode via a `command:` override on the `app` service — the [pattern documented upstream](https://serversideup.net/open-source/docker-php/docs/framework-guides/laravel/octane).
To fall back to classic FrankenPHP + PHP-worker mode (matches the prior
PHP-FPM request semantics — slower per request but bulletproof if a
worker-mode bug hits), delete the `command:` line from the `app` service
and recreate the container:
To fall back to classic FrankenPHP + PHP-worker mode (matches the prior PHP-FPM request semantics — slower per request but bulletproof if a worker-mode bug hits), delete the `command:` line from the `app` service and recreate the container:
```yaml
services:
@ -66,24 +49,21 @@ services:
# command: [...octane:start...] # remove this line
```
Under worker mode, `KvpService` (the JSON-backed key/value store used for
non-hot settings) is eventually consistent: writes from one worker
propagate to other workers on their next read of the same key. If your
deployment requires strong consistency for KVP, run with `--workers=1` or
remove the `command:` override.
Module authors: under Octane the framework stays booted across requests, so avoid per-request data in singleton-bound services, `static` array accumulators, and `boot()`-time toggles without a matching per-request reset. See `app/Http/Middleware/DisableActivityLoggingByDefault.php` for the pattern used to keep activity logging request-scoped.
Module authors: under Octane the framework stays booted across requests,
so avoid per-request data in singleton-bound services, `static` array
accumulators, and `boot()`-time toggles without a matching per-request
reset. See `app/Http/Middleware/DisableActivityLoggingByDefault.php` for
the pattern used to keep activity logging request-scoped.
## Development Environment
## Development Environment with Docker
The development environment uses mise. You can read about mise and install it from [here](https://mise.jdx.dev/). If you want mise to handle installing the correct PHP version, you can run:
A full development environment can be brought up using Docker and
[Laravel Sail](https://laravel.com/docs/10.x/sail), without having to install composer/npm locally
```shellscript
mise install
```
```bash
If you install the `mise` shell script integration, the correct PHP version will automatically be selected for you.~~~~
A full development environment can be brought up using Docker and [Laravel Sail](https://laravel.com/docs/10.x/sail), without having to install composer/npm locally
```shellscript
make docker-test
# **OR** with docker directly
@ -101,16 +81,15 @@ docker run --rm \
Then go to `http://localhost`.
Instead of repeatedly typing vendor/bin/sail to execute Sail commands, you may wish to configure a
shell alias that allows you to execute Sail's commands more easily:
Instead of repeatedly typing vendor/bin/sail to execute Sail commands, you may wish to configure a shell alias that allows you to execute Sail's commands more easily:
```bash
```shellscript
alias sail='[ -f sail ] && sh sail || sh vendor/bin/sail'
```
Then you can execute php, artisan, composer, npm, etc. commands using the sail prefix:
```bash
```shellscript
# PHP commands within Laravel Sail...
sail php --version
@ -130,7 +109,7 @@ To interact with databases (MariaDB, Redis...), please refer to the Laravel Sail
Yarn is required, run:
```bash
```shellscript
make build-assets
```
@ -138,10 +117,9 @@ This will build all the assets according to the webpack file.
### Laravel Boost
If you want to use AI agents for your development workflow, please ensure you install
[Laravel Boost](https://laravel.com/ai/boost) by running the following command:
If you want to use AI agents for your development workflow, please ensure you install [Laravel Boost](https://laravel.com/ai/boost) by running the following command:
```bash
```shellscript
php artisan boost:install
```
@ -150,7 +128,3 @@ php artisan boost:install
## Contributors
Thank you to everyone who've contributed to phpVMS!
<a href="https://github.com/phpvms/phpvms/graphs/contributors">
<img src="https://contrib.rocks/image?repo=phpvms/phpvms" alt="contributors images"/>
</a>

View File

@ -27,17 +27,14 @@ class CarbonCast implements CastsAttributes
return new Carbon($value);
}
/**
* Transform the attribute to its underlying model values.
*
* @param Model $model
* @param mixed $value
* @return mixed
*/
public function set($model, string $key, $value, array $attributes)
{
if ($value instanceof Carbon) {
return $value->toIso8601ZuluString();
return $value->toDateTimeString();
}
if (is_string($value)) {
return (new Carbon($value))->toDateTimeString();
}
return $value;

View File

@ -173,6 +173,12 @@ class SetVisibleFlights extends Listener
*/
private static function sqlBool(bool $value): string
{
$driver = DB::connection()->getDriverName();
if ($driver === 'pgsql') {
return $value ? 'TRUE' : 'FALSE';
}
return $value ? '1' : '0';
}
}

View File

@ -10,5 +10,5 @@ use App\Models\Pirep;
class AcarsUpdate extends Event
{
public function __construct(public Pirep $pirep, public Acars $acars) {}
public function __construct(public Pirep $pirep, public ?Acars $acars = null) {}
}

View File

@ -42,7 +42,7 @@ class FlightImporter extends Importer
->rules(['required', 'integer']),
ImportColumn::make('callsign')
->rules(['max:4']),
->rules(['max:10']),
ImportColumn::make('route_code')
->rules(['max:5']),
@ -125,11 +125,11 @@ class FlightImporter extends Importer
ImportColumn::make('load_factor')
->numeric()
->rules(['nullable', 'integer']),
->rules(['nullable', 'numeric', 'min:0', 'max:100']),
ImportColumn::make('load_factor_variance')
->numeric()
->rules(['nullable', 'integer']),
->rules(['nullable', 'numeric', 'min:0', 'max:100']),
ImportColumn::make('route')
->fillRecordUsing(function (Flight $record, ?string $state): void {

View File

@ -51,7 +51,7 @@ class FlightForm
TextInput::make('callsign')
->label(__('flights.callsign'))
->string()
->maxLength(4),
->maxLength(10),
TextInput::make('flight_number')
->label(__('flights.flightnumber'))
@ -82,10 +82,16 @@ class FlightForm
Grid::make()->schema([
TextInput::make('load_factor')
->numeric()
->minValue(0)
->maxValue(100)
->stripCharacters('%')
->helperText(__('filament.flight_load_factor_hint')),
TextInput::make('load_factor_variance')
->numeric()
->minValue(0)
->maxValue(100)
->stripCharacters('%')
->helperText(__('filament.flight_load_factor_variance_hint')),
])

View File

@ -18,6 +18,7 @@ class NewsForm
->label(__('filament.news_subject'))
->string()
->required()
->maxLength(200)
->columnSpanFull(),
RichEditor::make('body')

View File

@ -34,6 +34,7 @@ use App\Services\RouteForge\RouteForgeService;
use Illuminate\Http\JsonResponse;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\DB;
/**
* Backend HTTP entry points for the RouteForge admin tool.
@ -125,10 +126,8 @@ final class RouteForgeController extends Controller
$query = FlightBundle::query()->orderBy('name');
if (is_string($search) && $search !== '') {
// SQLite + MySQL both fold LIKE case-insensitively for ASCII; the
// picker payload is name-keyed so non-ASCII picker UX is a non-issue
// at the VA scale this is sized for.
$query->where('name', 'like', '%'.$search.'%');
$like = DB::connection()->getDriverName() === 'pgsql' ? 'ilike' : 'like';
$query->where('name', $like, '%'.$search.'%');
}
$paginated = $query->paginate($perPage);

View File

@ -20,6 +20,7 @@ use DateTime;
use Illuminate\Database\QueryException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
@ -168,15 +169,16 @@ class AcarsController extends Controller
}
try {
if (!empty($position['id'])) {
Acars::updateOrInsert(
['id' => $position['id']],
$position
);
} else {
$update = Acars::create($position);
$update->save();
}
DB::transaction(function () use ($position): void {
if (!empty($position['id'])) {
Acars::updateOrInsert(
['id' => $position['id']],
$position
);
} else {
Acars::create($position);
}
});
$count++;
} catch (QueryException $ex) {
@ -229,15 +231,16 @@ class AcarsController extends Controller
}
try {
if (isset($log['id'])) {
Acars::updateOrInsert(
['id' => $log['id']],
$log
);
} else {
$acars = Acars::create($log);
$acars->save();
}
DB::transaction(function () use ($log): void {
if (isset($log['id'])) {
Acars::updateOrInsert(
['id' => $log['id']],
$log
);
} else {
Acars::create($log);
}
});
$count++;
} catch (QueryException $ex) {
@ -281,15 +284,16 @@ class AcarsController extends Controller
}
try {
if (isset($log['id'])) {
Acars::updateOrInsert(
['id' => $log['id']],
$log
);
} else {
$acars = Acars::create($log);
$acars->save();
}
DB::transaction(function () use ($log): void {
if (isset($log['id'])) {
Acars::updateOrInsert(
['id' => $log['id']],
$log
);
} else {
Acars::create($log);
}
});
$count++;
} catch (QueryException $ex) {

View File

@ -31,7 +31,7 @@ class SearchAirportsRequest extends FormRequest
{
public const array SEARCHABLE_FIELDS = ['iata', 'icao', 'name'];
public const array SEARCHABLE_OPERATORS = ['=', 'like'];
public const array SEARCHABLE_OPERATORS = ['=', 'like', 'ilike'];
public const array SEARCH_MODES = ['substring', 'prefix'];

View File

@ -294,6 +294,7 @@ class Airport extends Model
return [
'lat' => 'float',
'lon' => 'float',
'elevation' => 'integer',
'hub' => 'boolean',
'ground_handling_cost' => 'float',
'fuel_100ll_cost' => 'float',

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Models;
use App\Contracts\Model;
use Illuminate\Database\Eloquent\Attributes\WithoutIncrementing;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Support\Carbon;
@ -35,6 +36,7 @@ use Illuminate\Support\Carbon;
*
* @mixin \Eloquent
*/
#[WithoutIncrementing]
class Event extends Model
{
use HasFactory;

View File

@ -222,20 +222,21 @@ class Flight extends Model
protected function casts(): array
{
return [
'flight_number' => 'integer',
'days' => 'integer',
'level' => 'integer',
'distance' => DistanceCast::class,
'flight_time' => 'integer',
'flight_type' => FlightType::class,
'departure_time' => 'datetime:H:i:s',
'arrival_time' => 'datetime:H:i:s',
'start_date' => 'datetime',
'end_date' => 'datetime',
'load_factor' => 'double',
'load_factor_variance' => 'double',
'pilot_pay' => 'float',
'has_bid' => 'boolean',
'flight_number' => 'integer',
'days' => 'integer',
'level' => 'integer',
'distance' => DistanceCast::class,
'flight_time' => 'integer',
'flight_type' => FlightType::class,
'departure_time' => 'datetime:H:i:s',
'arrival_time' => 'datetime:H:i:s',
'start_date' => 'datetime',
'end_date' => 'datetime',
// `load_factor` and `load_factor_variance` double casts are handled by
// their Attribute mutators so blank string inputs canonicalize to NULL
// rather than causing MySQL strict-mode errors on DECIMAL columns.
'pilot_pay' => 'float',
'has_bid' => 'boolean',
// `route_leg` int cast is handled by the routeLeg() Attribute
// mutator so empty / '0' inputs canonicalize to NULL rather than 0.
'enabled' => 'boolean',
@ -258,7 +259,7 @@ class Flight extends Model
/** @noinspection DynamicInvocationViaScopeResolutionInspection */
$flights = self::where('enabled', true);
foreach ($days as $day) {
$flights = $flights->where('days', '&', $day);
$flights = $flights->whereRaw('("days" & ?) > 0', [$day]);
}
return $flights;
@ -379,6 +380,32 @@ class Flight extends Model
);
}
/**
* Canonicalize `load_factor` convert blank strings to null
* so MySQL strict mode doesn't reject them for the DECIMAL column.
* Also handles float casting now that the `double` cast is removed.
*/
protected function loadFactor(): Attribute
{
return Attribute::make(
get: static fn (mixed $value): ?float => $value === null ? null : (float) $value,
set: static fn (mixed $value): ?float => blank($value) ? null : (float) $value,
);
}
/**
* Canonicalize `load_factor_variance` convert blank strings to null
* so MySQL strict mode doesn't reject them for the DECIMAL column.
* Also handles float casting now that the `double` cast is removed.
*/
protected function loadFactorVariance(): Attribute
{
return Attribute::make(
get: static fn (mixed $value): ?float => $value === null ? null : (float) $value,
set: static fn (mixed $value): ?float => blank($value) ? null : (float) $value,
);
}
/**
* Collapse null / '' / 0 / '0' to canonical NULL for route-key fields.
*

View File

@ -10,7 +10,7 @@ use Illuminate\Support\Carbon;
/**
* @property int $id
* @property int $user_field_id
* @property string $user_id
* @property int $user_id
* @property string|null $value
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
@ -42,6 +42,14 @@ class UserFieldValue extends Model
public static array $rules = [];
#[\Override]
protected function casts(): array
{
return [
'user_id' => 'integer',
];
}
/**
* Return related field's name along with field values
*/

View File

@ -5,6 +5,7 @@ namespace App\Queries;
use App\Http\Requests\SearchAirportsRequest;
use App\Models\Airport;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
/**
* Build an Eloquent\Builder for airport listing/search endpoints.
@ -31,6 +32,11 @@ class AirportSearchQueryV1
{
public function __construct(private readonly SearchAirportsRequest $request) {}
private function likeOperator(): string
{
return DB::connection()->getDriverName() === 'pgsql' ? 'ilike' : 'like';
}
public function build(): Builder
{
$data = $this->request->validated();
@ -114,7 +120,7 @@ class AirportSearchQueryV1
*/
private function resolveSearchFields(?string $searchFields, array $searchDataKeys): array
{
$defaultFields = array_fill_keys(SearchAirportsRequest::SEARCHABLE_FIELDS, 'like');
$defaultFields = array_fill_keys(SearchAirportsRequest::SEARCHABLE_FIELDS, $this->likeOperator());
if ($searchFields === null || $searchFields === '') {
return $defaultFields;
}

View File

@ -7,6 +7,7 @@ namespace App\Queries;
use App\Http\Requests\SearchFlightsRequest;
use App\Models\Flight;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
/**
* Builds the Eloquent query for Flight list/search endpoints.
@ -19,14 +20,6 @@ use Illuminate\Database\Eloquent\Builder;
*/
class FlightSearchQuery
{
/**
* Field-specific search allowlist. Mirrors the old
* FlightRepository::$fieldSearchable + RequestCriteria's
* `?search=field:value;...` syntax. The 'like' entries match the
* legacy LIKE behavior; everything else is exact match.
*
* @var array<string, 'exact'|'like'>
*/
private const array FIELD_SEARCH = [
'arr_airport_id' => 'exact',
'callsign' => 'exact',
@ -41,6 +34,11 @@ class FlightSearchQuery
'notes' => 'like',
];
private function likeOperator(): string
{
return DB::connection()->getDriverName() === 'pgsql' ? 'ilike' : 'like';
}
/**
* @var list<string>
*/
@ -75,12 +73,6 @@ class FlightSearchQuery
return $query;
}
/**
* Restore the legacy `?search=field:value;field:value` syntax.
* Mirrors PirepSearchQuery / UserSearchQuery from earlier phases.
*
* @param Builder<Flight> $query
*/
private function applySearch(Builder $query, SearchFlightsRequest $request): void
{
$search = trim((string) $request->input('search', ''));
@ -88,6 +80,8 @@ class FlightSearchQuery
return;
}
$like = $this->likeOperator();
if (str_contains($search, ':')) {
$clauses = [];
foreach (explode(';', $search) as $pair) {
@ -114,10 +108,10 @@ class FlightSearchQuery
}
if ($clauses !== []) {
$query->where(function (Builder $q) use ($clauses): void {
$query->where(function (Builder $q) use ($clauses, $like): void {
foreach ($clauses as [$field, $value, $mode]) {
if ($mode === 'like') {
$q->orWhere($field, 'like', '%'.$value.'%');
$q->orWhere($field, $like, '%'.$value.'%');
} else {
$q->orWhere($field, '=', $value);
}
@ -128,9 +122,9 @@ class FlightSearchQuery
}
}
$query->where(function (Builder $q) use ($search): void {
$query->where(function (Builder $q) use ($search, $like): void {
foreach (self::FREE_TEXT_COLUMNS as $column) {
$q->orWhere($column, 'like', '%'.$search.'%');
$q->orWhere($column, $like, '%'.$search.'%');
}
});
}

View File

@ -7,6 +7,7 @@ namespace App\Queries;
use App\Http\Requests\SearchPirepsRequest;
use App\Models\Pirep;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
/**
* Builds the Eloquent query for PIREP list endpoints.
@ -37,6 +38,11 @@ class PirepSearchQuery
*/
private const array FREE_TEXT_COLUMNS = ['id', 'flight_number'];
private function likeOperator(): string
{
return DB::connection()->getDriverName() === 'pgsql' ? 'ilike' : 'like';
}
public function build(SearchPirepsRequest $request): Builder
{
$query = Pirep::query()
@ -92,8 +98,9 @@ class PirepSearchQuery
}
$query->where(function (Builder $q) use ($search): void {
$like = $this->likeOperator();
foreach (self::FREE_TEXT_COLUMNS as $col) {
$q->orWhere($col, 'like', '%'.$search.'%');
$q->orWhere($col, $like, '%'.$search.'%');
}
});
}

View File

@ -8,6 +8,7 @@ use App\Enums\UserState;
use App\Http\Requests\SearchUsersRequest;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
/**
* Builds the Eloquent query for the public pilots list (`/users`).
@ -37,6 +38,15 @@ class UserSearchQuery
'state' => '=',
];
private function resolveOperator(string $operator): string
{
if ($operator !== 'like') {
return $operator;
}
return $this->likeOperator();
}
/**
* Free-text search columns (when search has no `field:` prefix).
*
@ -44,6 +54,11 @@ class UserSearchQuery
*/
private const array FREE_TEXT_COLUMNS = ['name', 'email'];
private function likeOperator(): string
{
return DB::connection()->getDriverName() === 'pgsql' ? 'ilike' : 'like';
}
public function build(SearchUsersRequest $request): Builder
{
$query = User::query()
@ -109,14 +124,14 @@ class UserSearchQuery
continue;
}
$clauses[] = [$field, self::FIELD_SEARCH[$field], $value];
$clauses[] = [$field, $this->resolveOperator(self::FIELD_SEARCH[$field]), $value];
}
if ($clauses !== []) {
$query->where(function (Builder $q) use ($clauses): void {
foreach ($clauses as [$field, $operator, $value]) {
if ($operator === 'like') {
$q->orWhere($field, 'like', '%'.$value.'%');
if (in_array($operator, ['like', 'ilike'], true)) {
$q->orWhere($field, $operator, '%'.$value.'%');
} else {
$q->orWhere($field, '=', $value);
}
@ -130,9 +145,10 @@ class UserSearchQuery
}
// Free-text: OR across name + email
$query->where(function (Builder $q) use ($search): void {
$like = $this->likeOperator();
$query->where(function (Builder $q) use ($search, $like): void {
foreach (self::FREE_TEXT_COLUMNS as $col) {
$q->orWhere($col, 'like', '%'.$search.'%');
$q->orWhere($col, $like, '%'.$search.'%');
}
});
}

View File

@ -179,16 +179,32 @@ class FlightService extends Service
}
// Match nullable scalar columns including legacy empty-string values.
// Stored values may be NULL, '', or an actual scalar; treat empty as
// equivalent to null so casts that coerce '' to 0 (e.g. integer cast
// on route_leg) still resolve correctly.
foreach (['route_code', 'route_leg', 'days'] as $column) {
// route_code is a string column — empty strings are valid on all DBs.
// route_leg and days are integer columns — PG cannot compare int to '',
// so only check null and 0.
$stringColumns = ['route_code'];
$integerColumns = ['route_leg', 'days'];
foreach ($stringColumns as $column) {
$value = $flight->{$column};
if (in_array($value, [null, '', '0'], true)) {
$query->where(function ($q) use ($column): void {
$q->whereNull($column)
->orWhere($column, '')
->orWhere($column, 0);
});
} else {
$query->where($column, $value);
}
}
foreach ($integerColumns as $column) {
$value = $flight->{$column};
if (in_array($value, [null, '', 0, '0'], true)) {
$query->where(function ($q) use ($column): void {
$q->whereNull($column)
->orWhere($column, '')
->orWhere($column, 0);
});
} else {

View File

@ -58,6 +58,10 @@ class AirportImporter extends ImportExport
$row['fuel_jeta_cost'] = (float) $row['fuel_jeta_cost'];
}
$row['elevation'] = is_numeric($row['elevation'] ?? '') ? (int) $row['elevation'] : null;
$row['fuel_100ll_cost'] = is_numeric($row['fuel_100ll_cost'] ?? '') ? (float) $row['fuel_100ll_cost'] : null;
$row['fuel_mogas_cost'] = is_numeric($row['fuel_mogas_cost'] ?? '') ? (float) $row['fuel_mogas_cost'] : null;
try {
Airport::updateOrCreate([
'id' => $row['icao'],

View File

@ -46,8 +46,8 @@ class FlightImporter extends ImportExport
'distance' => 'nullable|numeric',
'flight_time' => 'required|integer',
'flight_type' => 'required|alpha',
'load_factor' => 'nullable',
'load_factor_variance' => 'nullable',
'load_factor' => 'nullable|numeric|min:0|max:100',
'load_factor_variance' => 'nullable|numeric|min:0|max:100',
'pilot_pay' => 'nullable',
'route' => 'nullable',
'notes' => 'nullable',
@ -226,9 +226,9 @@ class FlightImporter extends ImportExport
$this->processAirport($row['alt_airport']);
}
$this->processSubfleets($flight, $row['subfleets']);
$this->processFares($flight, $row['fares']);
$this->processFields($flight, $row['fields']);
$this->processSubfleets($flight, $row['subfleets'] ?? '');
$this->processFares($flight, $row['fares'] ?? '');
$this->processFields($flight, $row['fields'] ?? '');
$this->log('Imported row '.($index + 1));

View File

@ -70,8 +70,8 @@ class SubfleetImporter extends ImportExport
return false;
}
$this->processFares($subfleet, $row['fares']);
$this->processRanks($subfleet, $row['ranks']);
$this->processFares($subfleet, $row['fares'] ?? '');
$this->processRanks($subfleet, $row['ranks'] ?? '');
$this->log('Imported '.$row['type']);

View File

@ -85,10 +85,11 @@ class ImportService extends Service
$records = $reader->getRecords($header_rows);
foreach ($records as $offset => $row) {
// turn it into a collection and run some filtering
$row = collect($row)->map(function ($val, $index): string {
$row = collect($row)->map(function ($val, $index): ?string {
$val = trim($val);
$val = str_ireplace(['\\n', '\\r'], '', $val);
return str_ireplace(['\\n', '\\r'], '', $val);
return $val === '' ? null : $val;
})->toArray();
// Try to validate

View File

@ -443,9 +443,9 @@ class PirepService extends Service
// If pirep is still at PENDING or DRAFT state decide the default behavior by looking at rank settings
if ($pirep->state === PirepState::PENDING || $pirep->state === PirepState::DRAFT) {
if ($pirep->source === PirepSource::ACARS && $pirep->user->rank->auto_approve_acars) {
if ($pirep->source === PirepSource::ACARS && $pirep->user->rank?->auto_approve_acars) {
$default_state = PirepState::ACCEPTED;
} elseif ($pirep->source === PirepSource::MANUAL && $pirep->user->rank->auto_approve_manual) {
} elseif ($pirep->source === PirepSource::MANUAL && $pirep->user->rank?->auto_approve_manual) {
$default_state = PirepState::ACCEPTED;
}
}

View File

@ -28,6 +28,7 @@ use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Auth\Events\Registered;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
@ -208,12 +209,15 @@ class UserService extends Service
return $user;
}
$user->pilot_id = $this->getNextAvailablePilotId();
$user->save();
return DB::transaction(function () use ($user): User {
$maxPilotId = (int) User::withTrashed()->max('pilot_id');
$user->pilot_id = $maxPilotId + 1;
$user->save();
Log::info('Set pilot ID for user '.$user->id.' to '.$user->pilot_id);
Log::info('Set pilot ID for user '.$user->id.' to '.$user->pilot_id);
return $user;
return $user;
});
}
/**
@ -236,19 +240,21 @@ class UserService extends Service
return $user;
}
if ($this->isPilotIdAlreadyUsed($pilot_id)) {
Log::error('User with id '.$pilot_id.' already exists');
return DB::transaction(function () use ($user, $pilot_id): User {
if (User::where('pilot_id', '=', $pilot_id)->exists()) {
Log::error('User with id '.$pilot_id.' already exists');
throw new UserPilotIdExists($user);
}
throw new UserPilotIdExists($user);
}
$old_id = $user->pilot_id;
$user->pilot_id = $pilot_id;
$user->save();
$old_id = $user->pilot_id;
$user->pilot_id = $pilot_id;
$user->save();
Log::info('Changed pilot ID for user '.$user->id.' from '.$old_id.' to '.$user->pilot_id);
Log::info('Changed pilot ID for user '.$user->id.' from '.$old_id.' to '.$user->pilot_id);
return $user;
return $user;
});
}
/**
@ -268,12 +274,12 @@ class UserService extends Service
/** @var Airline $airline */
foreach ($airlines as $airline) {
if (str_contains($pilot_id, $airline->icao)) {
if (str_starts_with($pilot_id, $airline->icao)) {
$ident_str = $airline->icao;
break;
}
if (!empty($airline->iata) && str_contains($pilot_id, (string) $airline->iata)) {
if (!empty($airline->iata) && str_starts_with($pilot_id, (string) $airline->iata)) {
$ident_str = $airline->iata;
break;
}
@ -284,6 +290,10 @@ class UserService extends Service
}
$parsed_pilot_id = str_replace($ident_str, '', $pilot_id);
if ($parsed_pilot_id === '' || !ctype_digit($parsed_pilot_id)) {
throw new PilotIdNotFound($pilot_id);
}
$user = User::where(['airline_id' => $airline->id, 'pilot_id' => $parsed_pilot_id])->first();
if (empty($user)) {
throw new PilotIdNotFound($pilot_id);

View File

@ -1,6 +1,7 @@
{
"name": "phpvms/phpvms",
"description": "phpVMS - Virtual Airline Administration",
"version": "8.0",
"keywords": [
"phpvms",
"virtual",

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
/** @noinspection PhpIllegalPsrClassPathInspection */
namespace Database\Factories;
@ -29,9 +31,9 @@ class AcarsFactory extends Factory
'id' => null,
'pirep_id' => null,
'log' => fake()->text(100),
'lat' => fake()->latitude,
'lon' => fake()->longitude,
'distance' => fake()->randomFloat(2, 0, 6000),
'lat' => fake()->latitude(),
'lon' => fake()->longitude(),
'distance' => fake()->numberBetween(0, 6000),
'heading' => fake()->numberBetween(0, 359),
'altitude_agl' => fake()->numberBetween(20, 400),
'altitude_msl' => fake()->numberBetween(20, 400),

View File

@ -40,8 +40,8 @@ class AircraftFactory extends Factory
return [
'subfleet_id' => fn () => Subfleet::factory()->create()->id,
'airport_id' => fn () => Airport::factory()->create()->id,
'iata' => fake()->unique()->text(5),
'icao' => fake()->unique()->text(5),
'iata' => fake()->unique()->lexify('???'),
'icao' => fake()->unique()->lexify('????'),
'name' => fake()->text(50),
'registration' => fake()->unique()->text(10),
'hex_code' => ICAO::createHexCode(),

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Database\Factories;
use App\Models\Event;
@ -20,6 +22,7 @@ class EventFactory extends Factory
$startDate = fake()->dateTimeBetween('-1 month', '+1 month');
return [
'id' => fake()->unique()->randomNumber(5),
'type' => fake()->numberBetween(),
'name' => fake()->text(50),
'description' => fake()->text(150),

View File

@ -32,7 +32,7 @@ class ExpenseFactory extends Factory
return [
'airline_id' => null,
'name' => fake()->text(20),
'amount' => fake()->randomFloat(2, 100, 1000),
'amount' => fake()->numberBetween(100, 1000),
'type' => ExpenseType::FLIGHT,
'multiplier' => false,
'ref_model_type' => Expense::class,

View File

@ -31,8 +31,8 @@ class NewsFactory extends Factory
{
return [
'user_id' => fn () => User::factory()->create()->id,
'subject' => fake()->text(),
'body' => fake()->sentence(),
'subject' => fake()->sentence(),
'body' => fake()->text(),
];
}
}

View File

@ -56,7 +56,7 @@ class PirepFactory extends Factory
'planned_distance' => fake()->randomFloat(2, 0, 6000),
'flight_time' => fake()->numberBetween(60, 360),
'planned_flight_time' => fake()->numberBetween(60, 360),
'zfw' => fake()->randomFloat(2),
'zfw' => fake()->randomFloat(2, 0, 500000),
'block_fuel' => fake()->randomFloat(2, 0, 1000),
'fuel_used' => fn (array $pirep): float => round($pirep['block_fuel'] * .9, 2),
'block_on_time' => Carbon::now('UTC'),

View File

@ -12,7 +12,7 @@ return new class() extends Migration
public function up(): void
{
if (!Schema::hasTable('migrations_data')) {
Schema::create('migrations_data', function (Blueprint $table) {
Schema::create('migrations_data', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -23,7 +23,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('users')) {
Schema::create('users', function (Blueprint $table) {
Schema::create('users', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -67,7 +67,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('acars')) {
Schema::create('acars', function (Blueprint $table) {
Schema::create('acars', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -100,7 +100,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('activity_log')) {
Schema::create('activity_log', function (Blueprint $table) {
Schema::create('activity_log', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -122,7 +122,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('aircraft')) {
Schema::create('aircraft', function (Blueprint $table) {
Schema::create('aircraft', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -145,7 +145,7 @@ return new class() extends Migration
$table->string('simbrief_type', 25)->nullable();
$table->decimal('fuel_onboard')->unsigned()->nullable()->default(0);
$table->unsignedBigInteger('flight_time')->nullable()->default(0);
$table->char('status', 1)->default('A');
$table->string('status', 1)->default('A');
$table->unsignedTinyInteger('state')->default(0);
$table->timestamps();
$table->softDeletes();
@ -153,7 +153,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('airlines')) {
Schema::create('airlines', function (Blueprint $table) {
Schema::create('airlines', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -175,7 +175,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('airports')) {
Schema::create('airports', function (Blueprint $table) {
Schema::create('airports', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -201,7 +201,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('awards')) {
Schema::create('awards', function (Blueprint $table) {
Schema::create('awards', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -218,7 +218,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('bids')) {
Schema::create('bids', function (Blueprint $table) {
Schema::create('bids', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -233,7 +233,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('events')) {
Schema::create('events', function (Blueprint $table) {
Schema::create('events', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -249,7 +249,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('expenses')) {
Schema::create('expenses', function (Blueprint $table) {
Schema::create('expenses', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -257,7 +257,7 @@ return new class() extends Migration
$table->unsignedInteger('airline_id')->nullable();
$table->string('name');
$table->unsignedInteger('amount');
$table->char('type');
$table->string('type', 1);
$table->string('flight_type', 50)->nullable();
$table->boolean('charge_to_user')->nullable()->default(false);
$table->boolean('multiplier')->nullable()->default(false);
@ -271,7 +271,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('failed_jobs')) {
Schema::create('failed_jobs', function (Blueprint $table) {
Schema::create('failed_jobs', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -285,7 +285,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('fares')) {
Schema::create('fares', function (Blueprint $table) {
Schema::create('fares', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -304,7 +304,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('files')) {
Schema::create('files', function (Blueprint $table) {
Schema::create('files', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -324,7 +324,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('flight_fare')) {
Schema::create('flight_fare', function (Blueprint $table) {
Schema::create('flight_fare', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -340,7 +340,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('flight_field_values')) {
Schema::create('flight_field_values', function (Blueprint $table) {
Schema::create('flight_field_values', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -354,7 +354,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('flight_fields')) {
Schema::create('flight_fields', function (Blueprint $table) {
Schema::create('flight_fields', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -365,7 +365,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('flight_subfleet')) {
Schema::create('flight_subfleet', function (Blueprint $table) {
Schema::create('flight_subfleet', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -379,7 +379,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('flights')) {
Schema::create('flights', function (Blueprint $table) {
Schema::create('flights', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -397,7 +397,7 @@ return new class() extends Migration
$table->unsignedInteger('level')->nullable()->default(0);
$table->decimal('distance')->unsigned()->nullable()->default(0);
$table->unsignedInteger('flight_time')->nullable();
$table->char('flight_type', 1)->default('J');
$table->string('flight_type', 1)->default('J');
$table->decimal('load_factor', 5)->nullable();
$table->decimal('load_factor_variance', 5)->nullable();
$table->text('route')->nullable();
@ -422,7 +422,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('invites')) {
Schema::create('invites', function (Blueprint $table) {
Schema::create('invites', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -437,7 +437,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('jobs')) {
Schema::create('jobs', function (Blueprint $table) {
Schema::create('jobs', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -452,7 +452,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('journal_transactions')) {
Schema::create('journal_transactions', function (Blueprint $table) {
Schema::create('journal_transactions', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -475,7 +475,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('journals')) {
Schema::create('journals', function (Blueprint $table) {
Schema::create('journals', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -493,7 +493,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('kvp')) {
Schema::create('kvp', function (Blueprint $table) {
Schema::create('kvp', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -503,7 +503,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('ledgers')) {
Schema::create('ledgers', function (Blueprint $table) {
Schema::create('ledgers', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -515,7 +515,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('navdata')) {
Schema::create('navdata', function (Blueprint $table) {
Schema::create('navdata', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -531,7 +531,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('news')) {
Schema::create('news', function (Blueprint $table) {
Schema::create('news', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -544,7 +544,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('notifications')) {
Schema::create('notifications', function (Blueprint $table) {
Schema::create('notifications', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -552,7 +552,7 @@ return new class() extends Migration
$table->string('type');
$table->string('notifiable_type');
$table->unsignedBigInteger('notifiable_id');
$table->text('data');
$table->json('data');
$table->timestamp('read_at')->nullable();
$table->timestamps();
@ -561,7 +561,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('pages')) {
Schema::create('pages', function (Blueprint $table) {
Schema::create('pages', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -580,7 +580,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('password_resets')) {
Schema::create('password_resets', function (Blueprint $table) {
Schema::create('password_resets', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -633,7 +633,7 @@ return new class() extends Migration
}*/
if (!Schema::hasTable('pirep_comments')) {
Schema::create('pirep_comments', function (Blueprint $table) {
Schema::create('pirep_comments', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -646,7 +646,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('pirep_fares')) {
Schema::create('pirep_fares', function (Blueprint $table) {
Schema::create('pirep_fares', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -665,7 +665,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('pirep_field_values')) {
Schema::create('pirep_field_values', function (Blueprint $table) {
Schema::create('pirep_field_values', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -680,7 +680,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('pirep_fields')) {
Schema::create('pirep_fields', function (Blueprint $table) {
Schema::create('pirep_fields', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -694,7 +694,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('pireps')) {
Schema::create('pireps', function (Blueprint $table) {
Schema::create('pireps', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -707,7 +707,7 @@ return new class() extends Migration
$table->string('flight_number', 10)->nullable()->index();
$table->string('route_code', 5)->nullable();
$table->string('route_leg', 5)->nullable();
$table->char('flight_type', 1)->default('J');
$table->string('flight_type', 1)->default('J');
$table->string('dpt_airport_id', 5)->index();
$table->string('arr_airport_id', 5)->index();
$table->string('alt_airport_id', 5)->nullable();
@ -726,7 +726,7 @@ return new class() extends Migration
$table->unsignedTinyInteger('source')->nullable()->default(0);
$table->string('source_name', 50)->nullable();
$table->unsignedSmallInteger('state')->default(1);
$table->char('status', 3)->default('SCH');
$table->string('status', 3)->default('SCH');
$table->dateTime('submitted_at')->nullable();
$table->dateTime('block_off_time')->nullable();
$table->dateTime('block_on_time')->nullable();
@ -736,7 +736,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('ranks')) {
Schema::create('ranks', function (Blueprint $table) {
Schema::create('ranks', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -786,7 +786,7 @@ return new class() extends Migration
}*/
if (!Schema::hasTable('sessions')) {
Schema::create('sessions', function (Blueprint $table) {
Schema::create('sessions', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -800,7 +800,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('settings')) {
Schema::create('settings', function (Blueprint $table) {
Schema::create('settings', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -820,7 +820,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('simbrief')) {
Schema::create('simbrief', function (Blueprint $table) {
Schema::create('simbrief', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -840,7 +840,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('simbrief_aircraft')) {
Schema::create('simbrief_aircraft', function (Blueprint $table) {
Schema::create('simbrief_aircraft', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -853,7 +853,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('simbrief_airframes')) {
Schema::create('simbrief_airframes', function (Blueprint $table) {
Schema::create('simbrief_airframes', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -869,7 +869,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('simbrief_layouts')) {
Schema::create('simbrief_layouts', function (Blueprint $table) {
Schema::create('simbrief_layouts', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -881,7 +881,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('stats')) {
Schema::create('stats', function (Blueprint $table) {
Schema::create('stats', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -895,7 +895,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('subfleet_fare')) {
Schema::create('subfleet_fare', function (Blueprint $table) {
Schema::create('subfleet_fare', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -912,7 +912,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('subfleet_rank')) {
Schema::create('subfleet_rank', function (Blueprint $table) {
Schema::create('subfleet_rank', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -927,7 +927,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('subfleets')) {
Schema::create('subfleets', function (Blueprint $table) {
Schema::create('subfleets', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -950,7 +950,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('typerating_subfleet')) {
Schema::create('typerating_subfleet', function (Blueprint $table) {
Schema::create('typerating_subfleet', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -963,7 +963,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('typerating_user')) {
Schema::create('typerating_user', function (Blueprint $table) {
Schema::create('typerating_user', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -976,7 +976,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('typeratings')) {
Schema::create('typeratings', function (Blueprint $table) {
Schema::create('typeratings', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -993,7 +993,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('user_awards')) {
Schema::create('user_awards', function (Blueprint $table) {
Schema::create('user_awards', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -1007,7 +1007,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('user_field_values')) {
Schema::create('user_field_values', function (Blueprint $table) {
Schema::create('user_field_values', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -1022,7 +1022,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('user_fields')) {
Schema::create('user_fields', function (Blueprint $table) {
Schema::create('user_fields', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -1039,7 +1039,7 @@ return new class() extends Migration
}
if (!Schema::hasTable('user_oauth_tokens')) {
Schema::create('user_oauth_tokens', function (Blueprint $table) {
Schema::create('user_oauth_tokens', function (Blueprint $table): void {
$table->collation = 'utf8mb4_unicode_ci';
$table->charset = 'utf8mb4';
@ -1054,7 +1054,7 @@ return new class() extends Migration
}
if (Schema::hasTable('permission_role')) {
Schema::table('permission_role', function (Blueprint $table) {
Schema::table('permission_role', function (Blueprint $table): void {
// Check if the foreign key already exists
// See https://github.com/laravel/framework/discussions/43443
$foreignKeys = collect(Schema::getForeignKeys('permission_role'));
@ -1074,7 +1074,7 @@ return new class() extends Migration
}
if (Schema::hasTable('permission_user')) {
Schema::table('permission_user', function (Blueprint $table) {
Schema::table('permission_user', function (Blueprint $table): void {
$foreignKeys = collect(Schema::getForeignKeys('permission_user'));
if ($foreignKeys->where('name', 'permission_user_permission_id_foreign')->count() === 0) {
@ -1086,7 +1086,7 @@ return new class() extends Migration
}
if (Schema::hasTable('role_user')) {
Schema::table('role_user', function (Blueprint $table) {
Schema::table('role_user', function (Blueprint $table): void {
$foreignKeys = collect(Schema::getForeignKeys('role_user'));
if ($foreignKeys->where('name', 'role_user_role_id_foreign')->count() === 0) {

View File

@ -1,26 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class() extends Migration
{
public function up(): void
{
Schema::table('airlines', function (Blueprint $table): void {
$table->string('icao', 12)->change();
$table->string('iata', 12)->nullable()->change();
$table->string('country', 3)->nullable()->change();
});
}
public function down(): void
{
Schema::table('airlines', function (Blueprint $table): void {
$table->string('icao', 5)->change();
$table->string('iata', 5)->nullable()->change();
$table->string('country', 2)->nullable()->change();
});
}
};

View File

@ -1,24 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class() extends Migration
{
public function up(): void
{
Schema::table('activity_log', function (Blueprint $table): void {
$table->string('subject_id', 128)->nullable()->change();
$table->string('batch_uuid', 36)->nullable()->change();
});
}
public function down(): void
{
Schema::table('activity_log', function (Blueprint $table): void {
$table->char('subject_id', 36)->nullable()->change();
$table->char('batch_uuid', 36)->nullable()->change();
});
}
};

View File

@ -1,8 +0,0 @@
[env]
BUILDKIT_HOST = "docker-container://buildkit"
[tasks.setup]
run = ["mise install", "mise run run-buildkit-container || true"]
[tasks.run-buildkit-container]
run = "docker run --rm --privileged -d --name buildkit -e BUILDKIT_DEBUG=1 moby/buildkit:latest"

View File

@ -31,9 +31,9 @@
<env name="BCRYPT_ROUNDS" value="4" force="true"/>
<env name="BROADCAST_CONNECTION" value="null" force="true"/>
<env name="CACHE_STORE" value="array" force="true"/>
<env name="DB_CONNECTION" value="sqlite" force="true"/>
<env name="DB_DATABASE" value=":memory:" force="true"/>
<env name="DB_URL" value="" force="true"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="DB_URL" value=""/>
<env name="MAIL_MAILER" value="array" force="true"/>
<env name="QUEUE_CONNECTION" value="sync" force="true"/>
<env name="SESSION_DRIVER" value="array" force="true"/>

32
resources/mise/php.ini Normal file
View File

@ -0,0 +1,32 @@
; php.ini - Minimal Testing Configuration
error_reporting = E_ALL
display_errors = On
display_startup_errors = On
log_errors = On
html_errors = On
memory_limit = -1
upload_max_filesize = 100M
post_max_size = 100M
max_execution_time = 300
max_input_time = 300
variables_order = "EGPCS"
default_socket_timeout = 60
date.timezone = UTC
session.use_strict_mode = 1
session.use_cookies = 1
session.use_only_cookies = 1
extension_dir = "/opt/homebrew/lib/php/pecl/20250925"
; Common Extensions (Uncomment as needed)
; extension=curl
; extension=gd
; extension=mbstring
; extension=openssl
; extension=pdo_mysql
; On windows:
;extension_dir = "ext"

View File

@ -261,6 +261,7 @@ it('can retrieve an airport', function (): void {
apiAs($user);
$airport = Airport::factory()->create();
$airport->refresh();
$response = $this->get('/api/airports/'.$airport->icao);

View File

@ -234,11 +234,11 @@ test('plain search matches free text columns', function (): void {
/** @var Flight $target */
$target = Flight::factory()->create([
'dpt_airport_id' => 'KLAX',
'callsign' => 'SEARCHME',
'callsign' => 'SCH',
]);
Flight::factory()->create([
'dpt_airport_id' => 'KJFK',
'callsign' => 'IGNOREME',
'callsign' => 'IGN',
]);
$results = flightSearchRun(['search' => 'LAX']);

View File

@ -1189,6 +1189,7 @@ test('daily expenses are applied', function (): void {
'airline_id' => null,
'type' => ExpenseType::DAILY,
]);
$expense->refresh();
/** @var RecurringFinanceService $recurringFService */
$recurringFService = app(RecurringFinanceService::class);
@ -1199,15 +1200,12 @@ test('daily expenses are applied', function (): void {
'ref_model_type' => Expense::class,
'ref_model_id' => $expense->id,
'debit' => Money::createFromAmount($expense->amount)->toAmount(),
'post_date' => Carbon::getTestNow(),
]);
$this->assertDatabaseHas('journal_transactions', [
'journal_id' => $airline2->journal->id,
'ref_model_type' => Expense::class,
'ref_model_id' => $expense->id,
'debit' => Money::createFromAmount($expense->amount)->toAmount(),
'post_date' => Carbon::getTestNow(),
]);
});

View File

@ -250,7 +250,7 @@ test('aircraft exporter', function (): void {
$collection = collect([$aircraft]);
$file = $exporter->exportAircraft($collection);
$status = $importer->importAircraft($file);
$status = $importer->importAircraft($file, delete_previous: false);
expect($status['success'])->toHaveCount(1)
->and($status['errors'])->toHaveCount(0);
});
@ -272,7 +272,7 @@ test('airport exporter', function (): void {
$importer = app(ImportService::class);
$exporter = app(ExportService::class);
$file = $exporter->exportAirports(collect([$airport]));
$status = $importer->importAirports($file);
$status = $importer->importAirports($file, delete_previous: false);
expect($status['success'])->toHaveCount(1)
->and($status['errors'])->toHaveCount(0);
@ -330,7 +330,7 @@ test('flight exporter', function (): void {
$importer = app(ImportService::class);
$exporter = app(ExportService::class);
$file = $exporter->exportFlights(collect([$flight]));
$status = $importer->importFlights($file);
$status = $importer->importFlights($file, delete_previous: '');
expect($status['success'])->toHaveCount(1)
->and($status['errors'])->toHaveCount(0);
});
@ -339,14 +339,14 @@ test('invalid file import', function (): void {
// $this->expectException(ValidationException::class);
$file_path = base_path('tests/data/aircraft.csv');
$importer = app(ImportService::class);
$status = $importer->importAirports($file_path);
$status = $importer->importAirports($file_path, delete_previous: false);
expect($status['errors'])->toHaveCount(2);
});
test('empty cols', function (): void {
$file_path = base_path('tests/data/expenses_empty_rows.csv');
$importer = app(ImportService::class);
$status = $importer->importExpenses($file_path);
$status = $importer->importExpenses($file_path, delete_previous: false);
expect($status['success'])->toHaveCount(8)
->and($status['errors'])->toHaveCount(0);
});
@ -367,7 +367,7 @@ test('expense exporter', function (): void {
$importer = app(ImportService::class);
$exporter = app(ExportService::class);
$file = $exporter->exportExpenses(collect([$expense]));
$status = $importer->importExpenses($file);
$status = $importer->importExpenses($file, delete_previous: false);
expect($status['success'])->toHaveCount(1)
->and($status['errors'])->toHaveCount(0);
@ -383,7 +383,7 @@ test('expense importer', function (): void {
$importer = app(ImportService::class);
$file_path = base_path('tests/data/expenses.csv');
$status = $importer->importExpenses($file_path);
$status = $importer->importExpenses($file_path, delete_previous: false);
expect($status['success'])->toHaveCount(8)
->and($status['errors'])->toHaveCount(0);
@ -414,7 +414,7 @@ test('expense importer', function (): void {
test('fare importer', function (): void {
$file_path = base_path('tests/data/fares.csv');
$importer = app(ImportService::class);
$status = $importer->importFares($file_path);
$status = $importer->importFares($file_path, delete_previous: false);
expect($status['success'])->toHaveCount(4)
->and($status['errors'])->toHaveCount(0);
@ -591,7 +591,7 @@ test('aircraft importer', function (): void {
$importer = app(ImportService::class);
// $subfleet = \App\Models\Subfleet::factory()->create(['type' => 'A32X']);
$file_path = base_path('tests/data/aircraft.csv');
$status = $importer->importAircraft($file_path);
$status = $importer->importAircraft($file_path, delete_previous: false);
expect($status['success'])->toHaveCount(1)
->and($status['errors'])->toHaveCount(1);
@ -615,7 +615,7 @@ test('aircraft importer', function (): void {
// Now try importing the updated file, the status for the aircraft should change
// to being stored
$file_path = base_path('tests/data/aircraft-update.csv');
$status = $importer->importAircraft($file_path);
$status = $importer->importAircraft($file_path, delete_previous: false);
expect($status['success'])->toHaveCount(1);
$aircraft = Aircraft::where([
@ -628,7 +628,7 @@ test('aircraft importer', function (): void {
test('airport importer', function (): void {
$importer = app(ImportService::class);
$file_path = base_path('tests/data/airports.csv');
$status = $importer->importAirports($file_path);
$status = $importer->importAirports($file_path, delete_previous: false);
expect($status['success'])->toHaveCount(2)
->and($status['errors'])->toHaveCount(1);
@ -648,8 +648,8 @@ test('airport importer', function (): void {
->and($airport->country)->toEqual('US')
->and($airport->timezone)->toEqual('America/Chicago')
->and($airport->hub)->toBeTrue()
->and($airport->lat)->toEqual('30.1945')
->and($airport->lon)->toEqual('-97.6699')
->and($airport->lat)->toEqualWithDelta(30.1945, 0.001)
->and($airport->lon)->toEqualWithDelta(-97.6699, 0.001)
->and($airport->ground_handling_cost)->toEqual(0.0)
->and($airport->fuel_jeta_cost)->toEqual(setting('airports.default_jet_a_fuel_cost'))
->and($airport->notes)->toEqual('Test Note');
@ -668,7 +668,7 @@ test('airport importer', function (): void {
test('airport importer invalid inputs', function (): void {
$importer = app(ImportService::class);
$file_path = base_path('tests/data/airports_errors.csv');
$status = $importer->importAirports($file_path);
$status = $importer->importAirports($file_path, delete_previous: false);
expect($status['success'])->toHaveCount(5)
->and($status['errors'])->toHaveCount(1);
@ -683,8 +683,8 @@ test('airport importer invalid inputs', function (): void {
->and($airport->iata)->toEqual('')
->and($airport->timezone)->toEqual('America/Winnipeg')
->and($airport->hub)->toBeFalse()
->and($airport->lat)->toEqual('50.0564003')
->and($airport->lon)->toEqual('-97.03250122');
->and($airport->lat)->toEqualWithDelta(50.0564, 0.001)
->and($airport->lon)->toEqualWithDelta(-97.03250, 0.001);
});
test('subfleet importer', function (): void {
@ -696,7 +696,7 @@ test('subfleet importer', function (): void {
$importer = app(ImportService::class);
$file_path = base_path('tests/data/subfleets.csv');
$status = $importer->importSubfleets($file_path);
$status = $importer->importSubfleets($file_path, delete_previous: false);
expect($status['success'])->toHaveCount(1)
->and($status['errors'])->toHaveCount(1);
@ -707,7 +707,7 @@ test('subfleet importer', function (): void {
])->first();
expect($subfleet)->not->toBeNull()
->and($subfleet->id)->toEqual($airline->id)
->and($subfleet->airline_id)->toEqual($airline->id)
->and($subfleet->type)->toEqual('A32X')
->and($subfleet->name)->toEqual('Airbus A320');
@ -749,7 +749,7 @@ test('subfleet importer', function (): void {
test('airport special chars importer', function (): void {
$importer = app(ImportService::class);
$file_path = base_path('tests/data/airports_special_chars.csv');
$status = $importer->importAirports($file_path);
$status = $importer->importAirports($file_path, delete_previous: false);
// See if it imported
$airport = Airport::where([

View File

@ -241,11 +241,13 @@ test('pirep notifications', function (): void {
Notification::fake();
$rank = Rank::factory()->create();
$user = User::factory()->create([
'name' => 'testPirepNotifications user',
'flights' => 0,
'flight_time' => 0,
'rank_id' => 1,
'rank_id' => $rank->id,
]);
$admin = createAdminUser(['name' => 'testPirepNotifications Admin']);
@ -709,7 +711,7 @@ test('diversion handler reuses matching reposition flight and attaches subfleet'
'airline_id' => $airline->id,
'dpt_airport_id' => $departureAirport->id,
'arr_airport_id' => $originalArrivalAirport->id,
'callsign' => 'TEST6000',
'callsign' => 'TST6',
'flight_number' => 6000,
]);
$flight->subfleets()->syncWithoutDetaching([$subfleet->id]);

View File

@ -270,8 +270,8 @@ test('attach to pirep', function (): void {
$fix = $acars->firstWhere('name', 'BOMUP');
expect($fix['name'])->toEqual('BOMUP')
->and($fix['lat'])->toEqual(24.484639)
->and($fix['lon'])->toEqual(54.578444)
->and($fix['lat'])->toEqualWithDelta(24.484639, 0.00001)
->and($fix['lon'])->toEqualWithDelta(54.578444, 0.00001)
->and($fix['order'])->toEqual(1);
$briefing->refresh();

View File

@ -269,7 +269,7 @@ test('user pilot id split', function (): void {
expect($found_user->id)->toEqual($user->id);
// Look for them with the IATA code
$found_user = $userSvc->findUserByPilotId($user->airline->iata.$user->id);
$found_user = $userSvc->findUserByPilotId($user->airline->iata.$user->pilot_id);
expect($found_user->id)->toEqual($user->id);
});
@ -293,21 +293,21 @@ test('user pilot id added', function (): void {
$new_user = User::factory()->make()->makeVisible(['api_key', 'name', 'email'])->toArray();
$new_user['password'] = Hash::make('secret');
$user = $userSvc->createUser($new_user);
expect($user->pilot_id)->toEqual($user->id);
expect($user->pilot_id)->toBeGreaterThan(0);
// Add a second user
$new_user = User::factory()->make()->makeVisible(['api_key', 'name', 'email'])->toArray();
$new_user['password'] = Hash::make('secret');
$user2 = $userSvc->createUser($new_user);
expect($user2->pilot_id)->toEqual($user2->id);
expect($user2->pilot_id)->toBeGreaterThan($user->pilot_id);
// Now try to change the original user's pilot_id to 4
$user = $userSvc->changePilotId($user, 4);
expect($user->pilot_id)->toEqual(4);
// Create a new user and the pilot_id should be 5
// Create a new user and the pilot_id should be greater than the manually set 4
$user3 = User::factory()->create();
expect($user3->pilot_id)->toEqual(5);
expect($user3->pilot_id)->toBeGreaterThan(4);
});
test('user pilot deleted', function (): void {
@ -320,7 +320,7 @@ test('user pilot deleted', function (): void {
$new_user = User::factory()->make()->makeVisible(['api_key', 'name', 'email'])->toArray();
$new_user['password'] = Hash::make('secret');
$user = $userSvc->createUser($new_user);
expect($user->pilot_id)->toEqual($user->id);
expect($user->pilot_id)->toBeGreaterThan(0);
// Delete the user
$userSvc->removeUser($user);
@ -343,7 +343,7 @@ test('user pilot deleted with pireps', function (): void {
$new_user = User::factory()->make()->makeVisible(['api_key', 'name', 'email'])->toArray();
$new_user['password'] = Hash::make('secret');
$user = $userSvc->createUser($new_user);
expect($user->pilot_id)->toEqual($user->id);
expect($user->pilot_id)->toBeGreaterThan(0);
Pirep::factory()->create([
'user_id' => $user->id,
@ -449,7 +449,7 @@ test('event called when profile updated', function (): void {
$body = [
'name' => 'Test User',
'email' => $user->email,
'airline_id' => 1,
'airline_id' => $user->airline_id,
];
$resp = $this->actingAs($user)->put('/profile/'.$user->id, $body);