2020-06-30 19:06:05 +02:00
< ? php
/**
* IndexController . php
* Copyright ( c ) 2020 james @ firefly - iii . org
*
* This file is part of Firefly III ( https :// github . com / firefly - iii ) .
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation , either version 3 of the
* License , or ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License
* along with this program . If not , see < https :// www . gnu . org / licenses />.
*/
declare ( strict_types = 1 );
namespace FireflyIII\Http\Controllers\Bill ;
2021-03-28 11:46:23 +02:00
2020-06-30 19:06:05 +02:00
use FireflyIII\Http\Controllers\Controller ;
use FireflyIII\Models\Bill ;
use FireflyIII\Repositories\Bill\BillRepositoryInterface ;
2020-06-30 20:33:08 +02:00
use FireflyIII\Repositories\ObjectGroup\OrganisesObjectGroups ;
2026-01-17 07:03:18 +01:00
use FireflyIII\Support\Facades\Navigation ;
2025-07-31 10:00:05 +02:00
use FireflyIII\Support\JsonApi\Enrichments\SubscriptionEnrichment ;
2020-06-30 19:06:05 +02:00
use FireflyIII\Transformers\BillTransformer ;
2025-07-31 10:00:05 +02:00
use FireflyIII\User ;
2023-11-04 11:31:14 +01:00
use Illuminate\Contracts\View\Factory ;
use Illuminate\Contracts\View\View ;
use Illuminate\Foundation\Application ;
2020-07-01 06:33:21 +02:00
use Illuminate\Http\JsonResponse ;
use Illuminate\Http\Request ;
2026-01-17 07:03:18 +01:00
use Illuminate\Support\Facades\Log ;
2020-06-30 19:06:05 +02:00
use Symfony\Component\HttpFoundation\ParameterBag ;
/**
* Class IndexController
*/
class IndexController extends Controller
{
2020-06-30 20:33:08 +02:00
use OrganisesObjectGroups ;
2020-10-19 18:44:52 +02:00
2020-06-30 19:06:05 +02:00
private BillRepositoryInterface $repository ;
/**
* BillController constructor .
*/
public function __construct ()
{
parent :: __construct ();
2026-01-23 15:09:50 +01:00
$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 );
2020-06-30 19:06:05 +02:00
2026-01-23 15:09:50 +01:00
return $next ( $request );
});
2020-06-30 19:06:05 +02:00
}
/**
* Show all bills .
*/
2026-01-17 07:18:08 +01:00
public function index () : Application | Factory | \Illuminate\Contracts\Foundation\Application | View
2020-06-30 19:06:05 +02:00
{
2020-06-30 20:33:08 +02:00
$this -> cleanupObjectGroups ();
$this -> repository -> correctOrder ();
2026-01-10 08:16:41 +01:00
$this -> repository -> correctTransfers ();
2026-01-23 15:14:29 +01:00
$start = session ( 'start' );
$end = session ( 'end' );
$collection = $this -> repository -> getBills ();
$total = $collection -> count ();
2025-07-31 10:00:05 +02:00
2026-01-23 15:14:29 +01:00
$parameters = new ParameterBag ();
2025-07-31 10:00:05 +02:00
// enrich
/** @var User $admin */
2026-01-23 15:14:29 +01:00
$admin = auth () -> user ();
$enrichment = new SubscriptionEnrichment ();
2025-07-31 10:00:05 +02:00
$enrichment -> setUser ( $admin );
2026-01-17 07:03:18 +01:00
$enrichment -> setStart ( $start -> clone ());
2025-07-31 10:00:05 +02:00
$enrichment -> setEnd ( $end );
2026-01-23 15:14:29 +01:00
$collection = $enrichment -> enrich ( $collection );
2025-07-31 10:00:05 +02:00
2026-01-17 07:03:18 +01:00
$parameters -> set ( 'start' , $start -> clone ());
2020-06-30 19:06:05 +02:00
$parameters -> set ( 'end' , $end );
2025-08-01 06:12:36 +02:00
$parameters -> set ( 'convertToPrimary' , $this -> convertToPrimary );
2025-08-01 13:48:32 +02:00
$parameters -> set ( 'primaryCurrency' , $this -> primaryCurrency );
2024-12-26 10:24:39 +01:00
2020-06-30 19:06:05 +02:00
/** @var BillTransformer $transformer */
2024-12-26 10:24:39 +01:00
$transformer = app ( BillTransformer :: class );
2020-06-30 19:06:05 +02:00
$transformer -> setParameters ( $parameters );
// loop all bills, convert to array and add rules and stuff.
2026-01-23 15:14:29 +01:00
$rules = $this -> repository -> getRulesForBills ( $collection );
2020-06-30 19:06:05 +02:00
// make bill groups:
2026-01-23 15:14:29 +01:00
$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.
2023-12-20 19:35:52 +01:00
2020-06-30 19:06:05 +02:00
/** @var Bill $bill */
foreach ( $collection as $bill ) {
2026-01-23 15:14:29 +01:00
$array = $transformer -> transform ( $bill );
$groupOrder = ( int ) $array [ 'object_group_order' ];
2020-06-30 19:06:05 +02:00
// make group array if necessary:
2023-12-10 06:45:59 +01:00
$bills [ $groupOrder ] ? ? = [
2022-12-29 19:41:57 +01:00
'object_group_id' => $array [ 'object_group_id' ],
'object_group_title' => $array [ 'object_group_title' ],
2026-01-23 15:14:29 +01:00
'bills' => [],
2022-12-29 19:41:57 +01:00
];
2020-07-02 20:13:47 +02:00
2026-01-23 15:14:29 +01:00
$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 ;
2020-07-02 20:13:47 +02:00
$array [ 'currency_decimal_places' ] = $currency -> decimal_places ;
2026-01-23 15:14:29 +01:00
$array [ 'attachments' ] = $this -> repository -> getAttachments ( $bill );
$array [ 'rules' ] = $rules [ $bill [ 'id' ]] ? ? [];
$bills [ $groupOrder ][ 'bills' ][] = $array ;
2020-06-30 19:06:05 +02:00
}
// order by key
ksort ( $bills );
// summarise per currency / per group.
2026-01-23 15:14:29 +01:00
$sums = $this -> getSums ( $bills );
$totals = $this -> getTotals ( $sums );
$today = now () -> startOfDay ();
2020-06-30 19:06:05 +02:00
2026-01-23 15:09:50 +01:00
return view ( 'bills.index' , [ 'bills' => $bills , 'sums' => $sums , 'total' => $total , 'totals' => $totals , 'today' => $today ]);
2020-06-30 19:06:05 +02:00
}
2021-03-28 11:46:23 +02:00
2023-06-21 12:34:58 +02:00
private function getSums ( array $bills ) : array
2020-06-30 19:06:05 +02:00
{
2026-01-17 07:03:18 +01:00
Log :: debug ( sprintf ( 'now in getSums(count:%d)' , count ( $bills )));
2023-06-21 12:34:58 +02:00
$sums = [];
2025-11-25 20:12:23 +01:00
$range = Navigation :: getViewRange ( true );
2023-06-21 12:34:58 +02:00
/** @var array $group */
foreach ( $bills as $groupOrder => $group ) {
2026-01-17 07:03:18 +01:00
Log :: debug ( sprintf ( 'Summing up group "%s"' , $group [ 'object_group_title' ]));
if ( 0 === count ( $group [ 'bills' ])) {
Log :: debug ( 'Group has no subscriptions, continue' );
2026-01-17 07:18:08 +01:00
2026-01-17 07:03:18 +01:00
continue ;
}
Log :: debug ( sprintf ( 'Group has %d subscription(s)' , count ( $group [ 'bills' ])));
2026-01-17 07:18:08 +01:00
2023-06-21 12:34:58 +02:00
/** @var array $bill */
foreach ( $group [ 'bills' ] as $bill ) {
if ( false === $bill [ 'active' ]) {
2026-01-17 07:03:18 +01:00
Log :: debug ( sprintf ( 'Skip subscription #%d, inactive.' , $bill [ 'id' ]));
2026-01-17 07:18:08 +01:00
2023-06-21 12:34:58 +02:00
continue ;
}
2026-01-17 07:03:18 +01:00
Log :: debug ( sprintf ( 'Now at subscription #%d.' , $bill [ 'id' ]));
2023-06-21 12:34:58 +02:00
2026-01-23 15:14:29 +01:00
$currencyId = $bill [ 'currency_id' ];
2023-12-10 06:45:59 +01:00
$sums [ $groupOrder ][ $currencyId ] ? ? = [
2023-06-21 12:34:58 +02:00
'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' ,
2025-08-09 06:59:55 +02:00
'total_left_to_pay' => '0' ,
2023-06-21 12:34:58 +02:00
'period' => $range ,
2026-01-23 15:14:29 +01:00
'per_period' => '0' ,
2023-06-21 12:34:58 +02:00
];
2026-01-23 15:09:50 +01:00
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' ]
));
2023-12-19 20:18:28 +01:00
2023-06-21 12:34:58 +02:00
// only fill in avg when bill is active.
2023-12-19 20:18:28 +01:00
if ( null !== $bill [ 'next_expected_match' ]) {
2026-01-23 15:14:29 +01:00
$avg = bcdiv ( bcadd (( string ) $bill [ 'amount_min' ], ( string ) $bill [ 'amount_max' ]), '2' );
$avg = bcmul ( $avg , ( string ) count ( $bill [ 'pay_dates' ]));
2023-06-21 12:34:58 +02:00
$sums [ $groupOrder ][ $currencyId ][ 'avg' ] = bcadd ( $sums [ $groupOrder ][ $currencyId ][ 'avg' ], $avg );
2026-01-17 07:03:18 +01:00
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 ) {
2026-01-25 10:55:27 +01:00
$avg = bcdiv (
bcadd (( string ) $bill [ 'amount_min' ], ( string ) $bill [ 'amount_max' ]),
'2'
);
2026-01-23 15:14:29 +01:00
$avg = bcmul ( $avg , ( string ) $count );
2026-01-17 07:03:18 +01:00
$sums [ $groupOrder ][ $currencyId ][ 'total_left_to_pay' ] = bcadd ( $sums [ $groupOrder ][ $currencyId ][ 'total_left_to_pay' ], $avg );
2026-01-23 15:09:50 +01:00
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' ]
);
2026-01-17 07:03:18 +01:00
}
2025-08-09 06:59:55 +02:00
}
}
2026-01-23 15:14:29 +01:00
$perPeriod = $this -> amountPerPeriod ( $bill , $range );
2026-01-17 07:03:18 +01:00
Log :: debug ( sprintf ( 'Add amount %s to per_period' , $perPeriod ));
2023-06-21 12:34:58 +02:00
// fill in per period regardless:
2026-01-17 07:03:18 +01:00
$sums [ $groupOrder ][ $currencyId ][ 'per_period' ] = bcadd ( $sums [ $groupOrder ][ $currencyId ][ 'per_period' ], $perPeriod );
2023-06-21 12:34:58 +02:00
}
2020-06-30 19:06:05 +02:00
}
2023-12-20 19:35:52 +01:00
2023-06-21 12:34:58 +02:00
return $sums ;
2020-06-30 19:06:05 +02:00
}
2020-07-01 06:33:21 +02:00
2020-07-01 11:47:16 +02:00
private function amountPerPeriod ( array $bill , string $range ) : string
{
2026-01-23 15:14:29 +01:00
$avg = bcdiv ( bcadd (( string ) $bill [ 'amount_min' ], ( string ) $bill [ 'amount_max' ]), '2' );
2020-07-01 11:47:16 +02:00
2025-11-09 09:07:14 +01:00
Log :: debug ( sprintf ( 'Amount per period for bill #%d "%s"' , $bill [ 'id' ], $bill [ 'name' ]));
Log :: debug ( sprintf ( 'Average is %s' , $avg ));
2020-07-01 11:47:16 +02:00
// calculate amount per year:
2026-01-23 15:09:50 +01:00
$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 )));
2020-07-01 11:47:16 +02:00
// per period:
2026-01-23 15:14:29 +01:00
$division = [
2023-02-19 10:50:43 +01:00
'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' ,
2026-01-23 15:14:29 +01:00
'last365' => '1' ,
2020-07-01 11:47:16 +02:00
];
2026-01-23 15:14:29 +01:00
$perPeriod = bcdiv ( $yearAmount , $division [ $range ]);
2020-07-01 11:47:16 +02:00
2025-11-09 09:07:14 +01:00
Log :: debug ( sprintf ( 'Amount per %s is %s (%s / %s)' , $range , $perPeriod , $yearAmount , $division [ $range ]));
2020-07-01 11:47:16 +02:00
return $perPeriod ;
}
2020-10-19 18:44:52 +02:00
private function getTotals ( array $sums ) : array
{
$totals = [];
if ( count ( $sums ) < 2 ) {
return [];
}
2023-12-20 19:35:52 +01:00
2020-10-19 18:44:52 +02:00
/**
* @ var array $array
*/
2020-10-26 19:15:57 +01:00
foreach ( $sums as $array ) {
2020-10-19 18:44:52 +02:00
/**
2025-08-09 08:04:02 +02:00
* @ var int $currencyId
2020-10-19 18:44:52 +02:00
* @ var array $entry
*/
foreach ( $array as $currencyId => $entry ) {
2026-01-17 07:18:08 +01:00
$totals [ $currencyId ] ? ? = [
2022-12-29 19:41:57 +01:00
'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' ],
2026-01-23 15:14:29 +01:00
'per_period' => '0' ,
2022-12-29 19:41:57 +01:00
];
2026-01-23 15:14:29 +01:00
$totals [ $currencyId ][ 'avg' ] = bcadd ( $totals [ $currencyId ][ 'avg' ], ( string ) $entry [ 'avg' ]);
2026-01-23 15:09:50 +01:00
$totals [ $currencyId ][ 'per_period' ] = bcadd ( $totals [ $currencyId ][ 'per_period' ], ( string ) $entry [ 'per_period' ]);
2020-10-19 18:44:52 +02:00
}
}
return $totals ;
}
2024-02-22 20:11:09 +01:00
/**
* Set the order of a bill .
*/
public function setOrder ( Request $request , Bill $bill ) : JsonResponse
{
2026-01-23 15:09:50 +01:00
$objectGroupTitle = ( string ) $request -> get ( 'objectGroupTitle' );
$newOrder = ( int ) $request -> get ( 'order' );
2024-02-22 20:11:09 +01:00
$this -> repository -> setOrder ( $bill , $newOrder );
if ( '' !== $objectGroupTitle ) {
$this -> repository -> setObjectGroup ( $bill , $objectGroupTitle );
}
if ( '' === $objectGroupTitle ) {
$this -> repository -> removeObjectGroup ( $bill );
}
return response () -> json ([ 'data' => 'OK' ]);
}
2020-06-30 19:06:05 +02:00
}