diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0caebe33..cd69490a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/app/Console/Commands/ResetDemoAccountCommand.php b/app/Console/Commands/ResetDemoAccountCommand.php index bcfcf1e9..1540b3e3 100644 --- a/app/Console/Commands/ResetDemoAccountCommand.php +++ b/app/Console/Commands/ResetDemoAccountCommand.php @@ -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'); } diff --git a/app/Console/Commands/SetupMainUser.php b/app/Console/Commands/SetupMainUser.php index 53cb98f5..3145a592 100644 --- a/app/Console/Commands/SetupMainUser.php +++ b/app/Console/Commands/SetupMainUser.php @@ -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) { diff --git a/app/Http/Controllers/Api/CashflowAnalyticsController.php b/app/Http/Controllers/Api/CashflowAnalyticsController.php index 9d030c82..26e848d0 100644 --- a/app/Http/Controllers/Api/CashflowAnalyticsController.php +++ b/app/Http/Controllers/Api/CashflowAnalyticsController.php @@ -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), ]); } diff --git a/app/Http/Controllers/OpenBanking/AuthorizationController.php b/app/Http/Controllers/OpenBanking/AuthorizationController.php index d04f8dde..e018aa8c 100644 --- a/app/Http/Controllers/OpenBanking/AuthorizationController.php +++ b/app/Http/Controllers/OpenBanking/AuthorizationController.php @@ -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); diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 7dc3e9c3..47c274ff 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -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, diff --git a/app/Jobs/SyncBankingConnectionJob.php b/app/Jobs/SyncBankingConnectionJob.php index d3f5e3e4..c22566ec 100644 --- a/app/Jobs/SyncBankingConnectionJob.php +++ b/app/Jobs/SyncBankingConnectionJob.php @@ -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; } } diff --git a/app/Listeners/ScheduleDripEmailsListener.php b/app/Listeners/ScheduleDripEmailsListener.php index 0fa75ad1..1dcaafbc 100644 --- a/app/Listeners/ScheduleDripEmailsListener.php +++ b/app/Listeners/ScheduleDripEmailsListener.php @@ -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()); diff --git a/app/Listeners/SyncUserToResendListener.php b/app/Listeners/SyncUserToResendListener.php index 004746ee..4dfbcf86 100644 --- a/app/Listeners/SyncUserToResendListener.php +++ b/app/Listeners/SyncUserToResendListener.php @@ -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); } } diff --git a/app/Models/Account.php b/app/Models/Account.php index 3a47898c..c38e94fc 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -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 */ public function user(): BelongsTo { return $this->belongsTo(User::class); } + /** @return BelongsTo */ public function bank(): BelongsTo { return $this->belongsTo(Bank::class); } + /** @return HasMany */ public function transactions(): HasMany { return $this->hasMany(Transaction::class); } + /** @return HasMany */ public function balances(): HasMany { return $this->hasMany(AccountBalance::class); } + /** @return BelongsTo */ public function bankingConnection(): BelongsTo { return $this->belongsTo(BankingConnection::class); diff --git a/app/Models/AccountBalance.php b/app/Models/AccountBalance.php index 9b2e3223..b9a04e07 100644 --- a/app/Models/AccountBalance.php +++ b/app/Models/AccountBalance.php @@ -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 */ public function account(): BelongsTo { return $this->belongsTo(Account::class); diff --git a/app/Models/AutomationRule.php b/app/Models/AutomationRule.php index faefea94..1b4c2f37 100644 --- a/app/Models/AutomationRule.php +++ b/app/Models/AutomationRule.php @@ -32,11 +32,13 @@ class AutomationRule extends Model ]; } + /** @return BelongsTo */ public function user(): BelongsTo { return $this->belongsTo(User::class); } + /** @return BelongsTo */ public function category(): BelongsTo { return $this->belongsTo(Category::class, 'action_category_id'); diff --git a/app/Models/Bank.php b/app/Models/Bank.php index a3afd859..fdd541ef 100644 --- a/app/Models/Bank.php +++ b/app/Models/Bank.php @@ -20,11 +20,13 @@ class Bank extends Model 'user_id', ]; + /** @return BelongsTo */ public function user(): BelongsTo { return $this->belongsTo(User::class); } + /** @return HasMany */ public function accounts(): HasMany { return $this->hasMany(Account::class); diff --git a/app/Models/BankingConnection.php b/app/Models/BankingConnection.php index be8a2cc7..61c87351 100644 --- a/app/Models/BankingConnection.php +++ b/app/Models/BankingConnection.php @@ -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|null $pending_accounts_data + */ class BankingConnection extends Model { /** @use HasFactory<\Database\Factories\BankingConnectionFactory> */ @@ -52,11 +59,13 @@ class BankingConnection extends Model ]; } + /** @return BelongsTo */ public function user(): BelongsTo { return $this->belongsTo(User::class); } + /** @return HasMany */ public function accounts(): HasMany { return $this->hasMany(Account::class); diff --git a/app/Models/Budget.php b/app/Models/Budget.php index 66569b95..8b257237 100644 --- a/app/Models/Budget.php +++ b/app/Models/Budget.php @@ -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 */ public function user(): BelongsTo { return $this->belongsTo(User::class); } + /** @return BelongsTo */ public function category(): BelongsTo { return $this->belongsTo(Category::class); } + /** @return BelongsTo */ public function label(): BelongsTo { return $this->belongsTo(Label::class); } + /** @return HasMany */ public function periods(): HasMany { return $this->hasMany(BudgetPeriod::class); diff --git a/app/Models/BudgetPeriod.php b/app/Models/BudgetPeriod.php index 0e680ed0..a4782368 100644 --- a/app/Models/BudgetPeriod.php +++ b/app/Models/BudgetPeriod.php @@ -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 */ public function budget(): BelongsTo { return $this->belongsTo(Budget::class); } + /** @return HasMany */ public function budgetTransactions(): HasMany { return $this->hasMany(BudgetTransaction::class); diff --git a/app/Models/BudgetTransaction.php b/app/Models/BudgetTransaction.php index bf945b3b..a211e1ca 100644 --- a/app/Models/BudgetTransaction.php +++ b/app/Models/BudgetTransaction.php @@ -24,11 +24,13 @@ class BudgetTransaction extends Model ]; } + /** @return BelongsTo */ public function transaction(): BelongsTo { return $this->belongsTo(Transaction::class); } + /** @return BelongsTo */ public function budgetPeriod(): BelongsTo { return $this->belongsTo(BudgetPeriod::class); diff --git a/app/Models/Category.php b/app/Models/Category.php index 6e1a8563..6bd706ae 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -30,11 +30,13 @@ class Category extends Model ]; } + /** @return BelongsTo */ public function user(): BelongsTo { return $this->belongsTo(User::class); } + /** @return HasMany */ public function transactions(): HasMany { return $this->hasMany(Transaction::class); diff --git a/app/Models/EncryptedMessage.php b/app/Models/EncryptedMessage.php index 8794295c..c87ec87a 100644 --- a/app/Models/EncryptedMessage.php +++ b/app/Models/EncryptedMessage.php @@ -16,6 +16,7 @@ class EncryptedMessage extends Model 'iv', ]; + /** @return BelongsTo */ public function user(): BelongsTo { return $this->belongsTo(User::class); diff --git a/app/Models/ExchangeRate.php b/app/Models/ExchangeRate.php index a3864a6f..aefe5e95 100644 --- a/app/Models/ExchangeRate.php +++ b/app/Models/ExchangeRate.php @@ -5,6 +5,9 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +/** + * @property array $rates + */ class ExchangeRate extends Model { /** @use HasFactory<\Database\Factories\ExchangeRateFactory> */ diff --git a/app/Models/Label.php b/app/Models/Label.php index 5c8847fd..c5467d45 100644 --- a/app/Models/Label.php +++ b/app/Models/Label.php @@ -20,6 +20,7 @@ class Label extends Model 'user_id', ]; + /** @return BelongsTo */ public function user(): BelongsTo { return $this->belongsTo(User::class); diff --git a/app/Models/Transaction.php b/app/Models/Transaction.php index 20710e15..74c1bc2a 100644 --- a/app/Models/Transaction.php +++ b/app/Models/Transaction.php @@ -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 */ public function user(): BelongsTo { return $this->belongsTo(User::class); } + /** @return BelongsTo */ public function account(): BelongsTo { return $this->belongsTo(Account::class); } + /** @return BelongsTo */ public function category(): BelongsTo { return $this->belongsTo(Category::class); @@ -76,6 +83,7 @@ class Transaction extends Model ->withTimestamps(); } + /** @return HasMany */ public function budgetTransactions(): HasMany { return $this->hasMany(BudgetTransaction::class); diff --git a/app/Models/User.php b/app/Models/User.php index caef4dfd..8ef194e1 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -69,31 +69,37 @@ class User extends Authenticatable implements HasLocalePreference, MustVerifyEma return $this->onboarded_at !== null; } + /** @return HasOne */ public function setting(): HasOne { return $this->hasOne(UserSetting::class); } + /** @return HasOne */ public function encryptedMessage(): HasOne { return $this->hasOne(EncryptedMessage::class); } + /** @return HasMany */ public function transactions(): HasMany { return $this->hasMany(Transaction::class); } + /** @return HasMany */ public function accounts(): HasMany { return $this->hasMany(Account::class); } + /** @return HasMany */ public function categories(): HasMany { return $this->hasMany(Category::class); } + /** @return HasMany */ public function banks(): HasMany { return $this->hasMany(Bank::class) @@ -103,26 +109,31 @@ class User extends Authenticatable implements HasLocalePreference, MustVerifyEma }); } + /** @return HasMany */ public function automationRules(): HasMany { return $this->hasMany(AutomationRule::class); } + /** @return HasMany */ public function labels(): HasMany { return $this->hasMany(Label::class); } + /** @return HasMany */ public function mailLogs(): HasMany { return $this->hasMany(UserMailLog::class); } + /** @return HasMany */ public function budgets(): HasMany { return $this->hasMany(Budget::class); } + /** @return HasMany */ public function bankingConnections(): HasMany { return $this->hasMany(BankingConnection::class); diff --git a/app/Models/UserSetting.php b/app/Models/UserSetting.php index 4d335e16..27450425 100644 --- a/app/Models/UserSetting.php +++ b/app/Models/UserSetting.php @@ -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> */ diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php index f0d21a55..5a721324 100644 --- a/app/Providers/FortifyServiceProvider.php +++ b/app/Providers/FortifyServiceProvider.php @@ -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); }); } } diff --git a/app/Services/AccountMetricsService.php b/app/Services/AccountMetricsService.php index b1df2d40..d910a9f0 100644 --- a/app/Services/AccountMetricsService.php +++ b/app/Services/AccountMetricsService.php @@ -86,7 +86,7 @@ class AccountMetricsService * Investment accounts get `_invested` entries. * * @param Collection $accounts - * @return array{data: list>, accounts: array>, currency_code: string} + * @return array{data: list>, 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 $accounts - * @return array{data: list>, accounts: array>, currency_code: string} + * @return array{data: list>, accounts: mixed, currency_code: string} */ public function getNetWorthDailyEvolution(string $userCurrency, Collection $accounts, Carbon $start, Carbon $end): array { diff --git a/app/Services/BalanceLookup.php b/app/Services/BalanceLookup.php index 3df17391..f3df9c61 100644 --- a/app/Services/BalanceLookup.php +++ b/app/Services/BalanceLookup.php @@ -38,7 +38,7 @@ class BalanceLookup { $instance = new self; - if (empty($accountIds) || (is_countable($accountIds) && count($accountIds) === 0)) { + if (empty($accountIds)) { return $instance; } diff --git a/app/Services/Banking/BalanceSyncService.php b/app/Services/Banking/BalanceSyncService.php index 47d22488..bc87222c 100644 --- a/app/Services/Banking/BalanceSyncService.php +++ b/app/Services/Banking/BalanceSyncService.php @@ -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(); diff --git a/app/Services/Banking/BinanceBalanceSyncService.php b/app/Services/Banking/BinanceBalanceSyncService.php index 4ce85507..203e564b 100644 --- a/app/Services/Banking/BinanceBalanceSyncService.php +++ b/app/Services/Banking/BinanceBalanceSyncService.php @@ -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; } diff --git a/app/Services/Banking/BinanceClient.php b/app/Services/Banking/BinanceClient.php index bad4e2b2..778400ce 100644 --- a/app/Services/Banking/BinanceClient.php +++ b/app/Services/Banking/BinanceClient.php @@ -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, ); } } diff --git a/app/Services/Banking/BitpandaBalanceSyncService.php b/app/Services/Banking/BitpandaBalanceSyncService.php index b451f71b..d8986a22 100644 --- a/app/Services/Banking/BitpandaBalanceSyncService.php +++ b/app/Services/Banking/BitpandaBalanceSyncService.php @@ -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); diff --git a/app/Services/Banking/BitpandaClient.php b/app/Services/Banking/BitpandaClient.php index d364dcf9..1f0934c1 100644 --- a/app/Services/Banking/BitpandaClient.php +++ b/app/Services/Banking/BitpandaClient.php @@ -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; diff --git a/app/Services/Banking/IndexaCapitalClient.php b/app/Services/Banking/IndexaCapitalClient.php index 732dca34..de87115c 100644 --- a/app/Services/Banking/IndexaCapitalClient.php +++ b/app/Services/Banking/IndexaCapitalClient.php @@ -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, net_amounts?: array} */ public function getPerformance(string $accountNumber): array { diff --git a/app/Services/BudgetPeriodService.php b/app/Services/BudgetPeriodService.php index 8caf03c2..50681d7e 100644 --- a/app/Services/BudgetPeriodService.php +++ b/app/Services/BudgetPeriodService.php @@ -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([ diff --git a/composer.json b/composer.json index e64d69f2..91a8ce9e 100644 --- a/composer.json +++ b/composer.json @@ -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": { diff --git a/composer.lock b/composer.lock index 1046b99b..7e7d9d67 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 00000000..aab49911 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,2 @@ +parameters: + ignoreErrors: [] diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 00000000..5557562a --- /dev/null +++ b/phpstan.neon @@ -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