fix(static-analysis): clear phpstan-baseline by fixing all suppressed errors (#183)

## 🚪 Why?

### Problem

PHPStan was running with a baseline of 56 suppressed errors, meaning
static analysis was not enforcing type safety across a significant
portion of the codebase. These errors were real type mismatches,
redundant null-safety operators, and incorrect PHPDoc annotations that
could mask bugs and make the code harder to reason about.

## 🔑 What?

### Changes

- Add `@property` PHPDoc annotations to `Account`, `BankingConnection`,
`ExchangeRate`, and `Transaction` models so Enum casts and typed columns
are visible to PHPStan
- Add `instanceof User` guards in `ScheduleDripEmailsListener`,
`SyncUserToResendListener`, and `FortifyServiceProvider` to properly
narrow `Authenticatable` to `App\Models\User`
- Remove redundant `?? false` and unnecessary nullsafe `?->value` in
`HandleInertiaRequests`
- Fix `SyncBankingConnectionJob`: use `->name` instead of `?->name` on
an always-loaded `bank` relation
- Remove `is_countable()` guard in `BalanceLookup` (parameter is always
`Collection|array`, both countable)
- Remove `?? []` / `?? default` fallbacks on fully-typed array keys
across `BalanceSyncService`, `BinanceBalanceSyncService`,
`BinanceClient`, `BitpandaBalanceSyncService`, `BitpandaClient`,
`IndexaCapitalClient`, `IndexaCapitalBalanceSyncService`, and
`AuthorizationController`
- Fix `BinanceClient::publicClient()` `retry()` call: use `when:` named
argument and `\Throwable` type hint to match `PendingRequest::retry()`
signature
- Update `IndexaCapitalClient::getPerformance()` `@return` to include
`portfolios` and `net_amounts` keys; simplify sync service to remove
dead null checks
- Replace nullsafe chain with ternary in `BudgetPeriodService`
- Replace `match` statement in `SetupMainUser` with `if/else` to
eliminate always-true comparison
- Clear `phpstan-baseline.neon` entirely (was 56 suppressed errors, now
0)

##  Verification

### Tests

- Existing tests pass: PHPStan level 5 reports 0 errors with empty
baseline
This commit is contained in:
Víctor Falcón 2026-03-02 12:22:30 +00:00 committed by GitHub
parent 2a1286e98a
commit 3e087bdcd7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 460 additions and 46 deletions

View File

@ -127,6 +127,22 @@ jobs:
DB_USERNAME: root
DB_PASSWORD: password
static-analysis:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
- name: Install PHP Dependencies
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
- name: Run PHPStan
run: vendor/bin/phpstan analyse --memory-limit=512M --no-progress
linter:
runs-on: ubuntu-latest
steps:
@ -209,7 +225,7 @@ jobs:
build-image:
runs-on: ubuntu-latest
needs: [tests, linter, performance-tests]
needs: [tests, linter, static-analysis, performance-tests]
if: (github.ref == 'refs/heads/main' && github.event_name == 'push') || github.event_name == 'workflow_dispatch'
permissions:
contents: read
@ -253,7 +269,7 @@ jobs:
deploy:
runs-on: ubuntu-latest
needs: [tests, linter, performance-tests]
needs: [tests, linter, static-analysis, performance-tests]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- name: Trigger deployment

View File

@ -106,7 +106,7 @@ class ResetDemoAccountCommand extends Command
$user->automationRules()->forceDelete();
$user->categories()->forceDelete();
$user->budgets()->forceDelete();
$user->encryptedMessage()?->delete();
$user->encryptedMessage()->delete();
$this->info(' Deleted existing data');
}

View File

