Commit Graph

721 Commits

Author SHA1 Message Date
Nabeel Shahzad
86aa5b8689
more refinement around the uninstall/removal process, added sample in for easier testing 2026-06-12 15:07:38 -05:00
Nabeel Shahzad
39c08ee835
feat(flights): integrate ACARS addon check and update dependencies - minimum PHP version is n ow 8.4. A lot of fixes, verified the boot cache db upsert, etc 2026-06-12 15:04:38 -05:00
Nabeel Shahzad
d0cf54c3de
refactor(addons): remove Module shim, legacy NWIDART bindings, and update controllers/tests
- Removed `Module` and `ModuleRepository` compatibility shims.
- Replaced NWIDART `Module` facade usages with the new `AddonRegistry`.
- Updated frontend controllers to use `AddonRegistry` for module handling.
- Added new feature tests to cover `AddonRegistry` lifecycle and rebuild cases.
- Updated bootstrapping logic and service bindings to drop NWIDART dependencies.

# Conflicts:
#	app/Models/Flight.php
#	app/Support/Utils.php
2026-06-12 15:04:35 -05:00
Nabeel Shahzad
69e0ded644
Addons self-boot proof of concept 2026-06-12 14:58:44 -05:00
Nabeel Shahzad
de88746227
test fixes 2026-06-01 22:41:04 -05:00
Nabeel Shahzad
67c244240a
postgres fixes 2026-06-01 18:38:58 -05:00
Nabeel Shahzad
1b05dfe757
build fix 2026-05-31 15:02:48 -05:00
Nabeel Shahzad
866ecfc15c
Eager load fares relationships across multiple models to improve consistency and reduce redundant queries.
Signed-off-by: Nabeel Shahzad <99736+nabeelio@users.noreply.github.com>
2026-05-30 17:02:07 -05:00
Nabeel Shahzad
7489d9dcda
More optimizations and adjustments 2026-05-27 14:26:29 -05:00
Nabeel Shahzad
bece5ca0d5
Update translations/label mappings 2026-05-26 18:25:05 -05:00
Nabeel Shahzad
3c7f0e3934
feat(routeforge): refine duplicate detection
Bundles three completed OpenSpec changes:

routeforge-duplicate-detection-refinement:
- StrictDuplicateKey value object owns the 5-tuple (bundle_id,
  airline_id, flight_number, route_code, route_leg). L4 / L5 / L12 /
  DuplicateChecker delegate; no more dup'd dupKey() / normalize().
- L5 severity flipped WARNING -> ERROR for same-bundle full-tuple
  matches. Scoped to enabled = true AND owner_type IS NULL AND same
  bundle_id.
- L12 new WARNING for cross-bundle airline+flight collisions.
  Catalog grows 12 -> 13 rules.
- DuplicateChecker (the typing-time /check-duplicates endpoint)
  classifies results as same_bundle / error vs cross_bundle / warning,
  passes existing_bundle_id + existing_bundle_name to the UI.
- New migration adds flights._dup_key generated column + UNIQUE
  index. STORED on MySQL / MariaDB / PostgreSQL; VIRTUAL on SQLite
  (ALTER TABLE quirk). Driver-switched expression.
- Migration first canonicalizes route_code / route_leg storage
  ('' / '0' / 0 -> NULL), then auto-disables pre-existing dup rows
  via a single window-function pass (lowest id per cluster kept),
  with activity-log entries per disabled row.
- Flight model gains routeCode() / routeLeg() Attribute mutators
  for canonical NULL on set. Legacy 'route_leg' => 'integer' cast
  removed.
- README MySQL min bumped 5.7 -> 8.0 (window functions needed
  by the dedup migration).
- Frontend types updated: DuplicateMatch now carries severity,
  kind, existing_bundle_id, existing_bundle_name.

routeforge-page-boot-via-api:
- /boot endpoint + RouteForgeBootResource serving SPA bootstrap
  envelope (CSRF, user, airlines, routes, config, translations).
- Frontend boot.ts state init, BootError component.

routeforge-controller-thin:
- Extract AirlineStatsService, CommitInputFactory,
  LintContextFactory from the controller.
- New BundlesRequest + RouteForgeBundleResource.

Misc cleanup:
- Support\Geo utility extracted from generator.ts logic.
- BaseRouteForgeBatchRequest validation polish.

Verified: 670 pest tests pass (2682 assertions). PHPStan clean on
touched paths. Pint clean (--dirty). OpenSpec validate --strict
green.
2026-05-26 10:19:33 -05:00
Nabeel Shahzad
beb5415c2f
Fixes from PR feedback 2026-05-24 12:34:35 -05:00
Nabeel Shahzad
1f9f5cc32a
test(routeforge): pest + vitest coverage + service fixes
Add 27 PHP test files + 6 TS test files + Vitest setup, surfacing
and fixing 4 production bugs in the process.

Test counts (all green):
- PHP: 545 tests / 2341 assertions / 25.25s
  - 12 lint rule tests (L1, L2, L2b, L3, L4, L5, L6, L7, L8, L9, L10, L11)
  - LintRunner dispatch + LintReport bucket aggregation
  - DuplicateChecker bulk-query + N+1 protection
  - RouteForgeService full commit pipeline (happy path, lint rollback,
    attach-existing branch, fare multiplier, withoutEvents suppression)
  - 6 endpoint feature tests (PreviewAirports, Subfleets, AirlineStats,
    CheckDuplicates, Lint, Commit)
