. */ declare(strict_types=1); namespace FireflyIII\Http\Controllers\Bill; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\Bill; use FireflyIII\Repositories\Bill\BillRepositoryInterface; use FireflyIII\Repositories\ObjectGroup\OrganisesObjectGroups; use FireflyIII\Support\Facades\Navigation; use FireflyIII\Support\JsonApi\Enrichments\SubscriptionEnrichment; use FireflyIII\Transformers\BillTransformer; use FireflyIII\User; use Illuminate\Contracts\View\Factory; use Illuminate\Contracts\View\View; use Illuminate\Foundation\Application; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; use Symfony\Component\HttpFoundation\ParameterBag; /** * Class IndexController */ class IndexController extends Controller { use OrganisesObjectGroups; private BillRepositoryInterface $repository; /** * BillController constructor. */ public function __construct() { parent::__construct(); $this->middleware(function ($request, $next) { app('view')->share('title', (string) trans('firefly.bills')); app('view')->share('mainTitleIcon', 'fa-calendar-o'); $this->repository = app(BillRepositoryInterface::class); return $next($request); }); } /** * Show all bills. */ public function index(): Application|Factory|\Illuminate\Contracts\Foundation\Application|View { $this->cleanupObjectGroups(); $this->repository->correctOrder(); $this->repository->correctTransfers(); $start = session('start'); $end = session('end'); $collection = $this->repository->getBills(); $total = $collection->count(); $parameters = new ParameterBag(); // enrich /** @var User $admin */ $admin = auth()->user(); $enrichment = new SubscriptionEnrichment(); $enrichment->setUser($admin); $enrichment->setStart($start->clone()); $enrichment->setEnd($end); $collection = $enrichment->enrich($collection); $parameters->set('start', $start->clone()); $parameters->set('end', $end); $parameters->set('convertToPrimary', $this->convertToPrimary); $parameters->set('primaryCurrency', $this->primaryCurrency); /** @var BillTransformer $transformer */ $transformer = app(BillTransformer::class); $transformer->setParameters($parameters); // loop all bills, convert to array and add rules and stuff. $rules = $this->repository->getRulesForBills($collection); // make bill groups: $bills = [0 => ['object_group_id' => 0, 'object_group_title' => (string) trans('firefly.default_group_title_name'), 'bills' => []]]; // the index is the order, not the ID. /** @var Bill $bill */ foreach ($collection as $bill) { $array = $transformer->transform($bill); $groupOrder = (int) $array['object_group_order']; // make group array if necessary: $bills[$groupOrder] ??= [ 'object_group_id' => $array['object_group_id'], 'object_group_title' => $array['object_group_title'], 'bills' => [], ]; $currency = $bill->transactionCurrency ?? $this->primaryCurrency; $array['currency_id'] = $currency->id; $array['currency_name'] = $currency->name; $array['currency_symbol'] = $currency->symbol; $array['currency_code'] = $currency->code; $array['currency_decimal_places'] = $currency->decimal_places; $array['attachments'] = $this->repository->getAttachments($bill); $array['rules'] = $rules[$bill['id']] ?? []; $bills[$groupOrder]['bills'][] = $array; } // order by key ksort($bills); // summarise per currency / per group. $sums = $this->getSums($bills); $totals = $this->getTotals($sums); $today = now()->startOfDay(); return view('bills.index', ['bills' => $bills, 'sums' => $sums, 'total' => $total, 'totals' => $totals, 'today' => $today]); } private function getSums(array $bills): array { Log::debug(sprintf('now in getSums(count:%d)', count($bills))); $sums = []; $range = Navigation::getViewRange(true); /** @var array $group */ foreach ($bills as $groupOrder => $group) { Log::debug(sprintf('Summing up group "%s"', $group['object_group_title'])); if (0 === count($group['bills'])) { Log::debug('Group has no subscriptions, continue'); continue; } Log::debug(sprintf('Group has %d subscription(s)', count($group['bills']))); /** @var array $bill */ foreach ($group['bills'] as $bill) { if (false === $bill['active']) { Log::debug(sprintf('Skip subscription #%d, inactive.', $bill['id'])); continue; } Log::debug(sprintf('Now at subscription #%d.', $bill['id'])); $currencyId = $bill['currency_id']; $sums[$groupOrder][$currencyId] ??= [ 'currency_id' => $currencyId, 'currency_code' => $bill['currency_code'], 'currency_name' => $bill['currency_name'], 'currency_symbol' => $bill['currency_symbol'], 'currency_decimal_places' => $bill['currency_decimal_places'], 'avg' => '0', 'total_left_to_pay' => '0', 'period' => $range, 'per_period' => '0', ]; Log::debug(sprintf( 'Start with avg:%s, total_left_to_pay:%s, per_period:%s', $sums[$groupOrder][$currencyId]['avg'], $sums[$groupOrder][$currencyId]['total_left_to_pay'], $sums[$groupOrder][$currencyId]['per_period'] )); // only fill in avg when bill is active. if (null !== $bill['next_expected_match']) { $avg = bcdiv(bcadd((string) $bill['amount_min'], (string) $bill['amount_max']), '2'); $avg = bcmul($avg, (string) count($bill['pay_dates'])); $sums[$groupOrder][$currencyId]['avg'] = bcadd($sums[$groupOrder][$currencyId]['avg'], $avg); Log::debug(sprintf('next expected match is "%s", avg is now %s', $bill['next_expected_match'], $sums[$groupOrder][$currencyId]['avg'])); // only fill in total_left_to_pay when bill is not yet paid. // #11474 and when it is expected in the current period if (count($bill['paid_dates']) < count($bill['pay_dates'])) { $count = count($bill['pay_dates']) - count($bill['paid_dates']); if ($count > 0) { $avg = bcdiv( bcadd((string) $bill['amount_min'], (string) $bill['amount_max']), '2' ); $avg = bcmul($avg, (string) $count); $sums[$groupOrder][$currencyId]['total_left_to_pay'] = bcadd($sums[$groupOrder][$currencyId]['total_left_to_pay'], $avg); Log::debug( sprintf( 'Bill has %d dates that need payment, total left to pay is now %s', $count, $sums[$groupOrder][$currencyId]['total_left_to_pay'] ), $bill['pay_dates'] ); } } } $perPeriod = $this->amountPerPeriod($bill, $range); Log::debug(sprintf('Add amount %s to per_period', $perPeriod)); // fill in per period regardless: $sums[$groupOrder][$currencyId]['per_period'] = bcadd($sums[$groupOrder][$currencyId]['per_period'], $perPeriod); } } return $sums; } private function amountPerPeriod(array $bill, string $range): string { $avg = bcdiv(bcadd((string) $bill['amount_min'], (string) $bill['amount_max']), '2'); Log::debug(sprintf('Amount per period for bill #%d "%s"', $bill['id'], $bill['name'])); Log::debug(sprintf('Average is %s', $avg)); // calculate amount per year: $multiplies = ['yearly' => '1', 'half-year' => '2', 'quarterly' => '4', 'monthly' => '12', 'weekly' => '52.17', 'daily' => '365.24']; $yearAmount = bcmul($avg, bcdiv($multiplies[$bill['repeat_freq']], (string) ($bill['skip'] + 1))); Log::debug(sprintf('Amount per year is %s (%s * %s / %s)', $yearAmount, $avg, $multiplies[$bill['repeat_freq']], (string) ($bill['skip'] + 1))); // per period: $division = [ '1Y' => '1', '6M' => '2', '3M' => '4', '1M' => '12', '1W' => '52.16', '1D' => '365.24', 'YTD' => '1', 'QTD' => '4', 'MTD' => '12', 'last7' => '52.16', 'last30' => '12', 'last90' => '4', 'last365' => '1', ]; $perPeriod = bcdiv($yearAmount, $division[$range]); Log::debug(sprintf('Amount per %s is %s (%s / %s)', $range, $perPeriod, $yearAmount, $division[$range])); return $perPeriod; } private function getTotals(array $sums): array { $totals = []; if (count($sums) < 2) { return []; } /** * @var array $array */ foreach ($sums as $array) { /** * @var int $currencyId * @var array $entry */ foreach ($array as $currencyId => $entry) { $totals[$currencyId] ??= [ 'currency_id' => $currencyId, 'currency_code' => $entry['currency_code'], 'currency_name' => $entry['currency_name'], 'currency_symbol' => $entry['currency_symbol'], 'currency_decimal_places' => $entry['currency_decimal_places'], 'avg' => '0', 'period' => $entry['period'], 'per_period' => '0', ]; $totals[$currencyId]['avg'] = bcadd($totals[$currencyId]['avg'], (string) $entry['avg']); $totals[$currencyId]['per_period'] = bcadd($totals[$currencyId]['per_period'], (string) $entry['per_period']); } } return $totals; } /** * Set the order of a bill. */ public function setOrder(Request $request, Bill $bill): JsonResponse { $objectGroupTitle = (string) $request->get('objectGroupTitle'); $newOrder = (int) $request->get('order'); $this->repository->setOrder($bill, $newOrder); if ('' !== $objectGroupTitle) { $this->repository->setObjectGroup($bill, $objectGroupTitle); } if ('' === $objectGroupTitle) { $this->repository->removeObjectGroup($bill); } return response()->json(['data' => 'OK']); } }