@ -80,11 +80,11 @@ class SetupMainUser extends Command
continue;
}
match ($fileName) {
'categories.json' => $this->importCategories($user, $data),
'automated_rules.json' => $this->importAutomationRules($user, $data),
default => $this->warn(" ! Skipping {$fileName} (no importer defined)"),
};
if ($fileName === 'categories.json') {
$this->importCategories($user, $data);
} else {
$this->importAutomationRules($user, $data);
}
}
foreach ($jsonFiles as $filePath) {

View File

@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api;
use App\Enums\CategoryType;
use App\Http\Controllers\Controller;
use App\Models\Category;
use App\Models\Transaction;
use App\Services\PeriodComparator;
use Carbon\Carbon;
@ -204,13 +205,13 @@ class CashflowAnalyticsController extends Controller
if ($uncategorized != 0) {
$categorized->push([
'category_id' => null,
'category' => (object) [
'category' => (new Category)->forceFill([
'id' => null,
'name' => $type === CategoryType::Income ? 'Unknown Income' : 'Unknown Expense',
'type' => $type,
'color' => 'gray',
'icon' => 'HelpCircle',
],
]),
'amount' => abs($uncategorized),
]);
}

View File

@ -103,7 +103,7 @@ class AuthorizationController extends Controller
'session_id' => $sessionData['session_id'],
'status' => BankingConnectionStatus::AwaitingMapping,
'valid_until' => $sessionData['access']['valid_until'] ?? null,
'pending_accounts_data' => $sessionData['accounts'] ?? [],
'pending_accounts_data' => $sessionData['accounts'],
]);
return redirect()->route('open-banking.map-accounts', $connection);

View File

@ -40,7 +40,7 @@ class HandleInertiaRequests extends Middleware
[$message, $author] = str(Inspiring::quotes()->random())->explode('-');
$user = $request->user();
$isDemoAccount = $user?->isDemoAccount() && ! app()->environment('local') ?? false;
$isDemoAccount = $user?->isDemoAccount() && ! app()->environment('local');
$isDemoQuery = $request->query('demo') === '1';
// Cache encryption checks to avoid duplicate queries
@ -83,7 +83,7 @@ class HandleInertiaRequests extends Middleware
'bestValuePlan' => config('subscriptions.best_value_plan', null),
'promo' => config('subscriptions.promo', []),
],
'chartColorScheme' => $user?->setting?->chart_color_scheme?->value ?? 'colorful',
'chartColorScheme' => $user?->setting?->chart_color_scheme->value ?? 'colorful',
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
'features' => [
'cashflow' => true,

View File

@ -141,7 +141,7 @@ class SyncBankingConnectionJob implements ShouldBeUnique, ShouldQueue
{
$dateFrom = $isFirstSync
? now()->subYear()->toDateString()
: $connection->last_synced_at->toDateString();
: ($connection->last_synced_at?->toDateString() ?? now()->subMonth()->toDateString());
$dateTo = now()->toDateString();
$strategy = $isFirstSync ? 'longest' : null;
@ -171,7 +171,7 @@ class SyncBankingConnectionJob implements ShouldBeUnique, ShouldQueue
}
if ($created > 0) {
$bankName = $account->bank?->name ?? __('Unknown Bank');
$bankName = $account->bank->name ?? __('Unknown Bank');
$transactionsPerBank[$bankName] = ($transactionsPerBank[$bankName] ?? 0) + $created;
}
}

View File

@ -7,6 +7,7 @@ use App\Jobs\Drip\SendImportHelpEmailJob;
use App\Jobs\Drip\SendOnboardingReminderEmailJob;
use App\Jobs\Drip\SendPromoCodeEmailJob;
use App\Jobs\Drip\SendWelcomeEmailJob;
use App\Models\User;
use Illuminate\Auth\Events\Registered;
class ScheduleDripEmailsListener
@ -19,6 +20,10 @@ class ScheduleDripEmailsListener
$user = $event->user;
if (! $user instanceof User) {
return;
}
SendWelcomeEmailJob::dispatch($user)->delay(now()->addMinutes(30));
SendOnboardingReminderEmailJob::dispatch($user)->delay(now()->addDay());
SendPromoCodeEmailJob::dispatch($user)->delay(now()->addDay());

View File

@ -2,6 +2,7 @@
namespace App\Listeners;
use App\Models\User;
use App\Services\ResendService;
use Illuminate\Auth\Events\Verified;
use Illuminate\Contracts\Queue\ShouldQueue;
@ -19,6 +20,12 @@ class SyncUserToResendListener implements ShouldQueue
return;
}
$this->resendService->createContact($event->user);
$user = $event->user;
if (! $user instanceof User) {
return;
}
$this->resendService->createContact($user);
}
}

