2020-03-14 10:25:12 +01:00
< ? php
2020-06-30 19:05:35 +02:00
2020-03-14 10:25:12 +01:00
/**
* CreateAutoBudgetLimits . php
2020-06-30 19:05:35 +02:00
* Copyright ( c ) 2020 james @ firefly - iii . org
2020-03-14 10:25:12 +01:00
*
* 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 />.
*/
2020-06-30 19:05:35 +02:00
declare ( strict_types = 1 );
2020-03-14 10:25:12 +01:00
namespace FireflyIII\Jobs ;
use Carbon\Carbon ;
use FireflyIII\Exceptions\FireflyException ;
use FireflyIII\Models\AutoBudget ;
use FireflyIII\Models\Budget ;
use FireflyIII\Models\BudgetLimit ;
use FireflyIII\Repositories\Budget\OperationsRepositoryInterface ;
use Illuminate\Bus\Queueable ;
use Illuminate\Contracts\Queue\ShouldQueue ;
use Illuminate\Foundation\Bus\Dispatchable ;
use Illuminate\Queue\InteractsWithQueue ;
use Illuminate\Queue\SerializesModels ;
use Illuminate\Support\Collection ;
/**
* Class CreateAutoBudgetLimits
*/
class CreateAutoBudgetLimits implements ShouldQueue
{
2022-10-30 14:24:28 +01:00
use Dispatchable ;
use InteractsWithQueue ;
use Queueable ;
use SerializesModels ;
2020-03-14 10:25:12 +01:00
2020-11-20 06:24:08 +01:00
private Carbon $date ;
2020-03-14 10:25:12 +01:00
/**
* Create a new job instance .
*/
public function __construct ( ? Carbon $date )
{
if ( null !== $date ) {
2021-03-27 20:01:28 +01:00
$newDate = clone $date ;
$newDate -> startOfDay ();
$this -> date = $newDate ;
2023-10-29 06:33:43 +01:00
app ( 'log' ) -> debug ( sprintf ( 'Created new CreateAutoBudgetLimits("%s")' , $this -> date -> format ( 'Y-m-d' )));
2020-03-14 10:25:12 +01:00
}
}
/**
* Execute the job .
*
* @ throws FireflyException
*/
public function handle () : void
{
2023-10-29 06:33:43 +01:00
app ( 'log' ) -> debug ( sprintf ( 'Now at start of CreateAutoBudgetLimits() job for %s.' , $this -> date -> format ( 'D d M Y' )));
2020-03-14 10:25:12 +01:00
$autoBudgets = AutoBudget :: get ();
2023-10-29 06:33:43 +01:00
app ( 'log' ) -> debug ( sprintf ( 'Found %d auto budgets.' , $autoBudgets -> count ()));
2020-03-14 10:25:12 +01:00
foreach ( $autoBudgets as $autoBudget ) {
$this -> handleAutoBudget ( $autoBudget );
}
}
2023-12-20 19:35:52 +01:00
public function setDate ( Carbon $date ) : void
{
$newDate = clone $date ;
$newDate -> startOfDay ();
$this -> date = $newDate ;
}
2023-05-29 13:56:55 +02:00
/**
2023-06-21 12:34:58 +02:00
* @ throws FireflyException
2023-05-29 13:56:55 +02:00
*/
2023-06-21 12:34:58 +02:00
private function handleAutoBudget ( AutoBudget $autoBudget ) : void
2023-05-29 13:56:55 +02:00
{
2023-06-21 12:34:58 +02:00
if ( null === $autoBudget -> budget ) {
2023-10-29 06:31:27 +01:00
app ( 'log' ) -> info ( sprintf ( 'Auto budget #%d is associated with a deleted budget.' , $autoBudget -> id ));
2023-06-21 12:34:58 +02:00
$autoBudget -> delete ();
2023-05-29 13:56:55 +02:00
2023-06-21 12:34:58 +02:00
return ;
}
if ( false === $autoBudget -> budget -> active ) {
2023-10-29 06:31:27 +01:00
app ( 'log' ) -> info ( sprintf ( 'Auto budget #%d is associated with an inactive budget.' , $autoBudget -> id ));
2023-05-15 06:18:02 +02:00
2023-06-21 12:34:58 +02:00
return ;
}
if ( ! $this -> isMagicDay ( $autoBudget )) {
2023-10-29 06:31:27 +01:00
app ( 'log' ) -> info (
2023-06-21 12:34:58 +02:00
sprintf (
'Today (%s) is not a magic day for %s auto-budget #%d (part of budget #%d "%s")' ,
$this -> date -> format ( 'Y-m-d' ),
$autoBudget -> period ,
$autoBudget -> id ,
$autoBudget -> budget -> id ,
$autoBudget -> budget -> name
)
);
2023-10-29 06:33:43 +01:00
app ( 'log' ) -> debug ( sprintf ( 'Done with auto budget #%d' , $autoBudget -> id ));
2023-05-15 06:18:02 +02:00
2023-06-21 12:34:58 +02:00
return ;
}
2023-10-29 06:31:27 +01:00
app ( 'log' ) -> info (
2023-05-15 06:18:02 +02:00
sprintf (
2023-06-21 12:34:58 +02:00
'Today (%s) is a magic day for %s auto-budget #%d (part of budget #%d "%s")' ,
$this -> date -> format ( 'Y-m-d' ),
$autoBudget -> period ,
$autoBudget -> id ,
$autoBudget -> budget -> id ,
$autoBudget -> budget -> name
2023-05-15 06:18:02 +02:00
)
);
2023-06-21 12:34:58 +02:00
// get date range for budget limit, based on range in auto-budget
$start = app ( 'navigation' ) -> startOfPeriod ( $this -> date , $autoBudget -> period );
$end = app ( 'navigation' ) -> endOfPeriod ( $start , $autoBudget -> period );
2023-05-15 06:18:02 +02:00
2023-06-21 12:34:58 +02:00
// find budget limit:
$budgetLimit = $this -> findBudgetLimit ( $autoBudget -> budget , $start , $end );
if ( null === $budgetLimit && AutoBudget :: AUTO_BUDGET_RESET === ( int ) $autoBudget -> auto_budget_type ) {
// that's easy: create one.
// do nothing else.
2023-05-15 06:18:02 +02:00
$this -> createBudgetLimit ( $autoBudget , $start , $end );
2023-10-29 06:33:43 +01:00
app ( 'log' ) -> debug ( sprintf ( 'Done with auto budget #%d' , $autoBudget -> id ));
2023-06-21 12:34:58 +02:00
2023-05-15 06:18:02 +02:00
return ;
}
2023-06-21 12:34:58 +02:00
if ( null === $budgetLimit && AutoBudget :: AUTO_BUDGET_ROLLOVER === ( int ) $autoBudget -> auto_budget_type ) {
// budget limit exists already,
$this -> createRollover ( $autoBudget );
2023-10-29 06:33:43 +01:00
app ( 'log' ) -> debug ( sprintf ( 'Done with auto budget #%d' , $autoBudget -> id ));
2023-05-15 06:18:02 +02:00
2023-06-21 12:34:58 +02:00
return ;
}
if ( null === $budgetLimit && AutoBudget :: AUTO_BUDGET_ADJUSTED === ( int ) $autoBudget -> auto_budget_type ) {
// budget limit exists already,
$this -> createAdjustedLimit ( $autoBudget );
2023-10-29 06:33:43 +01:00
app ( 'log' ) -> debug ( sprintf ( 'Done with auto budget #%d' , $autoBudget -> id ));
2023-05-15 06:18:02 +02:00
2023-06-21 12:34:58 +02:00
return ;
}
2023-10-29 06:33:43 +01:00
app ( 'log' ) -> debug ( sprintf ( 'Done with auto budget #%d' , $autoBudget -> id ));
2023-06-21 12:34:58 +02:00
}
2023-05-15 06:18:02 +02:00
2023-06-21 12:34:58 +02:00
/**
* @ throws FireflyException
*/
private function isMagicDay ( AutoBudget $autoBudget ) : bool
{
if ( 'daily' === $autoBudget -> period ) {
return true ;
}
2023-05-19 05:39:21 +02:00
2023-06-21 12:34:58 +02:00
if ( 'weekly' === $autoBudget -> period ) {
return $this -> date -> isMonday ();
2023-05-15 06:18:02 +02:00
}
2023-06-21 12:34:58 +02:00
if ( 'monthly' === $autoBudget -> period ) {
return 1 === $this -> date -> day ;
2023-05-15 06:18:02 +02:00
}
2023-06-21 12:34:58 +02:00
if ( 'quarterly' === $autoBudget -> period ) {
$format = 'm-d' ;
$value = $this -> date -> format ( $format );
return in_array ( $value , [ '01-01' , '04-01' , '07-01' , '10-01' ], true );
2023-05-16 21:04:06 +02:00
}
2023-06-21 12:34:58 +02:00
if ( 'half_year' === $autoBudget -> period ) {
$format = 'm-d' ;
$value = $this -> date -> format ( $format );
return in_array ( $value , [ '01-01' , '07-01' ], true );
}
if ( 'yearly' === $autoBudget -> period ) {
$format = 'm-d' ;
$value = $this -> date -> format ( $format );
return '01-01' === $value ;
}
2023-12-20 19:35:52 +01:00
2023-06-21 12:34:58 +02:00
throw new FireflyException ( sprintf ( 'isMagicDay() can\'t handle period "%s"' , $autoBudget -> period ));
}
private function findBudgetLimit ( Budget $budget , Carbon $start , Carbon $end ) : ? BudgetLimit
{
2023-10-29 06:33:43 +01:00
app ( 'log' ) -> debug (
2023-06-21 12:34:58 +02:00
sprintf (
'Going to find a budget limit for budget #%d ("%s") between %s and %s' ,
$budget -> id ,
$budget -> name ,
$start -> format ( 'Y-m-d' ),
$end -> format ( 'Y-m-d' )
)
);
return $budget -> budgetlimits ()
2023-12-20 19:35:52 +01:00
-> where ( 'start_date' , $start -> format ( 'Y-m-d' ))
-> where ( 'end_date' , $end -> format ( 'Y-m-d' )) -> first ()
;
2023-05-15 06:18:02 +02:00
}
2023-10-29 17:41:14 +01:00
private function createBudgetLimit ( AutoBudget $autoBudget , Carbon $start , Carbon $end , ? string $amount = null ) : void
2023-05-29 13:56:55 +02:00
{
2023-10-29 06:33:43 +01:00
app ( 'log' ) -> debug ( sprintf ( 'No budget limit exist. Must create one for auto-budget #%d' , $autoBudget -> id ));
2023-05-29 13:56:55 +02:00
if ( null !== $amount ) {
2023-10-29 06:33:43 +01:00
app ( 'log' ) -> debug ( sprintf ( 'Amount is overruled and will be set to %s' , $amount ));
2023-05-29 13:56:55 +02:00
}
$budgetLimit = new BudgetLimit ();
$budgetLimit -> budget () -> associate ( $autoBudget -> budget );
$budgetLimit -> transactionCurrency () -> associate ( $autoBudget -> transactionCurrency );
$budgetLimit -> start_date = $start ;
$budgetLimit -> end_date = $end ;
$budgetLimit -> amount = $amount ? ? $autoBudget -> amount ;
$budgetLimit -> period = $autoBudget -> period ;
2023-10-30 19:49:40 +01:00
$budgetLimit -> generated = 1 ;
2023-05-29 13:56:55 +02:00
$budgetLimit -> save ();
2023-10-29 06:33:43 +01:00
app ( 'log' ) -> debug ( sprintf ( 'Created budget limit #%d.' , $budgetLimit -> id ));
2023-05-29 13:56:55 +02:00
}
/**
* @ throws FireflyException
*/
private function createRollover ( AutoBudget $autoBudget ) : void
{
2023-10-29 06:33:43 +01:00
app ( 'log' ) -> debug ( sprintf ( 'Will now manage rollover for auto budget #%d' , $autoBudget -> id ));
2023-05-29 13:56:55 +02:00
// current period:
$start = app ( 'navigation' ) -> startOfPeriod ( $this -> date , $autoBudget -> period );
$end = app ( 'navigation' ) -> endOfPeriod ( $start , $autoBudget -> period );
// which means previous period:
$previousStart = app ( 'navigation' ) -> subtractPeriod ( $start , $autoBudget -> period );
$previousEnd = app ( 'navigation' ) -> endOfPeriod ( $previousStart , $autoBudget -> period );
2023-10-29 06:33:43 +01:00
app ( 'log' ) -> debug (
2023-05-29 13:56:55 +02:00
sprintf (
'Current period is %s-%s, so previous period is %s-%s' ,
$start -> format ( 'Y-m-d' ),
$end -> format ( 'Y-m-d' ),
$previousStart -> format ( 'Y-m-d' ),
$previousEnd -> format ( 'Y-m-d' )
)
);
// has budget limit in previous period?
$budgetLimit = $this -> findBudgetLimit ( $autoBudget -> budget , $previousStart , $previousEnd );
if ( null === $budgetLimit ) {
2023-10-29 06:33:43 +01:00
app ( 'log' ) -> debug ( 'No budget limit exists in previous period, so create one.' );
2023-05-29 13:56:55 +02:00
// if not, create it and we're done.
$this -> createBudgetLimit ( $autoBudget , $start , $end );
2023-10-29 06:33:43 +01:00
app ( 'log' ) -> debug ( sprintf ( 'Done with auto budget #%d' , $autoBudget -> id ));
2023-05-29 13:56:55 +02:00
return ;
}
2023-10-29 06:33:43 +01:00
app ( 'log' ) -> debug ( 'Budget limit exists for previous period.' );
2023-05-29 13:56:55 +02:00
// if has one, calculate expenses and use that as a base.
$repository = app ( OperationsRepositoryInterface :: class );
$repository -> setUser ( $autoBudget -> budget -> user );
$spent = $repository -> sumExpenses ( $previousStart , $previousEnd , null , new Collection ([ $autoBudget -> budget ]), $autoBudget -> transactionCurrency );
2023-11-05 19:41:37 +01:00
$currencyId = $autoBudget -> transaction_currency_id ;
2023-05-29 13:56:55 +02:00
$spentAmount = $spent [ $currencyId ][ 'sum' ] ? ? '0' ;
2023-10-29 06:33:43 +01:00
app ( 'log' ) -> debug ( sprintf ( 'Spent in previous budget period (%s-%s) is %s' , $previousStart -> format ( 'Y-m-d' ), $previousEnd -> format ( 'Y-m-d' ), $spentAmount ));
2023-05-29 13:56:55 +02:00
// if you spent more in previous budget period, than whatever you had previous budget period, the amount resets
// previous budget limit + spent
$budgetLeft = bcadd ( $budgetLimit -> amount , $spentAmount );
$totalAmount = $autoBudget -> amount ;
2023-10-29 06:33:43 +01:00
app ( 'log' ) -> debug ( sprintf ( 'Total amount left for previous budget period is %s' , $budgetLeft ));
2023-05-29 13:56:55 +02:00
if ( - 1 !== bccomp ( '0' , $budgetLeft )) {
2023-10-29 06:31:27 +01:00
app ( 'log' ) -> info ( sprintf ( 'The amount left is negative, so it will be reset to %s.' , $totalAmount ));
2023-05-29 13:56:55 +02:00
}
if ( 1 !== bccomp ( '0' , $budgetLeft )) {
$totalAmount = bcadd ( $budgetLeft , $totalAmount );
2023-10-29 06:31:27 +01:00
app ( 'log' ) -> info ( sprintf ( 'The amount left is positive, so the new amount will be %s.' , $totalAmount ));
2023-05-29 13:56:55 +02:00
}
// create budget limit:
$this -> createBudgetLimit ( $autoBudget , $start , $end , $totalAmount );
2023-10-29 06:33:43 +01:00
app ( 'log' ) -> debug ( sprintf ( 'Done with auto budget #%d' , $autoBudget -> id ));
2023-05-29 13:56:55 +02:00
}
2023-06-21 12:34:58 +02:00
private function createAdjustedLimit ( AutoBudget $autoBudget ) : void
2023-05-29 13:56:55 +02:00
{
2023-10-29 06:33:43 +01:00
app ( 'log' ) -> debug ( sprintf ( 'Will now manage rollover for auto budget #%d' , $autoBudget -> id ));
2023-06-21 12:34:58 +02:00
// current period:
$start = app ( 'navigation' ) -> startOfPeriod ( $this -> date , $autoBudget -> period );
$end = app ( 'navigation' ) -> endOfPeriod ( $start , $autoBudget -> period );
// which means previous period:
$previousStart = app ( 'navigation' ) -> subtractPeriod ( $start , $autoBudget -> period );
$previousEnd = app ( 'navigation' ) -> endOfPeriod ( $previousStart , $autoBudget -> period );
2023-10-29 06:33:43 +01:00
app ( 'log' ) -> debug (
2023-05-29 13:56:55 +02:00
sprintf (
2023-06-21 12:34:58 +02:00
'Current period is %s-%s, so previous period is %s-%s' ,
2023-05-29 13:56:55 +02:00
$start -> format ( 'Y-m-d' ),
2023-06-21 12:34:58 +02:00
$end -> format ( 'Y-m-d' ),
$previousStart -> format ( 'Y-m-d' ),
$previousEnd -> format ( 'Y-m-d' )
2023-05-29 13:56:55 +02:00
)
);
2023-06-21 12:34:58 +02:00
// has budget limit in previous period?
$budgetLimit = $this -> findBudgetLimit ( $autoBudget -> budget , $previousStart , $previousEnd );
2021-04-03 06:04:17 +02:00
2023-06-21 12:34:58 +02:00
if ( null === $budgetLimit ) {
2023-10-29 06:33:43 +01:00
app ( 'log' ) -> debug ( 'No budget limit exists in previous period, so create one.' );
2023-06-21 12:34:58 +02:00
// if not, create standard amount, and we're done.
$this -> createBudgetLimit ( $autoBudget , $start , $end );
2023-12-20 19:35:52 +01:00
2021-04-03 06:04:17 +02:00
return ;
}
2023-10-29 06:33:43 +01:00
app ( 'log' ) -> debug ( 'Budget limit exists for previous period.' );
2020-03-17 16:46:00 +01:00
2023-06-21 12:34:58 +02:00
// if has one, calculate expenses and use that as a base.
$repository = app ( OperationsRepositoryInterface :: class );
$repository -> setUser ( $autoBudget -> budget -> user );
$spent = $repository -> sumExpenses ( $previousStart , $previousEnd , null , new Collection ([ $autoBudget -> budget ]), $autoBudget -> transactionCurrency );
2023-11-05 19:41:37 +01:00
$currencyId = $autoBudget -> transaction_currency_id ;
2023-06-21 12:34:58 +02:00
$spentAmount = $spent [ $currencyId ][ 'sum' ] ? ? '0' ;
2023-10-29 06:33:43 +01:00
app ( 'log' ) -> debug ( sprintf ( 'Spent in previous budget period (%s-%s) is %s' , $previousStart -> format ( 'Y-m-d' ), $previousEnd -> format ( 'Y-m-d' ), $spentAmount ));
2020-03-14 10:25:12 +01:00
2023-06-21 12:34:58 +02:00
// what you spent in previous period PLUS the amount for the current period,
// if that is more than zero, that's the amount that will be set.
2020-03-14 10:25:12 +01:00
2023-06-21 12:34:58 +02:00
$budgetAvailable = bcadd ( bcadd ( $budgetLimit -> amount , $autoBudget -> amount ), $spentAmount );
$totalAmount = $autoBudget -> amount ;
2023-10-29 06:33:43 +01:00
app ( 'log' ) -> debug ( sprintf ( 'Total amount available for current budget period is %s' , $budgetAvailable ));
2021-06-01 12:25:01 +02:00
2023-06-21 12:34:58 +02:00
if ( - 1 !== bccomp ( $budgetAvailable , $totalAmount )) {
2023-10-29 06:31:27 +01:00
app ( 'log' ) -> info ( sprintf ( 'There is no overspending, no need to adjust. Budget limit amount will be %s.' , $budgetAvailable ));
2023-06-21 12:34:58 +02:00
// create budget limit:
$this -> createBudgetLimit ( $autoBudget , $start , $end , $budgetAvailable );
2020-03-14 10:25:12 +01:00
}
2023-06-21 12:34:58 +02:00
if ( 1 !== bccomp ( $budgetAvailable , $totalAmount ) && 1 === bccomp ( $budgetAvailable , '0' )) {
2023-10-29 06:31:27 +01:00
app ( 'log' ) -> info ( sprintf ( 'There was overspending, so the new amount will be %s.' , $budgetAvailable ));
2023-06-21 12:34:58 +02:00
// create budget limit:
$this -> createBudgetLimit ( $autoBudget , $start , $end , $budgetAvailable );
2020-03-14 10:25:12 +01:00
}
2023-06-21 12:34:58 +02:00
if ( 1 !== bccomp ( $budgetAvailable , $totalAmount ) && - 1 === bccomp ( $budgetAvailable , '0' )) {
2023-10-29 06:31:27 +01:00
app ( 'log' ) -> info ( 'There was overspending, but so much even this period cant fix that. Reset it to 1.' );
2023-06-21 12:34:58 +02:00
// create budget limit:
$this -> createBudgetLimit ( $autoBudget , $start , $end , '1' );
2023-05-15 06:18:02 +02:00
}
2023-10-29 06:33:43 +01:00
app ( 'log' ) -> debug ( sprintf ( 'Done with auto budget #%d' , $autoBudget -> id ));
2020-03-14 10:25:12 +01:00
}
2020-03-17 16:46:00 +01:00
}