orWhere('to_account_id', $accountId) // selectRaw query, secured with input sanitization ans parameter binding [$accountId}] ->selectRaw('SUM(CASE WHEN to_account_id = ? THEN amount ELSE -amount END) as balance', [$accountId]) ->value('balance'); return $balance ?? 0; }); } /** * Get accounts associated with a profile. * If no profileType and profileId is specified, the active profile is used. * Returns an array with account id, name, and balance (in minutes). * * @return void */ public function getAccountsInfo($profileType = null, $profileId = null) { if ($profileType === null) { $profileType = session('activeProfileType'); } if ($profileId === null) { $profileId = session('activeProfileId'); } $cacheKey = "accounts_info_{$profileType}_{$profileId}"; return Cache::remember($cacheKey, 60, function () use ($profileType, $profileId) { // Get the profile $profile = $profileType::find($profileId); if (!$profile) { return collect(); } // Check if profile has accounts relation (e.g., Admin profiles don't) if (!method_exists($profile, 'accounts')) { return collect(); } // Get the profile and its accounts in a single query $profile = $profileType::with(['accounts'])->find($profileId); if (!$profile) { return collect(); } // Return early if no accounts exist if ($profile->accounts->isEmpty()) { return collect(); } // Calculate the total balance of all accounts of the profile in a single query $accountIds = $profile->accounts->pluck('id')->toArray(); // Convert array to comma-separated string for selectRaw sanitization and parameter binding $accountIdsString = implode(',', $accountIds); $sumAccounts = DB::table('transactions') ->whereIn('from_account_id', $accountIds) ->orWhereIn('to_account_id', $accountIds) ->selectRaw("SUM(CASE WHEN to_account_id IN ($accountIdsString) THEN amount ELSE -amount END) as balance") ->value('balance'); $limitReceivable = $profile->limit_max - ($sumAccounts ?? 0) - $profile->limit_min; // If the limitReceivable is negative, set it to 0 if ($limitReceivable < 0) { $limitReceivable = 0; } // Map the collection to include the total balance $accounts = $profile->accounts->map(function ($account) use ($profileType, $limitReceivable) { return [ 'id' => $account->id, 'name' => __('messages.' . $account->name . '_account'), 'type' => strtolower(class_basename($profileType)), 'balance' => $this->getBalance($account->id), // Use getBalance function 'balanceTbFormat' => tbFormat($this->getBalance($account->id)), 'limitMin' => $account->limit_min, 'limitMax' => $account->limit_max, 'limitReceivable' => $limitReceivable, 'inactive' => $account->inactive_at ? \Illuminate\Support\Carbon::parse($account->inactive_at)->isPast() : false, 'inactiveAt' => $account->inactive_at, 'removed' => $account->deleted_at ? \Illuminate\Support\Carbon::parse($account->deleted_at)->isPast() : false, 'deletedAt' => $account->deleted_at, ]; }); return $accounts; }); } /** * Retrieves the account totals of a profile. * If no profileType and profileId is specified, the active profile is used. * * @param string|null $profileType The profile type. If null, the active profile type from the session will be used. * @param int|null $profileId The profile ID. If null, the active profile ID from the session will be used. * @param int|null $sinceDaysAgo The number of days to filter the counted transfers. If null, all transfers will be counted. * @return array Sum of all balances (in minutes), count of transfers, count of transfers received, count of transfers sent. */ public function getAccountsTotals($profileType = null, $profileId = null, $sinceDaysAgo = null) { if ($profileType === null) { $profileType = session('activeProfileType'); } if ($profileId === null) { $profileId = session('activeProfileId'); } // Get the profile instance $profile = $profileType::find($profileId); $type = strtolower(class_basename($profile)); // Robust check: profile exists and has accounts relation/method if ( !$profile || !(method_exists($profile, 'accounts') || property_exists($profile, 'accounts')) ) { return [ 'sumBalances' => 0, 'countTransfersSince' => $sinceDaysAgo !== null ? now()->subDays($sinceDaysAgo) : null, 'transfers' => 0, 'transfersReceived' => 0, 'transfersGiven' => 0, 'lastTransferDate' => null, ]; } // Get all accounts for the profile $accounts = $profile->accounts ?? collect(); if ($accounts->isEmpty()) { return [ 'sumBalances' => 0, 'countTransfersSince' => $sinceDaysAgo !== null ? now()->subDays($sinceDaysAgo) : null, 'transfers' => 0, 'transfersReceived' => 0, 'transfersGiven' => 0, 'lastTransferDate' => null, ]; } $accountIds = $accounts->pluck('id')->toArray(); // 1. Calculate sum of balances by iterating through the profile's accounts $sumBalances = 0; foreach ($accounts as $account) { $sumBalances += $this->getBalance($account->id); } // 2. Get all relevant transactions in a single query $transfersQuery = Transaction::where(function ($query) use ($accountIds) { $query->whereIn('from_account_id', $accountIds) ->orWhereIn('to_account_id', $accountIds); }); if ($sinceDaysAgo !== null) { $transfersQuery->whereDate('created_at', '>=', now()->subDays($sinceDaysAgo)); } // Get the date of the most recent transfer before fetching the full collection $lastTransferDate = (clone $transfersQuery)->latest('created_at')->value('created_at'); $transfers = $transfersQuery->get(); // 3. Initialize counters $countTransfers = 0; $countTransfersReceived = 0; $countTransfersGiven = 0; // 4. Process all transactions in a single loop foreach ($transfers as $transfer) { // Check if the transaction is internal (both from and to accounts belong to the profile) $isInternal = in_array($transfer->from_account_id, $accountIds) && in_array($transfer->to_account_id, $accountIds); // Only count non-internal transfers if (!$isInternal) { $countTransfers++; if (in_array($transfer->to_account_id, $accountIds)) { $countTransfersReceived++; } if (in_array($transfer->from_account_id, $accountIds)) { $countTransfersGiven++; } } } // 5. Assemble the result with privacy settings from config // Check if viewing own profile $activeProfileType = session('activeProfileType'); $activeProfileId = session('activeProfileId'); $isViewingOwnProfile = ($activeProfileType === $profileType && $activeProfileId === $profileId); if ($this->getCanManageAccounts() || $isViewingOwnProfile) { // Show all account info if user can manage accounts OR is viewing their own profile $totals = [ 'sumBalances' => $sumBalances, 'countTransfersSince' => $sinceDaysAgo !== null ? now()->subDays($sinceDaysAgo) : null, 'transfers' => $countTransfers, 'transfersReceived' => $countTransfersReceived, 'transfersGiven' => $countTransfersGiven, 'transfersReceivedOrGiven' => $countTransfersReceived + $countTransfersGiven, 'lastTransferDate' => $lastTransferDate, ]; } else { // Apply privacy settings from config for other viewers $totals = [ 'sumBalances' => timebank_config('account_info.'.$type.'.sumBalances_public', false) === true ? $sumBalances : null, 'countTransfersSince' => $sinceDaysAgo !== null ? now()->subDays($sinceDaysAgo) : null, 'transfers' => timebank_config('account_info.' . $type . '.countTransfers_public', false) === true ? $countTransfers : null, 'transfersReceived' => timebank_config('account_info.' . $type . '.countTransfersReceived_public', false) === true ? $countTransfersReceived : null, 'transfersGiven' => timebank_config('account_info.' . $type . '.countTransfersGiven_public', false) === true ? $countTransfersGiven : null, 'transfersReceivedOrGiven' => timebank_config('account_info.' . $type . '.countTransfersReceivedOrGiven_public', false) === true ? $countTransfersReceived + $countTransfersGiven : null, 'lastTransferDate' => timebank_config('account_info.' . $type . '.lastTransferDate_public', false) === true ? $lastTransferDate : null, ]; } return $totals; } }