View File

@ -10,6 +10,9 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* @property \App\Enums\AccountType $type
*/
class Account extends Model
{
/** @use HasFactory<\Database\Factories\AccountFactory> */
@ -37,26 +40,31 @@ class Account extends Model
];
}
/** @return BelongsTo<User, $this> */
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/** @return BelongsTo<Bank, $this> */
public function bank(): BelongsTo
{
return $this->belongsTo(Bank::class);
}
/** @return HasMany<Transaction, $this> */
public function transactions(): HasMany
{
return $this->hasMany(Transaction::class);
}
/** @return HasMany<AccountBalance, $this> */
public function balances(): HasMany
{
return $this->hasMany(AccountBalance::class);
}
/** @return BelongsTo<BankingConnection, $this> */
public function bankingConnection(): BelongsTo
{
return $this->belongsTo(BankingConnection::class);

View File

@ -7,6 +7,9 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property \Carbon\Carbon $balance_date
*/
class AccountBalance extends Model
{
/** @use HasFactory<\Database\Factories\AccountBalanceFactory> */
@ -28,6 +31,7 @@ class AccountBalance extends Model
];
}
/** @return BelongsTo<Account, $this> */
public function account(): BelongsTo
{
return $this->belongsTo(Account::class);

View File

@ -32,11 +32,13 @@ class AutomationRule extends Model
];
}
/** @return BelongsTo<User, $this> */
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/** @return BelongsTo<Category, $this> */
public function category(): BelongsTo
{
return $this->belongsTo(Category::class, 'action_category_id');

View File

@ -20,11 +20,13 @@ class Bank extends Model
'user_id',
];
/** @return BelongsTo<User, $this> */
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/** @return HasMany<Account, $this> */
public function accounts(): HasMany
{
return $this->hasMany(Account::class);

View File

@ -10,6 +10,13 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* @property bool $has_pending_accounts
* @property \App\Enums\BankingConnectionStatus $status
* @property \Carbon\Carbon|null $valid_until
* @property \Carbon\Carbon|null $last_synced_at
* @property array<int, mixed>|null $pending_accounts_data
*/
class BankingConnection extends Model
{
/** @use HasFactory<\Database\Factories\BankingConnectionFactory> */
@ -52,11 +59,13 @@ class BankingConnection extends Model
];
}
/** @return BelongsTo<User, $this> */
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/** @return HasMany<Account, $this> */
public function accounts(): HasMany
{
return $this->hasMany(Account::class);

View File

@ -11,6 +11,10 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* @property \App\Enums\RolloverType $rollover_type
* @property \App\Enums\BudgetPeriodType $period_type
*/
class Budget extends Model
{
use HasFactory, HasUuids, SoftDeletes;
@ -36,21 +40,25 @@ class Budget extends Model
];
}
/** @return BelongsTo<User, $this> */
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/** @return BelongsTo<Category, $this> */
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
/** @return BelongsTo<Label, $this> */
public function label(): BelongsTo
{
return $this->belongsTo(Label::class);
}
/** @return HasMany<BudgetPeriod, $this> */
public function periods(): HasMany
{
return $this->hasMany(BudgetPeriod::class);

View File

@ -8,6 +8,10 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* @property \Carbon\Carbon $start_date
* @property \Carbon\Carbon $end_date
*/
class BudgetPeriod extends Model
{
use HasFactory, HasUuids;
@ -32,11 +36,13 @@ class BudgetPeriod extends Model
];
}
/** @return BelongsTo<Budget, $this> */
public function budget(): BelongsTo
{
return $this->belongsTo(Budget::class);
}
/** @return HasMany<BudgetTransaction, $this> */
public function budgetTransactions(): HasMany
{
return $this->hasMany(BudgetTransaction::class);

View File

@ -24,11 +24,13 @@ class BudgetTransaction extends Model
];
}
/** @return BelongsTo<Transaction, $this> */
public function transaction(): BelongsTo
{
return $this->belongsTo(Transaction::class);
}
/** @return BelongsTo<BudgetPeriod, $this> */
public function budgetPeriod(): BelongsTo
{
return $this->belongsTo(BudgetPeriod::class);

View File

@ -30,11 +30,13 @@ class Category extends Model
];
}
/** @return BelongsTo<User, $this> */
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/** @return HasMany<Transaction, $this> */
public function transactions(): HasMany
{
return $this->hasMany(Transaction::class);

View File

@ -16,6 +16,7 @@ class EncryptedMessage extends Model
'iv',
];
/** @return BelongsTo<User, $this> */
public function user(): BelongsTo
{
return $this->belongsTo(User::class);

View File

@ -5,6 +5,9 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property array<string, float> $rates
*/
class ExchangeRate extends Model
{
/** @use HasFactory<\Database\Factories\ExchangeRateFactory> */

View File

@ -20,6 +20,7 @@ class Label extends Model
'user_id',
];
/** @return BelongsTo<User, $this> */
public function user(): BelongsTo
{
return $this->belongsTo(User::class);

View File

@ -15,6 +15,10 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* @property \Carbon\Carbon $transaction_date
* @property int|float $total_amount
*/
class Transaction extends Model
{
/** @use HasFactory<\Database\Factories\TransactionFactory> */
@ -54,16 +58,19 @@ class Transaction extends Model
];
}
/** @return BelongsTo<User, $this> */
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/** @return BelongsTo<Account, $this> */
public function account(): BelongsTo
{
return $this->belongsTo(Account::class);
}
/** @return BelongsTo<Category, $this> */
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
@ -76,6 +83,7 @@ class Transaction extends Model
->withTimestamps();
}
/** @return HasMany<BudgetTransaction, $this> */
public function budgetTransactions(): HasMany
{
return $this->hasMany(BudgetTransaction::class);

View File

@ -69,31 +69,37 @@ class User extends Authenticatable implements HasLocalePreference, MustVerifyEma
return $this->onboarded_at !== null;
}
/** @return HasOne<UserSetting, $this> */
public function setting(): HasOne
{
return $this->hasOne(UserSetting::class);
}
/** @return HasOne<EncryptedMessage, $this> */
public function encryptedMessage(): HasOne
{
return $this->hasOne(EncryptedMessage::class);
}
/** @return HasMany<Transaction, $this> */
public function transactions(): HasMany
{
return $this->hasMany(Transaction::class);
}
/** @return HasMany<Account, $this> */
public function accounts(): HasMany
{
return $this->hasMany(Account::class);
}
/** @return HasMany<Category, $this> */
public function categories(): HasMany
{
return $this->hasMany(Category::class);
}
/** @return HasMany<Bank, $this> */
public function banks(): HasMany
{
return $this->hasMany(Bank::class)
@ -103,26 +109,31 @@ class User extends Authenticatable implements HasLocalePreference, MustVerifyEma
});
}
/** @return HasMany<AutomationRule, $this> */
public function automationRules(): HasMany
{
return $this->hasMany(AutomationRule::class);
}
/** @return HasMany<Label, $this> */
public function labels(): HasMany
{
return $this->hasMany(Label::class);
}
/** @return HasMany<UserMailLog, $this> */
public function mailLogs(): HasMany
{
return $this->hasMany(UserMailLog::class);
}
/** @return HasMany<Budget, $this> */
public function budgets(): HasMany
{
return $this->hasMany(Budget::class);
}
/** @return HasMany<BankingConnection, $this> */
public function bankingConnections(): HasMany
{
return $this->hasMany(BankingConnection::class);

View File

@ -8,6 +8,9 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property \App\Enums\ChartColorScheme $chart_color_scheme
*/
class UserSetting extends Model
{
/** @use HasFactory<\Database\Factories\UserSettingFactory> */

View File

@ -7,6 +7,7 @@ use App\Actions\Fortify\CreateNewUser;
use App\Actions\Fortify\ResetUserPassword;
use App\Http\Responses\LoginResponse;
use App\Http\Responses\TwoFactorLoginResponse;
use App\Models\User;
use Illuminate\Auth\Events\Registered;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
@ -106,7 +107,13 @@ class FortifyServiceProvider extends ServiceProvider
private function configureEventListeners(): void
{
Event::listen(function (Registered $event) {
app(CreateDefaultCategories::class)->handle($event->user);
$user = $event->user;
if (! $user instanceof User) {
return;
}
app(CreateDefaultCategories::class)->handle($user);
});
}
}

View File

@ -86,7 +86,7 @@ class AccountMetricsService
* Investment accounts get `_invested` entries.
*
* @param Collection<int, Account> $accounts
* @return array{data: list<array<string, mixed>>, accounts: array<string, array<string, mixed>>, currency_code: string}
* @return array{data: list<array<string, mixed>>, accounts: mixed, currency_code: string}
*/
public function getNetWorthEvolution(string $userCurrency, Collection $accounts, Carbon $start, Carbon $end): array
{
@ -173,7 +173,7 @@ class AccountMetricsService
* Accounts with a different currency than the user also get `_original` entries.
*
* @param Collection<int, Account> $accounts
* @return array{data: list<array<string, mixed>>, accounts: array<string, array<string, mixed>>, currency_code: string}
* @return array{data: list<array<string, mixed>>, accounts: mixed, currency_code: string}
*/
public function getNetWorthDailyEvolution(string $userCurrency, Collection $accounts, Carbon $start, Carbon $end): array
{

View File

@ -38,7 +38,7 @@ class BalanceLookup
{
$instance = new self;
if (empty($accountIds) || (is_countable($accountIds) && count($accountIds) === 0)) {
if (empty($accountIds)) {
return $instance;
}

View File

@ -25,7 +25,7 @@ class BalanceSyncService
}
$result = $this->provider->getBalances($account->external_account_id);
$balances = $result['balances'] ?? [];
$balances = $result['balances'];
if (empty($balances)) {
return;
@ -69,7 +69,7 @@ class BalanceSyncService
$existingDates = $account->balances()
->pluck('balance_date')
->map(fn ($date) => $date->toDateString())
->map(fn (mixed $date) => $date instanceof \Carbon\Carbon ? $date->toDateString() : (string) $date)
->flip()
->all();

View File

@ -71,7 +71,7 @@ class BinanceBalanceSyncService
public function syncCurrentBalance(Account $account, BinanceClient $client, ?int $investedAmountCents = null): void
{
$accountData = $client->getAccount();
$balances = $accountData['balances'] ?? [];
$balances = $accountData['balances'];
if (empty($balances)) {
return;
@ -194,7 +194,7 @@ class BinanceBalanceSyncService
self::SNAPSHOT_WINDOW_DAYS,
);
foreach ($response['snapshotVos'] ?? [] as $snapshot) {
foreach ($response['snapshotVos'] as $snapshot) {
$snapshots[] = $snapshot;
}

View File

@ -143,7 +143,7 @@ class BinanceClient
->acceptJson()
->retry(
self::RETRY_BACKOFF_MS,
fn (Exception $e) => $e instanceof RequestException && $e->response->status() === 429,
when: fn (\Throwable $e) => $e instanceof RequestException && $e->response->status() === 429,
);
}
}

View File

@ -56,11 +56,11 @@ class BitpandaBalanceSyncService
$wallets = $client->getCryptoWallets();
$total = 0.0;
foreach ($wallets['data'] ?? [] as $wallet) {
$attributes = $wallet['attributes'] ?? [];
$balance = (float) ($attributes['balance'] ?? 0);
$symbol = $attributes['cryptocoin_symbol'] ?? null;
$deleted = $attributes['deleted'] ?? false;
foreach ($wallets['data'] as $wallet) {
$attributes = $wallet['attributes'];
$balance = (float) $attributes['balance'];
$symbol = $attributes['cryptocoin_symbol'];
$deleted = $attributes['deleted'];
if ($balance <= 0 || ! $symbol || $deleted) {
continue;
@ -91,10 +91,10 @@ class BitpandaBalanceSyncService
$wallets = $client->getFiatWallets();
$total = 0.0;
foreach ($wallets['data'] ?? [] as $wallet) {
$attributes = $wallet['attributes'] ?? [];
$balance = (float) ($attributes['balance'] ?? 0);
$symbol = strtoupper($attributes['fiat_symbol'] ?? '');
foreach ($wallets['data'] as $wallet) {
$attributes = $wallet['attributes'];
$balance = (float) $attributes['balance'];
$symbol = strtoupper($attributes['fiat_symbol']);
if ($balance <= 0 || ! $symbol) {
continue;
@ -161,7 +161,7 @@ class BitpandaBalanceSyncService
$total = 0.0;
foreach ($transactions as $transaction) {
$attributes = $transaction['attributes'] ?? [];
$attributes = $transaction['attributes'];
$status = $attributes['status'] ?? '';
$amount = (float) ($attributes['amount'] ?? 0);

View File

@ -115,7 +115,7 @@ class BitpandaClient
do {
$response = $this->getFiatTransactions($type, $cursor, 100);
$transactions = $response['data'] ?? [];
$transactions = $response['data'];
if (empty($transactions)) {
break;
@ -147,7 +147,7 @@ class BitpandaClient
do {
$response = $this->getTrades($cursor, 100);
$trades = $response['data'] ?? [];
$trades = $response['data'];
if (empty($trades)) {
break;

View File

@ -39,11 +39,7 @@ class IndexaCapitalClient
$accounts = [];
foreach ($userData['accounts'] ?? [] as $account) {
if (! isset($account['account_number'])) {
continue;
}
foreach ($userData['accounts'] as $account) {
$accountDetails = $this->getAccount($account['account_number']);
$accounts[] = $accountDetails;
}
@ -68,7 +64,7 @@ class IndexaCapitalClient
/**
* Get performance data for an account, including current portfolio value.
*
* @return array{total_amount: float, return: float, return_percentage: float}
* @return array{total_amount?: float, return?: float, return_percentage?: float, portfolios?: array<int, array{date?: string, total_amount?: float, return?: float}>, net_amounts?: array<string, float>}
*/
public function getPerformance(string $accountNumber): array
{

View File

@ -20,7 +20,7 @@ class BudgetPeriodService
// If no allocated amount provided, use the last period's amount or 0
if ($allocatedAmount === null) {
$lastPeriod = $budget->periods()->orderBy('end_date', 'desc')->first();
$allocatedAmount = $lastPeriod?->allocated_amount ?? 0;
$allocatedAmount = $lastPeriod !== null ? $lastPeriod->allocated_amount : 0;
}
return BudgetPeriod::create([

View File

@ -24,6 +24,7 @@
},
"require-dev": {
"fakerphp/faker": "^1.23",
"larastan/larastan": "^3.9",
"laravel/boost": "^2",
"laravel/pail": "^1.2.2",
"laravel/pint": "^1.24",
@ -33,7 +34,8 @@
"pestphp/pest": "^4.1",
"pestphp/pest-plugin-browser": "^4.0",
"pestphp/pest-plugin-laravel": "^4.0",
"testcontainers/testcontainers": "^1.0"
"testcontainers/testcontainers": "^1.0",
"tomasvotruba/unused-public": "^2.2"
},
"autoload": {
"psr-4": {

243
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "3d386f9d4f41f3e20d9431bb81b4e030",
"content-hash": "481c17881a66827f27d14db91425942d",
"packages": [
{
"name": "bacon/bacon-qr-code",
@ -9377,6 +9377,47 @@
},
"time": "2025-04-30T06:54:44+00:00"
},
{
"name": "iamcal/sql-parser",
"version": "v0.7",
"source": {
"type": "git",
"url": "https://github.com/iamcal/SQLParser.git",
"reference": "610392f38de49a44dab08dc1659960a29874c4b8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/iamcal/SQLParser/zipball/610392f38de49a44dab08dc1659960a29874c4b8",
"reference": "610392f38de49a44dab08dc1659960a29874c4b8",
"shasum": ""
},
"require-dev": {
"php-coveralls/php-coveralls": "^1.0",
"phpunit/phpunit": "^5|^6|^7|^8|^9"
},
"type": "library",
"autoload": {
"psr-4": {
"iamcal\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Cal Henderson",
"email": "cal@iamcal.com"
}
],
"description": "MySQL schema parser",
"support": {
"issues": "https://github.com/iamcal/SQLParser/issues",
"source": "https://github.com/iamcal/SQLParser/tree/v0.7"
},
"time": "2026-01-28T22:20:33+00:00"
},
{
"name": "jane-php/json-schema-runtime",
"version": "v7.10.4",
@ -9560,6 +9601,96 @@
},
"time": "2023-02-03T21:26:53+00:00"
},
{
"name": "larastan/larastan",
"version": "v3.9.3",
"source": {
"type": "git",
"url": "https://github.com/larastan/larastan.git",
"reference": "64a52bcc5347c89fdf131cb59f96ebfbc8d1ad65"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/larastan/larastan/zipball/64a52bcc5347c89fdf131cb59f96ebfbc8d1ad65",
"reference": "64a52bcc5347c89fdf131cb59f96ebfbc8d1ad65",
"shasum": ""
},
"require": {
"ext-json": "*",
"iamcal/sql-parser": "^0.7.0",
"illuminate/console": "^11.44.2 || ^12.4.1 || ^13",
"illuminate/container": "^11.44.2 || ^12.4.1 || ^13",
"illuminate/contracts": "^11.44.2 || ^12.4.1 || ^13",
"illuminate/database": "^11.44.2 || ^12.4.1 || ^13",
"illuminate/http": "^11.44.2 || ^12.4.1 || ^13",
"illuminate/pipeline": "^11.44.2 || ^12.4.1 || ^13",
"illuminate/support": "^11.44.2 || ^12.4.1 || ^13",
"php": "^8.2",
"phpstan/phpstan": "^2.1.32"
},
"require-dev": {
"doctrine/coding-standard": "^13",
"laravel/framework": "^11.44.2 || ^12.7.2 || ^13",
"mockery/mockery": "^1.6.12",
"nikic/php-parser": "^5.4",
"orchestra/canvas": "^v9.2.2 || ^10.0.1 || ^11",
"orchestra/testbench-core": "^9.12.0 || ^10.1 || ^11",
"phpstan/phpstan-deprecation-rules": "^2.0.1",
"phpunit/phpunit": "^10.5.35 || ^11.5.15 || ^12.5.8"
},
"suggest": {
"orchestra/testbench": "Using Larastan for analysing a package needs Testbench",
"phpmyadmin/sql-parser": "Install to enable Larastan's optional phpMyAdmin-based SQL parser automatically"
},
"type": "phpstan-extension",
"extra": {
"phpstan": {
"includes": [
"extension.neon"
]
},
"branch-alias": {
"dev-master": "3.0-dev"
}
},
"autoload": {
"psr-4": {
"Larastan\\Larastan\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Can Vural",
"email": "can9119@gmail.com"
}
],
"description": "Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel",
"keywords": [
"PHPStan",
"code analyse",
"code analysis",
"larastan",
"laravel",
"package",
"php",
"static analysis"
],
"support": {
"issues": "https://github.com/larastan/larastan/issues",
"source": "https://github.com/larastan/larastan/tree/v3.9.3"
},
"funding": [
{
"url": "https://github.com/canvural",
"type": "github"
}
],
"time": "2026-02-20T12:07:12+00:00"
},
{
"name": "laravel/boost",
"version": "v2.2.0",
@ -11628,6 +11759,59 @@
},
"time": "2025-11-21T15:09:14+00:00"
},
{
"name": "phpstan/phpstan",
"version": "2.1.40",
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b",
"reference": "9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b",
"shasum": ""
},
"require": {
"php": "^7.4|^8.0"
},
"conflict": {
"phpstan/phpstan-shim": "*"
},
"bin": [
"phpstan",
"phpstan.phar"
],
"type": "library",
"autoload": {
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHPStan - PHP Static Analysis Tool",
"keywords": [
"dev",
"static analysis"
],
"support": {
"docs": "https://phpstan.org/user-guide/getting-started",
"forum": "https://github.com/phpstan/phpstan/discussions",
"issues": "https://github.com/phpstan/phpstan/issues",
"security": "https://github.com/phpstan/phpstan/security/policy",
"source": "https://github.com/phpstan/phpstan-src"
},
"funding": [
{
"url": "https://github.com/ondrejmirtes",
"type": "github"
},
{
"url": "https://github.com/phpstan",
"type": "github"
}
],
"time": "2026-02-23T15:04:35+00:00"
},
{
"name": "phpunit/php-code-coverage",
"version": "12.5.3",
@ -13604,6 +13788,63 @@
],
"time": "2025-12-08T11:19:18+00:00"
},
{
"name": "tomasvotruba/unused-public",
"version": "2.2.0",
"source": {
"type": "git",
"url": "https://github.com/TomasVotruba/unused-public.git",
"reference": "ec82a9a1e3216eb5db174430a740ad8d9ac1dfc2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/TomasVotruba/unused-public/zipball/ec82a9a1e3216eb5db174430a740ad8d9ac1dfc2",
"reference": "ec82a9a1e3216eb5db174430a740ad8d9ac1dfc2",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0",
"phpstan/phpstan": "^2.1.33",
"webmozart/assert": "^1.11|^2.0"
},
"type": "phpstan-extension",
"extra": {
"phpstan": {
"includes": [
"config/extension.neon"
]
}
},
"autoload": {
"psr-4": {
"TomasVotruba\\UnusedPublic\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Detect unused public properties, constants and methods in your code",
"keywords": [
"phpstan-extension",
"static analysis"
],
"support": {
"issues": "https://github.com/TomasVotruba/unused-public/issues",
"source": "https://github.com/TomasVotruba/unused-public/tree/2.2.0"
},
"funding": [
{
"url": "https://www.paypal.me/rectorphp",
"type": "custom"
},
{
"url": "https://github.com/tomasvotruba",
"type": "github"
}
],
"time": "2026-01-07T22:23:06+00:00"
},
{
"name": "webmozart/assert",
"version": "2.1.5",

2
phpstan-baseline.neon Normal file
View File

@ -0,0 +1,2 @@
parameters:
ignoreErrors: []

67
phpstan.neon Normal file
View File

@ -0,0 +1,67 @@
includes:
- vendor/larastan/larastan/extension.neon
- vendor/tomasvotruba/unused-public/config/extension.neon
- phpstan-baseline.neon
parameters:
paths:
- app
level: 5
reportUnmatchedIgnoredErrors: false
unused_public:
methods: true
properties: true
constants: true
ignoreErrors:
# Controllers, Policies, Middleware, Jobs, Mail, Listeners are Laravel
# framework entry points called by the framework — not by PHP code.
- identifier: public.method.unused
path: app/Http/Controllers/*
- identifier: public.method.unused
path: app/Http/Controllers/**/*
- identifier: public.method.unused
path: app/Http/Middleware/*
- identifier: public.property.unused
path: app/Http/Middleware/*
- identifier: public.method.unused
path: app/Policies/*
- identifier: public.method.unused
path: app/Jobs/*
- identifier: public.property.unused
path: app/Jobs/*
- identifier: public.method.unused
path: app/Jobs/**/*
- identifier: public.property.unused
path: app/Jobs/**/*
- identifier: public.method.unused
path: app/Mail/*
- identifier: public.property.unused
path: app/Mail/*
- identifier: public.method.unused
path: app/Mail/**/*
- identifier: public.property.unused
path: app/Mail/**/*
- identifier: public.method.unused
path: app/Listeners/*
- identifier: public.property.unused
path: app/Listeners/*
# Model relationship methods are called dynamically by Eloquent
- identifier: public.method.unused
path: app/Models/*
- identifier: public.property.unused
path: app/Models/*
# Enum label/description methods used in Blade/frontend
- identifier: public.method.unused
path: app/Enums/*
# Actions used by Fortify/framework
- identifier: public.method.unused
path: app/Actions/*
# Service client methods may be used externally or reserved for future use
- identifier: public.method.unused
path: app/Services/Banking/BitpandaClient.php
- identifier: public.method.unused
path: app/Services/Banking/IndexaCapitalClient.php