Files
timebank-cc-public/app/Traits/AccountInfoTrait.php
Ronald Huynen 2547717edb Initial commit
2026-03-23 21:37:59 +01:00

244 lines
11 KiB
PHP

<?php
namespace App\Traits;
use App\Models\Transaction;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
trait AccountInfoTrait
{
use ProfilePermissionTrait;
/**
* Get the balance of an account.
*
* @param int $accountId The ID of the account.
* @return float The balance of the account in minutes.
*/
public function getBalance($accountId)
{
// TODO: Store balance in extra column of transactions table with model events for create/update/delete
// a new getBalanceFast() method could read this column for fast access of balance.
// This new method would then be used for non-critical balance info.
$cacheKey = "account_balance_{$accountId}";
return Cache::remember($cacheKey, 60, function () use ($accountId) {
$balance = Transaction::where('from_account_id', $accountId)
->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;
}
}