- TS: 46 tests / 6 files / 280ms via Vitest 4.1.7 + happy-dom
  - geo, timezone, flightNumber, timeStrategy, generator, lint

In-scope production fixes discovered by writing the tests:

1. DuplicateChecker eager-load was 'airline:id,code' but 'code' is an
   accessor over iata/icao columns. Loaded airline came back with
   NEITHER backing column populated and Flight::ident dropped the
   airline prefix in the duplicates response. Changed to
   'airline:id,iata,icao'.

2. LintRunner constructor takes 'array $rules' with no container-
   resolvable shape. Both /lint and /commit endpoints would have
   failed at runtime with 'Unresolvable dependency'. Registered as
   singleton in AppServiceProvider::register() binding to
   LintRunner::defaults().

3. BaseRouteForgeBatchRequest's 'subfleet_ids' rule was
   ['required', 'array', 'min:0']. 'required' rejects empty arrays
   in Laravel, contradicting 'min:0'. Relaxed to ['present', 'array'];
   L3 lint catches the empty case as a warning downstream.

4. RouteForgeService::commit() called auth()->user() directly,
   violating tests/Arch/GlobalTest's http-helpers rule (auth/session/
   request only allowed in App\Http / App\Filament / App\Livewire /
   App\Providers\Filament). Added $causerId field to CommitInput;
   controller stamps it from auth()->id(); service resolves User by id.

Infrastructure:
- tests/Support/RouteForgeTestHelpers.php — shared LintContext + row +
  airport + batchPayload builders. PSR-4 autoloaded via Tests\ namespace;
  static class avoids global-function name collisions across the 15+
  RouteForge test files.
- vitest.config.ts at project root, scoped to
  resources/js/admin/routeforge/**/*.test.ts. happy-dom provides the
  minimal window + DOM globals lib/i18n.ts depends on.
- package.json gains 'test': 'vitest run' script.
- Added vitest, happy-dom, @types/node as devDependencies.

Task 4.6.3 stays N/A — FlightNumberAssigner was removed per the
Section 4 banner; no PHP equivalent to test. Section 6.4.7's planned
PHP↔TS shared fixtures (tests/fixtures/routeforge/generator/) also
N/A for the same reason; TS generator tests stand alone.

Quality gates green:
- tsc --noEmit: clean
- oxlint (40 files / 93 rules): 0 warnings, 0 errors
- oxfmt --check: all formatted
- npm run build: routeforge bundle stays at 26.69 KB gzip
  (Decision 16 budget 60 KB → 33.31 KB headroom)
- vendor/bin/pint --dirty: passed
- vendor/bin/phpstan analyse: OK no errors
- vendor/bin/rector --dry-run: clean for RouteForge files
2026-05-24 11:55:36 -05:00
Nabeel Shahzad
2c7f4f5cc9
chore(routeforge): rector NewlineAfterStatement fixes
Two cosmetic blank-line-after-continue fixes flagged by
'vendor/bin/rector --dry-run' on the route-forge change:

- app/Services/RouteForge/Rules/L6OriginEqualsDestination.php
- app/Http/Controllers/Admin/RouteForgeController.php (decorate loop)

Section 9 quality gates green:
- tsc strict: clean
- oxlint (34 files / 93 rules): 0 warnings, 0 errors
- oxfmt: all formatted
- vite build: 113 modules, routeforge entry 26.69 KB gzip
  (Decision 16 budget 60 KB -> 33.31 KB headroom)
- pint --dirty: passed
- phpstan level 5: no errors
- rector dry-run scoped to these 2 files: clean post-fix

