2015-02-25 15:19:14 +01:00
< ? php
2022-12-29 19:42:26 +01:00
2016-05-20 12:41:23 +02:00
/**
* BillRepository . php
2020-02-16 14:00:57 +01:00
* Copyright ( c ) 2019 james @ firefly - iii . org
2016-05-20 12:41:23 +02:00
*
2019-10-02 06:37:26 +02:00
* This file is part of Firefly III ( https :// github . com / firefly - iii ) .
2016-10-05 06:52:15 +02:00
*
2019-10-02 06:37:26 +02:00
* 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 .
2017-10-21 08:40:00 +02:00
*
2019-10-02 06:37:26 +02:00
* This program is distributed in the hope that it will be useful ,
2017-10-21 08:40:00 +02:00
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
2019-10-02 06:37:26 +02:00
* GNU Affero General Public License for more details .
2017-10-21 08:40:00 +02:00
*
2019-10-02 06:37:26 +02:00
* 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 />.
2016-05-20 12:41:23 +02:00
*/
2017-04-09 07:44:22 +02:00
declare ( strict_types = 1 );
2015-02-25 15:19:14 +01:00
namespace FireflyIII\Repositories\Bill ;
2025-11-25 20:12:23 +01:00
use FireflyIII\Support\Facades\Navigation ;
2015-02-25 15:19:14 +01:00
use Carbon\Carbon ;
2019-10-30 20:02:21 +01:00
use FireflyIII\Exceptions\FireflyException ;
2018-02-22 20:07:14 +01:00
use FireflyIII\Factory\BillFactory ;
2020-05-07 06:44:01 +02:00
use FireflyIII\Models\Attachment ;
2015-02-25 15:19:14 +01:00
use FireflyIII\Models\Bill ;
2018-04-29 07:46:03 +02:00
use FireflyIII\Models\Note ;
2025-10-05 12:59:43 +02:00
use FireflyIII\Models\ObjectGroup ;
2021-04-07 07:53:05 +02:00
use FireflyIII\Models\Rule ;
2016-10-21 06:26:12 +02:00
use FireflyIII\Models\Transaction ;
2015-02-25 15:19:14 +01:00
use FireflyIII\Models\TransactionJournal ;
2018-02-25 19:09:05 +01:00
use FireflyIII\Repositories\Journal\JournalRepositoryInterface ;
2020-07-01 06:33:21 +02:00
use FireflyIII\Repositories\ObjectGroup\CreatesObjectGroups ;
2018-02-22 20:07:14 +01:00
use FireflyIII\Services\Internal\Destroy\BillDestroyService ;
use FireflyIII\Services\Internal\Update\BillUpdateService ;
2016-10-20 21:40:45 +02:00
use FireflyIII\Support\CacheProperties ;
2024-12-24 06:34:12 +01:00
use FireflyIII\Support\Facades\Amount ;
2025-02-23 12:28:27 +01:00
use FireflyIII\Support\Repositories\UserGroup\UserGroupInterface ;
2025-02-09 09:30:44 +01:00
use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait ;
2015-12-26 09:39:35 +01:00
use Illuminate\Database\Query\JoinClause ;
2018-02-06 18:11:33 +01:00
use Illuminate\Pagination\LengthAwarePaginator ;
2015-04-05 18:20:06 +02:00
use Illuminate\Support\Collection ;
2025-02-16 19:32:50 +01:00
use Illuminate\Support\Facades\DB ;
2024-01-05 10:55:46 +01:00
use Illuminate\Support\Facades\Log ;
2025-02-23 12:47:04 +01:00
use Illuminate\Support\Facades\Storage ;
2015-02-25 15:19:14 +01:00
/**
2017-11-15 12:25:49 +01:00
* Class BillRepository .
2015-02-25 15:19:14 +01:00
*/
2025-02-23 12:28:27 +01:00
class BillRepository implements BillRepositoryInterface , UserGroupInterface
2015-02-25 15:19:14 +01:00
{
2020-07-01 06:33:21 +02:00
use CreatesObjectGroups ;
2025-02-09 09:30:44 +01:00
use UserGroupTrait ;
2016-03-03 08:40:25 +01:00
2022-03-29 14:59:58 +02:00
public function billEndsWith ( string $query , int $limit ) : Collection
{
$search = $this -> user -> bills ();
if ( '' !== $query ) {
2024-10-10 06:30:05 +02:00
$search -> whereLike ( 'name' , sprintf ( '%%%s' , $query ));
2022-03-29 14:59:58 +02:00
}
$search -> orderBy ( 'name' , 'ASC' )
2025-03-14 17:45:16 +01:00
-> where ( 'active' , true )
;
2022-03-29 14:59:58 +02:00
return $search -> take ( $limit ) -> get ();
}
public function billStartsWith ( string $query , int $limit ) : Collection
{
$search = $this -> user -> bills ();
if ( '' !== $query ) {
2024-10-14 05:14:52 +02:00
$search -> whereLike ( 'name' , sprintf ( '%s%%' , $query ));
2022-03-29 14:59:58 +02:00
}
$search -> orderBy ( 'name' , 'ASC' )
2025-03-14 17:45:16 +01:00
-> where ( 'active' , true )
;
2022-03-29 14:59:58 +02:00
return $search -> take ( $limit ) -> get ();
}
2021-03-12 06:20:01 +01:00
/**
* Correct order of piggies in case of issues .
*/
public function correctOrder () : void
{
$set = $this -> user -> bills () -> orderBy ( 'order' , 'ASC' ) -> get ();
$current = 1 ;
foreach ( $set as $bill ) {
2023-11-05 19:55:39 +01:00
if ( $bill -> order !== $current ) {
2021-03-12 06:20:01 +01:00
$bill -> order = $current ;
$bill -> save ();
}
2023-12-20 19:35:52 +01:00
++ $current ;
2021-03-12 06:20:01 +01:00
}
}
2016-02-06 18:59:48 +01:00
public function destroy ( Bill $bill ) : bool
2015-04-05 18:20:06 +02:00
{
2018-02-22 20:07:14 +01:00
/** @var BillDestroyService $service */
$service = app ( BillDestroyService :: class );
$service -> destroy ( $bill );
2016-02-06 18:59:48 +01:00
return true ;
2015-04-05 18:20:06 +02:00
}
2021-03-12 06:20:01 +01:00
public function destroyAll () : void
{
2024-01-05 10:55:46 +01:00
Log :: channel ( 'audit' ) -> info ( 'Delete all bills through destroyAll' );
2021-03-12 06:20:01 +01:00
$this -> user -> bills () -> delete ();
}
2019-03-17 17:05:16 +01:00
/**
* Find bill by parameters .
*/
2019-05-04 20:58:43 +02:00
public function findBill ( ? int $billId , ? string $billName ) : ? Bill
2019-03-17 17:05:16 +01:00
{
if ( null !== $billId ) {
2023-11-05 08:15:17 +01:00
$searchResult = $this -> find ( $billId );
2025-05-27 17:06:15 +02:00
if ( $searchResult instanceof Bill ) {
2025-11-09 09:08:03 +01:00
Log :: debug ( sprintf ( 'Found bill based on #%d, will return it.' , $billId ));
2019-03-17 17:05:16 +01:00
return $searchResult ;
}
}
if ( null !== $billName ) {
2023-11-05 08:15:17 +01:00
$searchResult = $this -> findByName ( $billName );
2025-05-27 17:06:15 +02:00
if ( $searchResult instanceof Bill ) {
2025-11-09 09:08:03 +01:00
Log :: debug ( sprintf ( 'Found bill based on "%s", will return it.' , $billName ));
2019-03-17 17:05:16 +01:00
return $searchResult ;
}
}
2025-11-09 09:08:03 +01:00
Log :: debug ( 'Found no bill in findBill()' );
2019-03-17 17:05:16 +01:00
return null ;
}
2016-07-23 21:37:06 +02:00
/**
2023-06-21 12:34:58 +02:00
* Find a bill by ID .
2016-07-23 21:37:06 +02:00
*/
2023-06-21 12:34:58 +02:00
public function find ( int $billId ) : ? Bill
2016-07-23 21:37:06 +02:00
{
2025-01-04 07:39:16 +01:00
/** @var null|Bill */
2023-06-21 12:34:58 +02:00
return $this -> user -> bills () -> find ( $billId );
2016-07-23 21:37:06 +02:00
}
2016-01-20 15:21:27 +01:00
/**
2023-06-21 12:34:58 +02:00
* Find a bill by name .
2016-01-20 15:21:27 +01:00
*/
2023-06-21 12:34:58 +02:00
public function findByName ( string $name ) : ? Bill
2016-01-20 15:21:27 +01:00
{
2025-01-04 07:39:16 +01:00
/** @var null|Bill */
2023-06-21 12:34:58 +02:00
return $this -> user -> bills () -> where ( 'name' , $name ) -> first ([ 'bills.*' ]);
2016-01-20 15:21:27 +01:00
}
2018-12-09 13:09:43 +01:00
/**
* Get all attachments .
*/
public function getAttachments ( Bill $bill ) : Collection
{
2024-12-27 07:29:17 +01:00
$set = $bill -> attachments () -> get ();
2025-02-23 12:47:04 +01:00
$disk = Storage :: disk ( 'upload' );
2020-05-07 06:44:01 +02:00
2020-10-23 19:11:25 +02:00
return $set -> each (
2025-11-09 09:08:03 +01:00
static function ( Attachment $attachment ) use ( $disk ) : Attachment { // @phpstan-ignore-line
2020-05-07 06:44:01 +02:00
$notes = $attachment -> notes () -> first ();
$attachment -> file_exists = $disk -> exists ( $attachment -> fileName ());
2023-11-05 09:40:45 +01:00
$attachment -> notes_text = null !== $notes ? $notes -> text : '' ;
2020-05-07 06:44:01 +02:00
return $attachment ;
}
);
2018-12-09 13:09:43 +01:00
}
2016-02-06 18:59:48 +01:00
public function getBills () : Collection
2015-04-05 18:20:06 +02:00
{
2020-06-30 20:33:08 +02:00
return $this -> user -> bills ()
2025-03-14 17:45:16 +01:00
-> orderBy ( 'order' , 'ASC' )
-> orderBy ( 'active' , 'DESC' )
-> orderBy ( 'name' , 'ASC' ) -> get ()
;
2015-04-05 18:20:06 +02:00
}
2016-02-06 18:59:48 +01:00
public function getBillsForAccounts ( Collection $accounts ) : Collection
2015-12-12 10:33:19 +01:00
{
2022-12-29 19:42:26 +01:00
$fields = [
'bills.id' ,
'bills.created_at' ,
'bills.updated_at' ,
'bills.deleted_at' ,
'bills.user_id' ,
'bills.name' ,
'bills.amount_min' ,
'bills.amount_max' ,
'bills.date' ,
'bills.transaction_currency_id' ,
'bills.repeat_freq' ,
'bills.skip' ,
'bills.automatch' ,
'bills.active' ,
];
2016-10-01 09:37:18 +02:00
$ids = $accounts -> pluck ( 'id' ) -> toArray ();
2020-11-11 19:14:05 +01:00
2020-10-23 19:11:25 +02:00
return $this -> user -> bills ()
2025-03-14 17:45:16 +01:00
-> leftJoin (
'transaction_journals' ,
static function ( JoinClause $join ) : void {
$join -> on ( 'transaction_journals.bill_id' , '=' , 'bills.id' ) -> whereNull ( 'transaction_journals.deleted_at' );
}
)
-> leftJoin (
'transactions' ,
static function ( JoinClause $join ) : void {
$join -> on ( 'transaction_journals.id' , '=' , 'transactions.transaction_journal_id' ) -> where ( 'transactions.amount' , '<' , 0 );
}
)
-> whereIn ( 'transactions.account_id' , $ids )
-> whereNull ( 'transaction_journals.deleted_at' )
-> orderBy ( 'bills.active' , 'DESC' )
-> orderBy ( 'bills.name' , 'ASC' )
-> groupBy ( $fields )
-> get ( $fields )
;
2015-12-12 10:33:19 +01:00
}
2018-05-07 20:35:14 +02:00
/**
* Get all bills with these ID ' s .
*/
public function getByIds ( array $billIds ) : Collection
{
return $this -> user -> bills () -> whereIn ( 'id' , $billIds ) -> get ();
}
2018-04-29 07:46:03 +02:00
/**
* Get text or return empty string .
*/
public function getNoteText ( Bill $bill ) : string
{
2023-12-20 19:35:52 +01:00
/** @var null|Note $note */
2018-04-29 07:46:03 +02:00
$note = $bill -> notes () -> first ();
2024-12-22 08:43:12 +01:00
return ( string ) $note ? -> text ;
2018-04-29 07:46:03 +02:00
}
2020-08-06 20:08:04 +02:00
public function getOverallAverage ( Bill $bill ) : array
2016-07-23 21:37:06 +02:00
{
2018-02-25 19:09:05 +01:00
/** @var JournalRepositoryInterface $repos */
2025-03-14 17:45:16 +01:00
$repos = app ( JournalRepositoryInterface :: class );
2018-02-25 19:09:05 +01:00
$repos -> setUser ( $this -> user );
2020-08-06 20:08:04 +02:00
// get and sort on currency
2020-11-11 19:14:05 +01:00
$result = [];
2016-08-26 09:30:52 +02:00
$journals = $bill -> transactionJournals () -> get ();
2020-08-06 20:08:04 +02:00
2016-07-23 21:37:06 +02:00
/** @var TransactionJournal $journal */
foreach ( $journals as $journal ) {
2020-08-06 20:08:04 +02:00
/** @var Transaction $transaction */
2025-08-01 13:10:11 +02:00
$transaction = $journal -> transactions () -> where ( 'amount' , '<' , 0 ) -> first ();
$currencyId = ( int ) $journal -> transaction_currency_id ;
$currency = $journal -> transactionCurrency ;
2025-03-14 17:45:16 +01:00
$result [ $currencyId ] ? ? = [
2022-12-29 19:42:26 +01:00
'sum' => '0' ,
2025-08-01 13:10:11 +02:00
'pc_sum' => '0' ,
2022-12-29 19:42:26 +01:00
'count' => 0 ,
'avg' => '0' ,
2025-08-01 13:10:11 +02:00
'pc_avg' => '0' ,
2022-12-29 19:42:26 +01:00
'currency_id' => $currency -> id ,
'currency_code' => $currency -> code ,
'currency_symbol' => $currency -> symbol ,
'currency_decimal_places' => $currency -> decimal_places ,
];
2025-08-01 13:10:11 +02:00
$result [ $currencyId ][ 'sum' ] = bcadd ( $result [ $currencyId ][ 'sum' ], ( string ) $transaction -> amount );
2025-08-01 06:12:36 +02:00
$result [ $currencyId ][ 'pc_sum' ] = bcadd ( $result [ $currencyId ][ 'pc_sum' ], $transaction -> native_amount ? ? '0' );
2025-07-31 20:24:19 +02:00
if ( $journal -> foreign_currency_id === Amount :: getPrimaryCurrency () -> id ) {
2025-08-01 06:12:36 +02:00
$result [ $currencyId ][ 'pc_sum' ] = bcadd ( $result [ $currencyId ][ 'pc_sum' ], ( string ) $transaction -> amount );
2024-12-26 11:28:31 +01:00
}
2023-12-20 19:35:52 +01:00
++ $result [ $currencyId ][ 'count' ];
2016-07-23 21:37:06 +02:00
}
2020-08-06 20:08:04 +02:00
// after loop, re-loop for avg.
/**
2023-06-21 12:34:58 +02:00
* @ var int $currencyId
2020-08-06 20:08:04 +02:00
* @ var array $arr
*/
foreach ( $result as $currencyId => $arr ) {
2025-08-01 13:10:11 +02:00
$result [ $currencyId ][ 'avg' ] = bcdiv (( string ) $arr [ 'sum' ], ( string ) $arr [ 'count' ]);
2025-08-01 06:12:36 +02:00
$result [ $currencyId ][ 'pc_avg' ] = bcdiv (( string ) $arr [ 'pc_sum' ], ( string ) $arr [ 'count' ]);
2020-08-06 20:08:04 +02:00
}
2020-11-11 19:14:05 +01:00
2020-08-06 20:08:04 +02:00
return $result ;
2016-07-23 21:37:06 +02:00
}
2018-02-06 18:11:33 +01:00
public function getPaginator ( int $size ) : LengthAwarePaginator
{
2019-08-23 06:40:48 +02:00
return $this -> user -> bills ()
2025-03-14 17:45:16 +01:00
-> orderBy ( 'active' , 'DESC' )
-> orderBy ( 'name' , 'ASC' ) -> paginate ( $size )
;
2018-02-06 18:11:33 +01:00
}
2016-10-23 14:56:05 +02:00
/**
2017-06-28 15:45:28 +02:00
* The " paid dates " list is a list of dates of transaction journals that are linked to this bill .
2016-10-23 14:56:05 +02:00
*/
public function getPaidDatesInRange ( Bill $bill , Carbon $start , Carbon $end ) : Collection
{
2025-11-09 09:08:03 +01:00
// \Illuminate\Support\Facades\Log::debug('Now in getPaidDatesInRange()');
2020-11-11 19:14:05 +01:00
2024-11-02 05:14:03 +01:00
Log :: debug ( sprintf ( 'Search for linked journals between %s and %s' , $start -> toW3cString (), $end -> toW3cString ()));
2019-08-21 04:59:35 +02:00
return $bill -> transactionJournals ()
2025-07-20 14:02:53 +02:00
-> leftJoin ( 'transactions' , 'transactions.transaction_journal_id' , '=' , 'transaction_journals.id' )
-> leftJoin ( 'transaction_currencies AS currency' , 'currency.id' , '=' , 'transactions.transaction_currency_id' )
-> leftJoin ( 'transaction_currencies AS foreign_currency' , 'foreign_currency.id' , '=' , 'transactions.foreign_currency_id' )
-> where ( 'transactions.amount' , '>' , 0 )
2025-03-14 17:45:16 +01:00
-> before ( $end ) -> after ( $start ) -> get (
2023-12-20 19:35:52 +01:00
[
2023-05-29 13:56:55 +02:00
'transaction_journals.id' ,
'transaction_journals.date' ,
'transaction_journals.transaction_group_id' ,
2025-07-20 14:02:53 +02:00
'transactions.transaction_currency_id' ,
'currency.code AS transaction_currency_code' ,
'currency.decimal_places AS transaction_currency_decimal_places' ,
'transactions.foreign_currency_id' ,
'foreign_currency.code AS foreign_currency_code' ,
'foreign_currency.decimal_places AS foreign_currency_decimal_places' ,
'transactions.amount' ,
'transactions.foreign_amount' ,
2023-05-29 13:56:55 +02:00
]
2025-03-14 17:45:16 +01:00
)
;
2016-10-23 14:56:05 +02:00
}
2018-04-14 09:59:04 +02:00
/**
* Return all rules for one bill
*/
public function getRulesForBill ( Bill $bill ) : Collection
{
return $this -> user -> rules ()
2025-03-14 17:45:16 +01:00
-> leftJoin ( 'rule_actions' , 'rule_actions.rule_id' , '=' , 'rules.id' )
-> where ( 'rule_actions.action_type' , 'link_to_bill' )
-> where ( 'rule_actions.action_value' , $bill -> name )
-> get ([ 'rules.*' ])
;
2018-04-14 09:59:04 +02:00
}
2018-04-08 17:36:37 +02:00
/**
* Return all rules related to the bills in the collection , in an associative array :
* 5 = billid
*
* 5 => [[ 'id' => 1 , 'title' => 'Some rule' ],[ 'id' => 2 , 'title' => 'Some other rule' ]]
*/
public function getRulesForBills ( Collection $collection ) : array
{
2025-03-14 17:45:16 +01:00
$rules = $this -> user -> rules ()
-> leftJoin ( 'rule_actions' , 'rule_actions.rule_id' , '=' , 'rules.id' )
-> where ( 'rule_actions.action_type' , 'link_to_bill' )
-> get ([ 'rules.id' , 'rules.title' , 'rule_actions.action_value' , 'rules.active' ])
;
$array = [];
2023-12-20 19:35:52 +01:00
2021-04-07 07:53:05 +02:00
/** @var Rule $rule */
2018-04-08 17:36:37 +02:00
foreach ( $rules as $rule ) {
2025-03-14 17:45:16 +01:00
$array [ $rule -> action_value ] ? ? = [];
2018-05-07 20:35:14 +02:00
$array [ $rule -> action_value ][] = [ 'id' => $rule -> id , 'title' => $rule -> title , 'active' => $rule -> active ];
2018-04-08 17:36:37 +02:00
}
$return = [];
foreach ( $collection as $bill ) {
$return [ $bill -> id ] = $array [ $bill -> name ] ? ? [];
}
return $return ;
}
2020-08-06 20:08:04 +02:00
public function getYearAverage ( Bill $bill , Carbon $date ) : array
2016-07-23 21:37:06 +02:00
{
2018-02-25 19:09:05 +01:00
/** @var JournalRepositoryInterface $repos */
2025-03-14 17:45:16 +01:00
$repos = app ( JournalRepositoryInterface :: class );
2018-02-25 19:09:05 +01:00
$repos -> setUser ( $this -> user );
2020-08-06 20:08:04 +02:00
// get and sort on currency
2025-03-14 17:45:16 +01:00
$result = [];
2020-08-06 20:08:04 +02:00
2016-08-26 09:30:52 +02:00
$journals = $bill -> transactionJournals ()
2025-03-14 17:45:16 +01:00
-> where ( 'date' , '>=' , $date -> year . '-01-01 00:00:00' )
-> where ( 'date' , '<=' , $date -> year . '-12-31 23:59:59' )
-> get ()
;
2020-08-06 20:08:04 +02:00
2016-07-23 21:37:06 +02:00
/** @var TransactionJournal $journal */
foreach ( $journals as $journal ) {
2023-12-20 19:35:52 +01:00
/** @var null|Transaction $transaction */
2025-08-01 13:10:11 +02:00
$transaction = $journal -> transactions () -> where ( 'amount' , '<' , 0 ) -> first ();
2020-11-11 19:14:05 +01:00
if ( null === $transaction ) {
continue ;
}
2025-08-01 13:10:11 +02:00
$currencyId = ( int ) $journal -> transaction_currency_id ;
$currency = $journal -> transactionCurrency ;
2025-03-14 17:45:16 +01:00
$result [ $currencyId ] ? ? = [
2022-12-29 19:42:26 +01:00
'sum' => '0' ,
2025-08-01 13:10:11 +02:00
'pc_sum' => '0' ,
2022-12-29 19:42:26 +01:00
'count' => 0 ,
'avg' => '0' ,
'currency_id' => $currency -> id ,
'currency_code' => $currency -> code ,
'currency_symbol' => $currency -> symbol ,
'currency_decimal_places' => $currency -> decimal_places ,
];
2025-08-01 13:10:11 +02:00
$result [ $currencyId ][ 'sum' ] = bcadd ( $result [ $currencyId ][ 'sum' ], ( string ) $transaction -> amount );
2025-08-01 06:12:36 +02:00
$result [ $currencyId ][ 'pc_sum' ] = bcadd ( $result [ $currencyId ][ 'pc_sum' ], $transaction -> native_amount ? ? '0' );
2025-07-31 20:24:19 +02:00
if ( $journal -> foreign_currency_id === Amount :: getPrimaryCurrency () -> id ) {
2025-08-01 06:12:36 +02:00
$result [ $currencyId ][ 'pc_sum' ] = bcadd ( $result [ $currencyId ][ 'pc_sum' ], ( string ) $transaction -> amount );
2024-12-26 11:28:31 +01:00
}
2023-12-20 19:35:52 +01:00
++ $result [ $currencyId ][ 'count' ];
2016-07-23 21:37:06 +02:00
}
2020-08-06 20:08:04 +02:00
// after loop, re-loop for avg.
/**
2023-06-21 12:34:58 +02:00
* @ var int $currencyId
2020-08-06 20:08:04 +02:00
* @ var array $arr
*/
foreach ( $result as $currencyId => $arr ) {
2025-08-01 13:10:11 +02:00
$result [ $currencyId ][ 'avg' ] = bcdiv (( string ) $arr [ 'sum' ], ( string ) $arr [ 'count' ]);
2025-08-01 06:12:36 +02:00
$result [ $currencyId ][ 'pc_avg' ] = bcdiv (( string ) $arr [ 'pc_sum' ], ( string ) $arr [ 'count' ]);
2020-08-06 20:08:04 +02:00
}
2020-11-11 19:14:05 +01:00
2020-08-06 20:08:04 +02:00
return $result ;
2016-07-23 21:37:06 +02:00
}
2018-04-14 09:59:04 +02:00
/**
* Link a set of journals to a bill .
*/
2019-07-21 17:15:06 +02:00
public function linkCollectionToBill ( Bill $bill , array $transactions ) : void
2018-04-14 09:59:04 +02:00
{
2018-04-14 23:25:28 +02:00
/** @var Transaction $transaction */
foreach ( $transactions as $transaction ) {
2024-12-22 08:43:12 +01:00
$journal = $bill -> user -> transactionJournals () -> find (( int ) $transaction [ 'transaction_journal_id' ]);
2018-04-14 23:23:47 +02:00
$journal -> bill_id = $bill -> id ;
$journal -> save ();
2025-11-09 09:08:03 +01:00
Log :: debug ( sprintf ( 'Linked journal #%d to bill #%d' , $journal -> id , $bill -> id ));
2018-04-14 23:23:47 +02:00
}
2018-04-14 09:59:04 +02:00
}
2016-10-21 06:26:12 +02:00
/**
* Given the date in $date , this method will return a moment in the future where the bill is expected to be paid .
2023-12-22 20:12:38 +01:00
*/
2016-10-20 21:40:45 +02:00
public function nextExpectedMatch ( Bill $bill , Carbon $date ) : Carbon
2015-02-25 15:19:14 +01:00
{
2025-03-14 17:45:16 +01:00
$cache = new CacheProperties ();
2016-10-20 21:40:45 +02:00
$cache -> addProperty ( $bill -> id );
$cache -> addProperty ( 'nextExpectedMatch' );
2016-10-21 06:26:12 +02:00
$cache -> addProperty ( $date );
2016-10-20 21:40:45 +02:00
if ( $cache -> has ()) {
2021-06-30 06:17:38 +02:00
return $cache -> get ();
2015-02-25 15:19:14 +01:00
}
2016-10-20 21:40:45 +02:00
// find the most recent date for this bill NOT in the future. Cache this date:
2025-03-14 17:45:16 +01:00
$start = clone $bill -> date ;
2024-11-06 11:11:38 +01:00
$start -> startOfDay ();
2025-11-09 09:08:03 +01:00
Log :: debug ( 'nextExpectedMatch: Start is ' . $start -> format ( 'Y-m-d' ));
2016-10-20 21:40:45 +02:00
2016-10-21 07:29:25 +02:00
while ( $start < $date ) {
2025-11-09 09:08:03 +01:00
Log :: debug ( sprintf ( '$start (%s) < $date (%s)' , $start -> format ( 'Y-m-d H:i:s' ), $date -> format ( 'Y-m-d H:i:s' )));
2025-11-25 20:12:23 +01:00
$start = Navigation :: addPeriod ( $start , $bill -> repeat_freq , $bill -> skip );
2025-11-09 09:08:03 +01:00
Log :: debug ( 'Start is now ' . $start -> format ( 'Y-m-d H:i:s' ));
2016-10-21 07:29:25 +02:00
}
2016-10-20 21:40:45 +02:00
2025-11-25 20:12:23 +01:00
$end = Navigation :: addPeriod ( $start , $bill -> repeat_freq , $bill -> skip );
2024-11-06 11:11:38 +01:00
$end -> endOfDay ();
2016-10-20 21:40:45 +02:00
// see if the bill was paid in this period.
$journalCount = $bill -> transactionJournals () -> before ( $end ) -> after ( $start ) -> count ();
if ( $journalCount > 0 ) {
// this period had in fact a bill. The new start is the current end, and we create a new end.
2025-11-09 09:08:03 +01:00
Log :: debug ( sprintf ( 'Journal count is %d, so start becomes %s' , $journalCount , $end -> format ( 'Y-m-d' )));
2016-10-20 21:40:45 +02:00
$start = clone $end ;
2025-11-25 20:12:23 +01:00
$end = Navigation :: addPeriod ( $start , $bill -> repeat_freq , $bill -> skip );
2015-02-25 15:19:14 +01:00
}
2025-11-09 09:08:03 +01:00
Log :: debug ( 'nextExpectedMatch: Final start is ' . $start -> format ( 'Y-m-d' ));
Log :: debug ( 'nextExpectedMatch: Matching end is ' . $end -> format ( 'Y-m-d' ));
2016-10-21 07:29:25 +02:00
2016-10-20 21:40:45 +02:00
$cache -> store ( $start );
2015-02-25 15:19:14 +01:00
2016-10-20 21:40:45 +02:00
return $start ;
2015-02-25 15:19:14 +01:00
}
2023-06-21 12:34:58 +02:00
/**
* @ throws FireflyException
2023-12-22 20:12:38 +01:00
*/
2023-06-21 12:34:58 +02:00
public function store ( array $data ) : Bill
{
/** @var BillFactory $factory */
$factory = app ( BillFactory :: class );
$factory -> setUser ( $this -> user );
return $factory -> create ( $data );
}
2021-03-12 06:20:01 +01:00
public function removeObjectGroup ( Bill $bill ) : Bill
{
$bill -> objectGroups () -> sync ([]);
return $bill ;
}
2020-07-21 06:22:29 +02:00
public function searchBill ( string $query , int $limit ) : Collection
2019-03-02 14:12:09 +01:00
{
$query = sprintf ( '%%%s%%' , $query );
2024-10-10 06:30:05 +02:00
return $this -> user -> bills () -> whereLike ( 'name' , $query ) -> take ( $limit ) -> get ();
2019-03-02 14:12:09 +01:00
}
2021-03-12 06:20:01 +01:00
public function setObjectGroup ( Bill $bill , string $objectGroupTitle ) : Bill
{
$objectGroup = $this -> findOrCreateObjectGroup ( $objectGroupTitle );
2025-05-27 17:06:15 +02:00
if ( $objectGroup instanceof ObjectGroup ) {
2021-03-12 06:20:01 +01:00
$bill -> objectGroups () -> sync ([ $objectGroup -> id ]);
}
return $bill ;
}
public function setOrder ( Bill $bill , int $order ) : void
{
$bill -> order = $order ;
$bill -> save ();
}
2022-12-29 19:42:26 +01:00
public function sumPaidInRange ( Carbon $start , Carbon $end ) : array
2017-01-30 16:46:30 +01:00
{
2024-12-24 06:34:12 +01:00
Log :: debug ( sprintf ( 'sumPaidInRange from %s to %s' , $start -> toW3cString (), $end -> toW3cString ()));
2025-08-01 13:10:11 +02:00
$bills = $this -> getActiveBills ();
$return = [];
2025-08-01 06:12:36 +02:00
$convertToPrimary = Amount :: convertToPrimary ( $this -> user );
2025-08-01 13:10:11 +02:00
$primary = app ( 'amount' ) -> getPrimaryCurrency ();
2023-12-20 19:35:52 +01:00
2022-12-29 19:42:26 +01:00
/** @var Bill $bill */
foreach ( $bills as $bill ) {
2024-12-24 06:34:12 +01:00
2022-12-29 19:42:26 +01:00
/** @var Collection $set */
2025-03-21 20:20:29 +01:00
$set = $bill -> transactionJournals () -> after ( $start ) -> before ( $end ) -> get ([ 'transaction_journals.*' ]);
2025-08-01 06:12:36 +02:00
$currency = $convertToPrimary && $bill -> transactionCurrency -> id !== $primary -> id ? $primary : $bill -> transactionCurrency ;
2024-12-24 06:34:12 +01:00
$return [( int ) $currency -> id ] ? ? = [
2024-12-22 08:43:12 +01:00
'id' => ( string ) $currency -> id ,
2023-06-01 19:49:28 +02:00
'name' => $currency -> name ,
'symbol' => $currency -> symbol ,
'code' => $currency -> code ,
'decimal_places' => $currency -> decimal_places ,
'sum' => '0' ,
];
2025-03-21 20:20:29 +01:00
$setAmount = '0' ;
2024-12-25 07:13:41 +01:00
2023-06-01 19:49:28 +02:00
/** @var TransactionJournal $transactionJournal */
foreach ( $set as $transactionJournal ) {
2025-03-21 05:48:51 +01:00
// grab currency from transaction.
2025-03-21 20:20:29 +01:00
$transactionCurrency = $transactionJournal -> transactionCurrency ;
2025-03-21 05:48:51 +01:00
$return [( int ) $transactionCurrency -> id ] ? ? = [
'id' => ( string ) $transactionCurrency -> id ,
'name' => $transactionCurrency -> name ,
'symbol' => $transactionCurrency -> symbol ,
'code' => $transactionCurrency -> code ,
'decimal_places' => $transactionCurrency -> decimal_places ,
'sum' => '0' ,
];
// get currency from transaction as well.
$return [( int ) $transactionCurrency -> id ][ 'sum' ] = bcadd ( $return [( int ) $transactionCurrency -> id ][ 'sum' ], Amount :: getAmountFromJournalObject ( $transactionJournal ));
2025-03-21 20:20:29 +01:00
// $setAmount = bcadd($setAmount, Amount::getAmountFromJournalObject($transactionJournal));
2022-12-29 19:42:26 +01:00
}
2024-12-31 08:09:51 +01:00
// Log::debug(sprintf('Bill #%d ("%s") with %d transaction(s) and sum %s %s', $bill->id, $bill->name, $set->count(), $currency->code, $setAmount));
2025-03-21 20:20:29 +01:00
// $return[$currency->id]['sum'] = bcadd($return[$currency->id]['sum'], $setAmount);
2024-12-31 08:09:51 +01:00
// Log::debug(sprintf('Total sum is now %s', $return[$currency->id]['sum']));
2022-12-29 19:42:26 +01:00
}
2025-03-21 05:48:51 +01:00
// remove empty sets
2025-08-01 13:10:11 +02:00
$final = [];
2025-03-21 20:20:29 +01:00
foreach ( $return as $entry ) {
if ( 0 === bccomp ( $entry [ 'sum' ], '0' )) {
2025-03-21 05:48:51 +01:00
continue ;
}
$final [] = $entry ;
}
2023-12-20 19:35:52 +01:00
2025-03-21 05:48:51 +01:00
return $final ;
2017-01-30 16:46:30 +01:00
}
2023-06-21 12:34:58 +02:00
public function getActiveBills () : Collection
{
return $this -> user -> bills ()
2025-03-14 17:45:16 +01:00
-> where ( 'active' , true )
-> orderBy ( 'bills.name' , 'ASC' )
-> get ([ 'bills.*' , DB :: raw ( '((bills.amount_min + bills.amount_max) / 2) AS expectedAmount' )]) // @phpstan-ignore-line
;
2023-06-21 12:34:58 +02:00
}
2022-12-29 19:42:26 +01:00
public function sumUnpaidInRange ( Carbon $start , Carbon $end ) : array
2015-02-25 15:19:14 +01:00
{
2025-11-09 09:08:03 +01:00
Log :: debug ( sprintf ( 'Now in sumUnpaidInRange("%s", "%s")' , $start -> format ( 'Y-m-d' ), $end -> format ( 'Y-m-d' )));
2025-08-01 13:10:11 +02:00
$bills = $this -> getActiveBills ();
$return = [];
2025-08-01 06:12:36 +02:00
$convertToPrimary = Amount :: convertToPrimary ( $this -> user );
2025-08-01 13:10:11 +02:00
$primary = app ( 'amount' ) -> getPrimaryCurrency ();
2023-12-20 19:35:52 +01:00
2022-12-29 19:42:26 +01:00
/** @var Bill $bill */
foreach ( $bills as $bill ) {
2025-11-09 09:08:03 +01:00
// \Illuminate\Support\Facades\Log::debug(sprintf('Processing bill #%d ("%s")', $bill->id, $bill->name));
2025-03-14 17:45:16 +01:00
$dates = $this -> getPayDatesInRange ( $bill , $start , $end );
$count = $bill -> transactionJournals () -> after ( $start ) -> before ( $end ) -> count ();
$total = $dates -> count () - $count ;
2025-11-09 09:08:03 +01:00
// \Illuminate\Support\Facades\Log::debug(sprintf('Pay dates: %d, count: %d, left: %d', $dates->count(), $count, $total));
// \Illuminate\Support\Facades\Log::debug('dates', $dates->toArray());
2015-02-25 15:19:14 +01:00
2025-08-01 06:12:36 +02:00
$minField = $convertToPrimary && $bill -> transactionCurrency -> id !== $primary -> id ? 'native_amount_min' : 'amount_min' ;
$maxField = $convertToPrimary && $bill -> transactionCurrency -> id !== $primary -> id ? 'native_amount_max' : 'amount_max' ;
2024-12-31 08:05:25 +01:00
// Log::debug(sprintf('min field is %s, max field is %s', $minField, $maxField));
2024-12-23 17:32:15 +01:00
2022-12-29 19:42:26 +01:00
if ( $total > 0 ) {
2025-08-01 06:12:36 +02:00
$currency = $convertToPrimary && $bill -> transactionCurrency -> id !== $primary -> id ? $primary : $bill -> transactionCurrency ;
2025-03-14 17:45:16 +01:00
$average = bcdiv ( bcadd ( $bill -> { $maxField } ? ? '0' , $bill -> { $minField } ? ? '0' ), '2' );
2024-12-24 16:56:31 +01:00
Log :: debug ( sprintf ( 'Amount to pay is %s %s (%d times)' , $currency -> code , $average , $total ));
2025-03-14 17:45:16 +01:00
$return [ $currency -> id ] ? ? = [
2024-12-22 08:43:12 +01:00
'id' => ( string ) $currency -> id ,
2022-12-29 19:42:26 +01:00
'name' => $currency -> name ,
'symbol' => $currency -> symbol ,
'code' => $currency -> code ,
'decimal_places' => $currency -> decimal_places ,
'sum' => '0' ,
];
2024-12-22 08:43:12 +01:00
$return [ $currency -> id ][ 'sum' ] = bcadd ( $return [ $currency -> id ][ 'sum' ], bcmul ( $average , ( string ) $total ));
2022-12-29 19:42:26 +01:00
}
}
return $return ;
2015-02-25 15:19:14 +01:00
}
2019-09-23 17:11:01 +02:00
/**
2023-06-21 12:34:58 +02:00
* Between start and end , tells you on which date ( s ) the bill is expected to hit .
*/
public function getPayDatesInRange ( Bill $bill , Carbon $start , Carbon $end ) : Collection
{
$set = new Collection ();
$currentStart = clone $start ;
2025-11-09 09:08:03 +01:00
// \Illuminate\Support\Facades\Log::debug(sprintf('Now at bill "%s" (%s)', $bill->name, $bill->repeat_freq));
// \Illuminate\Support\Facades\Log::debug(sprintf('First currentstart is %s', $currentStart->format('Y-m-d')));
2023-06-21 12:34:58 +02:00
while ( $currentStart <= $end ) {
2025-11-09 09:08:03 +01:00
// \Illuminate\Support\Facades\Log::debug(sprintf('Currentstart is now %s.', $currentStart->format('Y-m-d')));
2023-06-21 12:34:58 +02:00
$nextExpectedMatch = $this -> nextDateMatch ( $bill , $currentStart );
2025-11-09 09:08:03 +01:00
// \Illuminate\Support\Facades\Log::debug(sprintf('Next Date match after %s is %s', $currentStart->format('Y-m-d'), $nextExpectedMatch->format('Y-m-d')));
2023-06-21 12:34:58 +02:00
if ( $nextExpectedMatch > $end ) { // If nextExpectedMatch is after end, we continue
break ;
}
$set -> push ( clone $nextExpectedMatch );
2025-11-09 09:08:03 +01:00
// \Illuminate\Support\Facades\Log::debug(sprintf('Now %d dates in set.', $set->count()));
2023-06-21 12:34:58 +02:00
$nextExpectedMatch -> addDay ();
2025-11-09 09:08:03 +01:00
// \Illuminate\Support\Facades\Log::debug(sprintf('Currentstart (%s) has become %s.', $currentStart->format('Y-m-d'), $nextExpectedMatch->format('Y-m-d')));
2023-06-21 12:34:58 +02:00
2025-03-14 17:45:16 +01:00
$currentStart = clone $nextExpectedMatch ;
2023-06-21 12:34:58 +02:00
}
return $set ;
}
/**
* Given a bill and a date , this method will tell you at which moment this bill expects its next
* transaction . Whether or not it is there already , is not relevant .
*/
public function nextDateMatch ( Bill $bill , Carbon $date ) : Carbon
{
$cache = new CacheProperties ();
$cache -> addProperty ( $bill -> id );
$cache -> addProperty ( 'nextDateMatch' );
$cache -> addProperty ( $date );
if ( $cache -> has ()) {
return $cache -> get ();
}
// find the most recent date for this bill NOT in the future. Cache this date:
$start = clone $bill -> date ;
while ( $start < $date ) {
2025-11-25 20:12:23 +01:00
$start = Navigation :: addPeriod ( $start , $bill -> repeat_freq , $bill -> skip );
2023-06-21 12:34:58 +02:00
}
$cache -> store ( $start );
return $start ;
}
2019-09-23 17:11:01 +02:00
public function unlinkAll ( Bill $bill ) : void
{
$this -> user -> transactionJournals () -> where ( 'bill_id' , $bill -> id ) -> update ([ 'bill_id' => null ]);
}
2020-06-30 20:33:08 +02:00
/**
2023-02-22 18:03:31 +01:00
* @ throws FireflyException
2023-12-22 20:12:38 +01:00
*/
2021-03-12 06:20:01 +01:00
public function update ( Bill $bill , array $data ) : Bill
2020-07-01 06:33:21 +02:00
{
2021-03-12 06:20:01 +01:00
/** @var BillUpdateService $service */
$service = app ( BillUpdateService :: class );
2020-07-11 15:13:15 +02:00
2021-03-12 06:20:01 +01:00
return $service -> update ( $bill , $data );
2020-07-11 15:13:15 +02:00
}
2015-02-25 15:19:14 +01:00
}