Out-of-scope rector findings (19 legacy Laravel-13 attribute migrations
across app/Models/*.php and app/Console/Commands/*.php) shipped on a
separate branch (rector-modernize-legacy).
2026-05-24 11:55:36 -05:00
Nabeel Shahzad
370c2f6fdf
feat(routeforge): admin batch flight composer + lint pipeline
New Filament page at /admin/route-forge: compose dozens of scheduled
flights across five topologies (Hub→Spokes, Spokes→Hub, Hub & Spokes,
Mesh, Chain), preview rows live, lint for operational issues, commit
as one Flight Bundle in a single atomic transaction.

Frontend: TypeScript + Preact 10 + @preact/signals + @date-fns/tz.
Separate Vite entry, 26.69 KB gzip (well under the 60 KB target).
Client-side generator + 12 lint rules; localStorage draft resume.

Backend: RouteForge service + 12 PHP lint rule classes + duplicate
checker + commit orchestration (Flight::withoutEvents + single
bundle-level activity log). Six endpoints under
/admin/route-forge/api/, gated by permission:create:flight.

Bundle picker is dual-mode — type a new name to create, or pick an
existing bundle to append flights to (read-only summary, server skips
the bundle persist step and stamps the existing id onto new flights).

Airport picker: click-to-open dropdown with 50-result alpha pages,
checkbox rows for multi-select, prefix-only search via new
searchMode=prefix opt-in on the shared AirportSearchQueryV1
(/api/airports default substring behavior preserved).

Quality: tsc strict mode clean, oxlint 0/0, phpstan no errors,
pint passing. Pest scaffolding deferred per workflow.

Tracking: openspec/changes/route-forge — 66/106 tasks complete
(sections 1, 2, 4, 5, 6, 7, 8; Pest 4.6/5.3/6.4 + sections 9–12
remaining).

# Conflicts:
#	package-lock.json
#	package.json
2026-05-24 11:55:33 -05:00
Arthur Pariente
dcc25a108f
Merge branch 'main' into refactor-subfleets-filtering 2026-05-23 22:35:50 +02:00
Nabeel Shahzad
28f49b97c3
feat(schema-modernization): foundations for RouteForge
Schema, model, Filament, cron, and visibility-semantics foundations
required by the RouteForge change. Four phases delivered as one
coherent change. Verified against four spec files (flight-bundles,
flight-time-storage, subfleet-capability, flight-visibility).

Phase 1.1 \xe2\x80\x94 Flight time columns
- flights.departure_time, flights.arrival_time TIME NULL
- Flight model accessors (Hi-format) + mutators (FlightTimeParser)
- FlightTimeParser supports 9 formats + Z/L/tz suffix stripping
- Migration backfills inline via chunked raw DB::table->update()
- Legacy dpt_time/arr_time VARCHAR columns preserved for one release

Phase 1.2 \xe2\x80\x94 Subfleet capability columns
- subfleets.cruise_speed, max_range_nm, route_types (CSV VARCHAR(64))
- FlightTypesCast splits/sorts/dedupes/logs-on-invalid
- routeforge defaults in config/phpvms.php
- SubfleetForm Operational Capability section

Phase 1.3 \xe2\x80\x94 Flight Bundles
- flight_bundles table + flights.bundle_id NOT NULL FK
- Default bundle seeded inline; existing flights backfilled
- FlightBundle model + factory + BundleObserver (queued recompute)
- FlightBundleResource as sole Flights nav entry (slug=flights,
  icon=OutlinedMap, sort=2)
- Nested FlightResource under FlightBundleResource (slug=flight,
  parentResource, create+edit only, no nav entry)
- FlightsRelationManager with relatedResource for full-page row actions
- FlightForm: bundle selector removed (route-bound), date inputs
  hidden when parent bundle owns dates, XSS-safe placeholder link
- Date columns stored as TIMESTAMP UTC, UI converts to local tz
- Soft-deleting a bundle leaves children non-deleted; visibility
  recompute sets visible=false (no cascade delete)
- Spatie Shield permissions: flight_bundle.* seeded

Phase 1.4 \xe2\x80\x94 Visibility semantics
- flights.active renamed to flights.enabled (admin source of truth)
- flights.visible is cron-managed combined state
- SetActiveFlights cron deleted; SetVisibleFlights cron added
- Two-pass bulk SQL UPDATE scoped by bundle, chunks of 500
- RecomputeBundleVisibility queued job dispatched on bundle save/restore
- Flight::scopeVisible added; scopeActive retained as plain alias
- Pilot-facing read sites use ->visible() (AirportController fix)
- FlightResource JSON + FlightExporter CSV expose both enabled+active
- Four-state status badge (Disabled / Disabled by Bundle / Enabled
  Out of Window / Enabled & In Window) replaces Visible toggle
- 5 visibility indexes added

Verification
- 469 Pest tests passed (2058 assertions)
- phpstan level 5 clean (646 files)
- pint --test clean
- rector --dry-run clean
- All four spec files PASS (28 requirements + 36 scenarios verified
  by kimi-spec-reviewer)

Migration path documented in docs/UPGRADING.md and openspec/changes/
schema-modernization-for-routeforge/.
2026-05-23 12:05:09 -05:00
Arthur Pariente
8caf34420b
refactor(FlightService, PirepService, UserService): replace UserService usage with direct user methods for subfleet and aircraft access 2026-05-23 10:58:36 +02:00
Arthur Parienté
7490e72523
[8.x] refactor: use native php enum instead of class (#2210)
* wip

* refactor: update enum handling and improve type hints across models

* refactor: deprecate labels and select methods in HasSelect trait for better separation of concerns

* copilot suggestions
2026-05-12 16:29:41 -05:00
Arthur Parienté
7de823a0a7
[8.x] refactor: migrate to the new modern laravel structure (#2207)
* refactor: migrate to the new modern laravel structure

* fix .env.test
2026-05-07 14:58:11 -05:00
Arthur Parienté
992b6d343a
[8.x] feature: upgrade to Laravel 13 (#2204)
* upgrade to laravel 13

* phpstan

* rollback to symfony 7.4

* fix: ModuleService merge

* refactor: new rector rules
2026-05-06 11:27:21 -05:00
Arthur Parienté
7c9b446262
[8.x] Refactor models and tests for slug handling and resource consistency (#2191)
* refactor: Casts, Observers and Traits

* refactor: replace Sluggable observer with HasSlug trait in models

* test: add HasSlug trait tests for slug generation and uniqueness

* refactor: rename resource classes for consistency and update references

* refactor: update return type of index method to use AnonymousResourceCollection

* refactor: simplify slug generation in HasSlug trait and update tests for consistency

* refactor: update namespace annotation in UserBidResource for clarity

* apply coderabbit suggestions

* fix tests
2026-05-03 11:38:30 -05:00
Nabeel S.
849ab9ac7f
chore: remove prettus/l5-repository (Phase 8 cleanup) (#2202)
* chore: remove prettus/l5-repository (Phase 8 cleanup)

Phase 8 of the Prettus repository removal. Phases 1-7 deleted all 25 concrete
repositories and rewired callers; this drops the package itself plus the
remaining scaffolding.

- Migrate Sluggable observer registration from ObserverServiceProviders boot
  to #[ObservedBy(Sluggable::class)] attributes on FlightField, FlightFieldValue,
  Page, PirepField, PirepFieldValue.
- Delete app/Providers/ObserverServiceProviders.php, app/Contracts/Repository.php,
  app/Repositories/ (incl. Criteria/WhereCriteria.php), config/repository.php.
- Drop RepositoryServiceProvider and ObserverServiceProviders from config/app.php.
- Sweep dead 'use Prettus\Validator\Exceptions\ValidatorException' imports from
  10 files plus the 12 orphaned '@throws ValidatorException' docblock lines that
  PHPStan would otherwise flag as throws.notThrowable.
- Move pagination default from deleted config/repository.php to config/phpvms.php
  ('pagination.limit' => 20, matches historical value). Update 8 prod callers +
  2 test sites to read 'phpvms.pagination.limit'.
- Drop App\Contracts\Repository exception from tests/Arch/GlobalTest.php
  http-helpers rule.
- composer remove prettus/l5-repository (drops prettus/laravel-validation
  transitively).

No public API, JSON shape, or behavior change. Pint, PHPStan level 5, Pest, and
Rector --dry-run all pass; the 7 pre-existing CSRF/ProfileUpdated test failures
on main remain unchanged.

* fix(pagination): default 50, max cap 100, central paginate_limit() helper

Address PR #2202 review feedback (CodeRabbit + Copilot):

- config/phpvms.php: pagination.limit = 50 (default page size),
  pagination.max = 100 (hard cap on ?limit= query input).
- Add paginate_limit() helper in app/helpers.php that resolves the raw
  ?limit= value, falls back to pagination.limit, and clamps to
  [1, pagination.max]. Single source of truth for per-page sanitization.
- Replace ad-hoc `$request->query('limit') ?: config(...)` patterns in
  8 controllers with paginate_limit():
  - Api: AirportController, FleetController, FlightController,
    NewsController, UserController (fleet + pireps)
  - Frontend: FlightController, PirepController, UserController
  Frontend/UserController previously used 20 as fallback default; now
  consistent with the rest of the app at 50.
- SearchAirportsRequest validator now bounds ?limit= against
  pagination.max (100) instead of pagination.limit (50), so callers can
  request the full clamp range. Add boundary test asserting limit=100
  passes and limit=101 fails.

Net effect: API endpoints can no longer be coerced into oversized result
sets via ?limit=. Default page size moves from 20 to 50 to match the
review consensus.
2026-05-02 14:39:06 -05:00
Nabeel S.
368034f7ef
Refactor/phase-7-journal (#2200)
* feat(journal): add JournalService, JournalTransactionQuery, model methods

Move balance math onto Journal model as instance methods returning Money:
- recalculateBalance(): self
- getCreditBalanceBetween(Carbon, ?Carbon, ?string): Money
- getDebitBalanceBetween(Carbon, ?Carbon, ?string): Money
Convert $casts to casts() method.

Add JournalService for writes (post, deleteAllForObject) — drops the
dead try/catch around ValidatorException; deleteAllForObject iterates so
JournalTransactionObserver fires (preserves cached balance).

Add JournalTransactionQuery as plain class for ref-model lookups
(replaces JournalRepository::getAllForObject). No FormRequest — no HTTP
endpoint drives it; arguments are object/journal/date.

Modernize observers with #[ObservedBy] on Journal and JournalTransaction;
remove imperative ::observe registrations from ObserverServiceProviders
to prevent double-firing.

Phase 7 setup commit. Callers still use JournalRepository; migration
follows in next commit.

* refactor(journal): migrate callers to JournalService and Journal model

FinanceService, PirepFinanceService, Api/PirepController, and the nightly
RecalculateBalances cron now use JournalService::post/deleteAllForObject,
Journal::recalculateBalance, and JournalTransactionQuery::build directly.

JournalService::post takes Eloquent\Model (not App\Contracts\Model) so
User references keep working without a wrapper. Drops dead ValidatorException
import and stale @throws docblocks now that Prettus is out of the call path.

The Phase 0 deleteAllForObject characterization test moves to
tests/Unit/JournalServiceTest.php with assertions intact.

* feat(repos): remove JournalRepository (Phase 7)

Last journal repository deleted. Reads now go through Journal model
(balance math) and JournalTransactionQuery (transaction lookups);
writes go through JournalService.

Drops the phpstan ignoreErrors entry that masked Repository magic-method
calls — no real repos remain in app/Repositories/, only Criteria/.

* fix(journal,finance): address latent bugs found in Phase 7 review

PirepService::delete now drops the pirep's journal entries before the
forceDelete. The polymorphic ref_model_id has no FK constraint, so
without explicit cleanup those rows would dangle — still summed by
nightly recalculate, skewing journal balances. Inject PirepFinanceService
to call deleteFinancesForPirep first.

JournalService::post now uses $journal->currency (with the system
setting as fallback) for the transaction.currency column. The legacy
behavior unconditionally stamped the system setting regardless of the
target journal's currency, so a non-default-currency journal could
silently log transactions in the wrong currency.

JournalTransaction::casts() had a typo: 'credits' (plural, no such
column) instead of 'credit'. Result: $tx->credit came back uncast
(string from MySQL bigint). Mostly masked by PHP coercion and
SUM() returning numeric, but a real risk for any code doing direct
arithmetic on $tx->credit.

JournalTransactionObserver renamed saved → created so that updates to
a JournalTransaction no longer re-add its credit/debit to the cached
journal balance. saved fires on both insert and update; created fires
only on insert.

PirepFinanceService::processFinancesForPirep wraps the delete + 9 pay*
calls in DB::transaction. A partial failure mid-pay would otherwise
leave the journal half-cleared with new entries on top.

JournalTransactionQuery::build replaces the broken
where('post_date', '=', Y-m-d) with whereDate. The legacy '=' compare
against a datetime column would silently match nothing for any row
not posted at midnight UTC. No production caller passes $date today,
so this is a dead-code fix — but the code is now correct.

Adds clarifying docblock notes on Journal::recalculateBalance (sums
unfiltered by post_date — disagrees with getCurrentBalance when
future-dated rows exist) and Journal::getCreditBalanceBetween
(whereDate day-precision vs getCreditBalanceOn datetime-precision).

* fix(journal,finance): address PR review feedback

CRITICAL
- JournalTransaction tags cast was 'array' but post() stores as
  CSV string. JSON decode of CSV returned null on read. Switch
  cast to 'string' to match storage format.

MAJOR
- PirepController finances_get / finances_recalculate now use
  findOrFail so a missing id yields 404 instead of a 500 type
  error from JournalTransactionQuery::build's non-nullable param.
- PirepService::delete is now wrapped in DB::transaction so
  partial-delete failures roll back. Same pattern as
  processFinancesForPirep.
- FinanceTest 'pirep expenses nightly' assertion was tautological
  (toHaveCount on the 3-key array shape always passes). The test
  also lacked initJournal calls and queried by ref_model=Airline
  even though processExpenses posts with ref_model=Expense. Fixed
  by calling initJournal and counting transactions on each
  airline's journal directly.

MINOR
- JournalTransactionQuery copy()'s the Carbon argument before
  setTimezone so callers don't see their instance mutated.
- Reverted the whereDate change for post_date (column is DATE,
  not DATETIME, so the legacy = compare was correct and uses the
  index; whereDate disables it).
- Switched JournalTransactionQuery to getKey() over ->id so the
  read path matches JournalService::deleteAllForObject.

NITPICKS
- RecalculateBalances cron uses chunkById(500) instead of all().
- JournalService::deleteAllForObject uses lazyById for memory
  safety on large cleanups.
- JournalService gets declare(strict_types=1).
- processFinancesForPirep declares its Pirep return type.
2026-04-29 14:13:45 -05:00
Nabeel S.
62d63a3f64
Refactor/phase 6 flight airline (#2197)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Switched many backend lookups to use centralized model-driven queries;
flight searching rebuilt with a new query flow for richer filtering,
multi-column sorting, and optional pagination.
* **Bug Fixes**
* Missing-record lookups now consistently return not-found responses;
diversion handling reliably reuses or creates reposition flights;
airline/flight selection lists and visible flight-type filtering
improved.
* **Tests**
* Added extensive unit and feature tests covering search, bids,
finances, ACARS, diversion, and related behaviors.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-29 09:56:19 -05:00
Nabeel Shahzad
bfe75ee870 chore(pirep): drop dead 'like' branch + stale @throws docblock
PirepSearchQuery::applySearch carried a defensive 'like' branch
suppressed via @phpstan-ignore. FIELD_SEARCH only contains '='
operators, so the branch was unreachable. Reshape FIELD_SEARCH to
list<string> + drop the branch. YAGNI: 1-line revert if a future
field needs LIKE.

Frontend/PirepController::index no longer throws RepositoryException
(removed with prettus). Drop the @throws docblock; pint nukes the
now-unused import.
2026-04-27 17:04:45 -05:00
Nabeel Shahzad
5ae3026bb1 fix(pirep): wire status filter into Api/UserController::pireps
SearchPirepsRequest validates ?status= as a documented filter, but
the controller only branched on ?state=. Clients passing ?status=
got their filter silently ignored. Wire it through.
2026-04-27 17:04:06 -05:00
Nabeel Shahzad
80be8da6c3 refactor(pirep): migrate Dashboard, Api/Pirep, Api/Acars controllers to Eloquent 2026-04-27 16:53:16 -05:00
Nabeel Shahzad
e20844a999 refactor(pirep): migrate Frontend/PirepController to direct Eloquent + PirepSearchQuery 2026-04-27 16:51:12 -05:00
Nabeel Shahzad
e5b917bd9b refactor(pirep): migrate Api/UserController::pireps to PirepSearchQuery 2026-04-27 16:48:51 -05:00
Nabeel S.
d601b1f9e2
Phase 5 of prettus repository removal. (#2195)
* test(setting): characterization tests for setting() helper + API shape

* refactor(setting,expense): add #[Scope] + #[ObservedBy] + casts() + drop dead $rules

* feat(services): add SettingService with cache-aware store/retrieve

* feat(finance): add FinanceService::getExpensesForType

* refactor(setting): migrate setting()/setting_save() helpers to SettingService

* refactor(setting): migrate Api controller, Filament page, importer, test helper to SettingService

* refactor(expense): migrate PirepFinanceService, ExportAction, FinanceTest to FinanceService::getExpensesForType

* refactor(setting,expense): delete SettingRepository and ExpenseRepository

* test(setting): use Setting::byKey scope in characterization test

* refactor(setting): drop redundant Cache::forget in Maintenance page

SettingService::store() now invalidates the per-key cache slot internally
(introduced in d9c94195). Manual Cache::forget calls after setting_save()
became dead code in this branch, mirroring the same cleanup applied to
Filament/Pages/Settings.php in 31aa55da. Drops the now-unused Cache import
as well — caught by deep review.

* fix(setting): increment count in SettingsImporter run

Pre-existing bug. $count was declared but never incremented, so the
import log always read 'Imported 0 settings' regardless of how many
settings the legacy importer actually wrote.

Found during Phase 5 deep review.

* refactor(filament): wrap settings save in DB::transaction

The Filament Settings page writes N setting rows in a foreach loop,
then calls FinanceService::changeJournalCurrencies() which rewrites
journal + journal_transaction rows to a (potentially) new currency.
A throw mid-loop or mid-currency-migration left the DB with half-saved
settings or a half-migrated journal.

Wrap both halves in a single DB::transaction() so a partial failure
rolls back all writes atomically.

Found during Phase 5 deep review.

* refactor(services): declare strict_types=1 across app/Services/

Phase 5 introduced declare(strict_types=1); in SettingService.php only.
This commit applies the same declaration to the remaining 65 service
files for consistency.

65 files migrated. 6 files required minimal signature/cast adjustments
to handle existing caller patterns where strict mode exposed implicit
type coercion at runtime or in phpstan:

- FareService::recalculateFares cast $pivot->capacity to (float)
  before floor() (DB returns string).
- Finance/RecurringFinanceService::processExpenses cast
  $expense->ref_model to (string) before explode() (nullable column).
- AirportLookup/VaCentralLookup, GeoService: pass $e->getMessage()
  to Log::error() instead of the Exception object.
- DatabaseService::time() cast Carbon to (string) (return type contract).
- ModuleService::installModule pass $file->getRealPath() to PharData
  and Madzipper::make() (UploadedFile -> string path).

No files reverted. No tests modified.

Found during Phase 5 deep review (convention drift).

* style(setting): align match arms for pint 1.29.1

Pint 1.29.1 aligns => arrows in match expressions under
binary_operator_spaces (with align_single_space_minimal). 1.29.0 did
not. CI installs latest globally and fails on the un-aligned form.

Bumped composer.lock so local matches CI.

* Update app/Services/Finance/RecurringFinanceService.php

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* refactor(installer): add strict types to LoggerTrait

LoggerTrait declared strict_types=1 but methods had no parameter or
return types. Added: comment(string $text): void, info(string $text):
void, error(string $text): void.

FlightImporter:59 was passing a Throwable to error() — switched to
$e->getMessage() to match the new contract. Same pattern applied in
5b2133a8 to VaCentralLookup and GeoService.

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-04-27 15:21:27 -05:00
Nabeel Shahzad
ac8e9e02c8
refactor(user): address PR #2194 review feedback
Fix issues raised by code review on Phase 4:

- UserController::index now honors validated `?limit` query param via
  paginate($request->integer('limit') ?: config('repository.pagination.limit', 15)),
  matching the convention used in NewsController, FleetController, AirportController.
- UserSearchQuery::applySearch joins multi-pair field-specific search with OR
  (matching the legacy Prettus RequestCriteria default and the documented behavior
  in SearchUsersRequest's PHPDoc), and falls back to free-text when a colon-prefixed
  payload contains no allowlisted fields (e.g. "8:30") instead of silently returning
  all users.
- Role::byName docblock corrected: case-sensitivity follows the database collation
  (case-sensitive on SQLite, case-insensitive on MySQL utf8mb4_unicode_ci) rather
  than being a guarantee of the scope itself.
- UserService::getUserFields PHPDoc rewritten to describe the actual three-valued
  contract: true=public-only, false=private-only, null=all visibility-allowed.
- Trivial scope tests removed: deleted RoleScopesTest entirely (both cases were
  smoke tests of where() with no real signal, one was even mis-named) and trimmed
  UserScopesTest to keep only the composition test that proves #[Scope] chaining
  works. The single-where scope tests are tested implicitly by feature tests.
- New UserSearchQueryTest cases lock the OR-join and free-text-fallback behaviors.
2026-04-26 16:42:29 -05:00
Nabeel Shahzad
606c484c6e
fix(user): preserve findOrFail contract in FlightController::bids
Code review caught: User::find() returns null on missing user, then
foreach ($user->bids) NPEs to 500. Old Prettus userRepo->find() called
findOrFail under the hood, throwing ModelNotFoundException for clean 404.

Restore old contract via User::findOrFail().
2026-04-26 16:23:24 -05:00
Nabeel Shahzad
315ce8d58d
refactor(user): migrate Api/User, FlightController, LatestPilots widget, Filament to direct Eloquent 2026-04-26 16:14:41 -05:00
Nabeel Shahzad
3961a67b97
refactor(user): absorb getUserFields into UserService + migrate ProfileController 2026-04-26 16:05:04 -05:00
Nabeel Shahzad
4a0e2a7b5a
refactor(user): migrate Frontend/UserController to UserSearchQuery 2026-04-26 15:51:14 -05:00
Nabeel Shahzad
2d59d97459
fix(airport): validate limits and preload options 2026-04-25 21:21:43 -05:00
Nabeel Shahzad
ef94e03f30
fix(airport): restore legacy search behavior and harden test bootstrap 2026-04-25 14:54:04 -05:00
Nabeel Shahzad
235cdb13f3 refactor(airport): delete AirportRepository
- Frontend/AirportController: drop airportRepo, use
  Airport::with('files')->find($id) ($id was already uppercased
  earlier in the method)
- Frontend/PirepController: drop airportRepo from constructor
  (only reference was a commented-out selectBoxList call). Phase
  2 precedent (Aircraft was the same case).
- Filament/Actions/ExportAction: AIRPORT case uses Airport::all()
- Delete app/Repositories/AirportRepository.php (dead selectBoxList
  goes with it)
- phpstan.neon: suppress larastan.relationExistence for
  FilesTrait::files() across Aircraft/Airline/Airport/Subfleet.
  The morphMany is trait-defined and works at runtime, but
  larastan can't statically resolve trait-declared polymorphic
  relations. First direct Eloquent with('files') call lands in
  this commit, hence the new suppression.

Final commit of Phase 3. AirportRepository is gone; 9 repositories
remain (was 10 at branch start). First Query class
(AirportSearchQuery) and search Form Request (SearchAirportsRequest)
are now in production, ready to be reused by Phases 4/6/7.
2026-04-25 12:57:38 -05:00
Nabeel Shahzad
22c39e8766 refactor(airport): migrate Api/AirportController to AirportSearchQuery
- index() and search() use AirportSearchQuery + SearchAirportsRequest
- index_hubs() uses Airport::byHub()->orderByIcao()
- get(Airport $airport) uses route model binding (case-insensitive
  via Airport::resolveRouteBinding)
- routes/api.php: rename airports/{id} -> airports/{airport} so
  Laravel binds to the typed parameter (URL format unchanged)
- Drop AirportRepository injection and Prettus criteria use
- Match Phase 2's perPage() pattern for ?limit= handling
- Add regression test asserting lowercase ICAO routes resolve

Public API contract preserved: ?search=field:value syntax, ?hub=,
?hubs=, ?limit=, ?orderBy=, ?sortedBy= all continue to work.
Drops un-tested Prettus magic params (?with, ?withCount, ?filter,
?searchFields, ?searchJoin) — narrows the public contract to what's
actually exercised.
2026-04-25 12:33:43 -05:00
Nabeel Shahzad
64a615789f
refactor: Phase 2 — Fleet domain (Aircraft, Subfleet, Navdata)
Phase 2 of the prettus/l5-repository removal. Deletes 3 Fleet-domain
repositories and migrates their callers to direct Eloquent. Introduces
the first #[ObservedBy] attribute migrations on Aircraft and Subfleet.

## Repositories deleted

- NavdataRepository (2 callers, no unique methods)
- AircraftRepository (6 callers, dead selectBoxList() also dropped)
- SubfleetRepository (5 callers, selectBoxList() ported as private
  helper on FlightController — its only caller)

## Caller migrations

- Filament/Actions/ExportAction: inline Aircraft::orderBy()->get(),
  Subfleet::all() (2 cases)
- Api/FleetController: empty constructor; Subfleet::with()->paginate(),
  Aircraft::with()->where()->first(); explicit ?limit= handling
- Api/UserController: Aircraft::find() (findWithoutFail → find);
  passes $perPage explicitly to UserService::getAllowableSubfleets
- Api/NewsController: explicit ?limit= handling restored (was lost in
  Phase 1 when migrating from the Repository contract's paginate()
  override at app/Contracts/Repository.php:112-129)
- Frontend/PirepController: constructor surgery + findOrFail/find swaps
- Frontend/FlightController: constructor surgery; ported subfleetSelectBoxList()
  with null-safe airline access
- Services/PirepService: constructor surgery + Aircraft::find / where
- Services/UserService: constructor surgery; Subfleet::when()->with();
  through() instead of transform() to preserve LengthAwarePaginator
  wrapper when paginating; $perPage parameter to keep request() out
  of services (Tests\Arch\GlobalTest forbids it)
- Services/AirlineService::canDeleteAirline: partial rewire (subfleet
  uses Subfleet::where()->exists(); pirep/flight repos remain for
  their phases)
- Services/FlightService: Navdata::whereIn('id', $route_points)->get()
- Services/GeoService: Navdata::where('id', $route_point)->get();
  dropped dead ModelNotFoundException catch (where()->get() never
  throws that)

## Model modernizations

- Aircraft: drop dead public static array $rules; attach
  #[ObservedBy(AircraftObserver::class)]
- Subfleet: drop dead $rules; convert public $casts property to
  protected casts() method (Laravel 11+ form); attach
  #[ObservedBy(SubfleetObserver::class)]
- ObserverServiceProviders: remove the imperative Aircraft::observe()
  and Subfleet::observe() lines + their now-unused imports. Other
  observer registrations (Airport, Flight, Journal, Setting, User,
  Sluggable) stay until their domain phases.

## Pagination contract preserved

paginate($limit)->appends($request->except(['page', 'user'])) on
all three migrated paginated endpoints (/api/fleet, /api/news,
/api/user/fleet). Mirrors the deleted Repository contract exactly,
including the 'user' exclusion (the test framework's auth middleware
leaks the full User model into request()->query() — without exclusion,
next_page URLs would carry the serialized User as query params).

## New tests

- Tests\Unit\AirlineTest: 'cannot delete airline with subfleet' and
  'can delete airline with no associations' — the rewired branch had
  no direct coverage and the all-clear happy path was untested.
- Tests\Feature\ApiTest: 3 new pagination contract tests for /api/fleet,
  /api/news, and /api/user/fleet — assert meta.per_page matches the
  requested limit AND meta.next_page (the codebase's
  CustomPaginatedResourceResponse moves it there from links.next)
  contains the original ?limit= forward.

## Known trade-off

GeoService::parseRoute previously enjoyed a 5-minute Prettus result
cache via findWhere; the new direct Navdata::where()->get() is
uncached. Acceptable: PK lookup on a small indexed table.

## Verification

- 240 Pest tests pass (1630 assertions); 0 failures
- PHPStan level 5: no errors
- Pint: pass
- 13 → 10 repositories
- 0 remaining references to AircraftRepository, SubfleetRepository,
  or NavdataRepository (one docblock breadcrumb in FlightController
  is intentional documentation)

## Followups

Tracked locally in docs/superpowers/followups.md:
- F2.1: Form Request validation pass on legacy controllers (raised
  by review on PirepController::fares — pre-existing semantic match
  with Prettus's find() throw-on-miss)
- F2.2: perf(filament/export): eager-load relations in CSV exporters
  to eliminate N+1 (raised by review on ExportAction — pre-existing
  in deleted repo code)
2026-04-25 10:21:57 -05:00
Nabeel Shahzad
e726fed381
chore: address PR review feedback from CodeRabbit + Copilot
Applies in-scope cleanups flagged during code review on PR #2190:

- helpers.php: fix stale "KVP repository" docblocks (→ "KVP service");
  rename local $kvpRepo → $kvpService; add ": void" return type and
  correct docblock on kvp_save() (it persists, not reads).
- Api/NewsController::index(): add explicit
  ": AnonymousResourceCollection" return type to replace stale
  "@return mixed" (the method body was rewritten in this PR).
- LegacyImporterService::$kvpRepo: type as KvpService instead of
  mixed — the @var docblock was updated in the rename commit but the
  property type was not.
- NewsService::deleteNews(): switch from News::findOrFail(\$id)->delete()
  to News::whereKey(\$id)->delete() so it stays idempotent, matching
  the null-return style of updateNews(). Matches old repo-layer
  semantics (silent no-op on missing id).
2026-04-24 21:16:33 -05:00
Nabeel Shahzad
625986a0c1
refactor(news): replace NewsRepository with Eloquent direct
- NewsService: direct News::create/find/fill+save/delete calls, no more
  repo dependency
- Api/NewsController: News::with/latest/paginate instead of repo chain
- LatestNews widget: same, using Eloquent latest() and paginate()
- Remove dead public static array $rules from News model (unused by
  anything — was a Prettus validator hook that was never activated)

The Phase 0 NewsListShapeTest locks the JSON contract, so API
consumers see no behavioral difference.

Part of Phase 1b of the Prettus repository removal.
2026-04-24 19:57:07 -05:00
Nabeel Shahzad
15357aa384
refactor(pages): replace PageRepository with bySlug scope + Eloquent direct
Added a #[Scope] bySlug($slug) method to Page model for the common
slug-lookup pattern. PageController and PageLinksComposer updated to
use the scope and direct Eloquent respectively.

Uses Laravel 11+ #[Scope] attribute syntax as per spec.

Part of Phase 1b of the Prettus repository removal.
2026-04-24 19:49:31 -05:00
Nabeel Shahzad
b1b36a8a9d
refactor(pirep-fields): replace PirepFieldRepository with Eloquent direct
Three callers all used plain Eloquent methods (all, whereIn) via the
repo layer. Replaced with direct PirepField::... calls. The repo
had no custom methods.

- PirepController (Frontend): 3 whereIn() call sites
- CreatePirepRequest, UpdatePirepRequest: all() call

Part of Phase 1b of the Prettus repository removal.
2026-04-24 19:22:58 -05:00
Nabeel Shahzad
f3bceae914
fix(news): eager-load user relation in /api/news to avoid N+1
The /api/news index endpoint paginates News records and serializes them
through NewsResource, which dereferences $this->user. Without the
eager load, multi-item responses would lazy-load on each iteration
(now fatal because preventLazyLoading is enabled).

Bumped the response-shape test to count(3) so the eager load is
genuinely exercised; the previous count(1) masked the N+1.
2026-04-24 17:50:13 -05:00
Nabeel Shahzad
f53b113e70
chore: enable Model::preventLazyLoading and fix existing N+1 violations
Enable Laravel's preventLazyLoading in dev/test environments to catch
N+1 query regressions during the upcoming repository refactor phases.

Fixes 3 distinct N+1 patterns revealed by the check (7 failing tests):

- Expense::ref_model: eager-load in ExpenseRepository::getAllForType
  and RecurringFinanceService::processExpenses (also in ImporterTest).
- Flight::airline: eager-load in SetActiveFlights::checkFlights where
  the Flight::ident accessor triggers lazy load via Log::info.
- Pirep::aircraft: add 'aircraft' to the eager loads in
  Api/UserController::pireps so the Pirep Resource doesn't lazy load
  aircraft during response serialization.

Part of Phase 0 for the Prettus repository removal.
2026-04-24 17:50:13 -05:00
Arthur Pariente
e9df3e386b fix(ProfileController): make simbrief_username optional in validation rules and fix password hash 2026-04-24 16:54:54 -05:00
Arthur Pariente
383d435b1f fix(ProfileController): update home_airport_id validation to be nullable 2026-04-24 16:54:54 -05:00
Arthur Pariente
e5c255bd4a fix(ProfileController): improve validation and refactor user data handling 2026-04-24 16:54:54 -05:00