diff --git a/app/Console/Commands/TeamBudgetRollover.php b/app/Console/Commands/TeamBudgetRollover.php index 44e4fe69..98d647e8 100644 --- a/app/Console/Commands/TeamBudgetRollover.php +++ b/app/Console/Commands/TeamBudgetRollover.php @@ -2,11 +2,11 @@ namespace App\Console\Commands; +use Illuminate\Console\Command; +use Illuminate\Support\Facades\DB; use App\Domains\AppCore\Models\Category; use App\Domains\Budget\Data\BudgetReservedNames; use App\Domains\Budget\Services\BudgetRolloverService; -use Illuminate\Console\Command; -use Illuminate\Support\Facades\DB; class TeamBudgetRollover extends Command { @@ -15,7 +15,7 @@ class TeamBudgetRollover extends Command * * @var string */ - protected $signature = 'app:team-budget-rollover {teamId}'; + protected $signature = 'app:team-budget-rollover {teamId} ?{date}'; /** * The console command description. @@ -31,26 +31,17 @@ public function handle(BudgetRolloverService $rolloverService) { $teamId = $this->argument('teamId'); - - $categories = Category::where([ - 'team_id' => $teamId, - ]) - ->whereNot('name', BudgetReservedNames::READY_TO_ASSIGN->value) - ->get(); + $date = $this->argument('date'); $monthsWithTransactions = DB::table('transaction_lines') + ->where([ + "team_id" => $teamId + ]) ->selectRaw("date_format(transaction_lines.date, '%Y-%m') AS date") ->groupBy(DB::raw("date_format(transaction_lines.date, '%Y-%m')")) - ->get() - ->pluck('date'); - - $total = count($monthsWithTransactions); - $count = 0; - foreach ($monthsWithTransactions as $month) { - $count++; - $rolloverService->rollMonth($teamId, $month."-01", $categories); - echo "updated month {$month}".PHP_EOL; - echo "{$count} of {$total}".PHP_EOL.PHP_EOL; - } + ->orderBy('date') + ->first(); + + $rolloverService->startFrom($teamId, $date ?? $monthsWithTransactions->date); } } diff --git a/app/Domains/Budget/Http/Controllers/BudgetCategoryController.php b/app/Domains/Budget/Http/Controllers/BudgetCategoryController.php index 45fe581f..6602cba1 100644 --- a/app/Domains/Budget/Http/Controllers/BudgetCategoryController.php +++ b/app/Domains/Budget/Http/Controllers/BudgetCategoryController.php @@ -12,6 +12,7 @@ use App\Http\Resources\CategoryGroupCollection; use Freesgen\Atmosphere\Http\InertiaController; use App\Domains\Transaction\Services\ReportService; +use App\Domains\Budget\Services\BudgetTargetService; use App\Domains\Budget\Services\BudgetAccountService; use App\Domains\Budget\Services\BudgetCategoryService; @@ -99,24 +100,28 @@ public function show(int $categoryId) } // Category Budget targets - public function addCategoryBudget(Request $request, $categoryId) + public function addCategoryBudget(Request $request, $categoryId, BudgetTargetService $budgetTargetService) { $category = Category::find($categoryId); - $postData = $request->post(); - $category->budget()->create(array_merge($postData, [ - 'name' => $category->name, - 'user_id' => $request->user()->id, - 'team_id' => $request->user()->current_team_id, - ])); + + $budgetTargetService->add( + $category, + request()->user(), + $request->post() + ); return Redirect::back(); } - public function updateCategoryBudget(Request $request, $categoryId) + public function updateCategoryBudget($categoryId, BudgetTargetService $budgetTargetService) { $category = Category::find($categoryId); - $postData = $request->post(); - $category->budget->update($postData); + $budgetTargetService->update( + $category, + $category->budget, + request()->user(), + request()->post() + ); return Redirect::back(); } diff --git a/app/Domains/Budget/Http/Controllers/BudgetMonthController.php b/app/Domains/Budget/Http/Controllers/BudgetMonthController.php index 0a9ecd8b..d7f25218 100644 --- a/app/Domains/Budget/Http/Controllers/BudgetMonthController.php +++ b/app/Domains/Budget/Http/Controllers/BudgetMonthController.php @@ -23,7 +23,7 @@ public function assign(BudgetMovementService $service, Category $category, strin { $isMovement = request()->post('type'); $postData = $this->getPostData(); - if (! $isMovement && $postData && $postData['budgeted'] != null) { + if (!$isMovement && $postData && $postData['budgeted'] != null) { $service->registerAssignment(new BudgetAssignData( $postData['team_id'], $postData['user_id'], diff --git a/app/Domains/Budget/Http/Controllers/BudgetTargetController.php b/app/Domains/Budget/Http/Controllers/BudgetTargetController.php index 5a9c3a98..6d6b3f2d 100644 --- a/app/Domains/Budget/Http/Controllers/BudgetTargetController.php +++ b/app/Domains/Budget/Http/Controllers/BudgetTargetController.php @@ -6,28 +6,20 @@ use App\Domains\AppCore\Models\Category; use App\Domains\Budget\Models\BudgetTarget; use App\Domains\Budget\Services\BudgetTargetService; -use App\Domains\Budget\Services\BudgetCategoryService; class BudgetTargetController extends Controller { - public function store(Category $category, BudgetCategoryService $service) + public function store(Category $category, BudgetTargetService $budgetTargetService) { $postData = request()->post(); - $service->addTarget($category, $postData); - + $budgetTargetService->add($category, request()->user(), $postData); return redirect()->back(); } - public function update(Category $category, BudgetTarget $budgetTarget) + public function update(Category $category, BudgetTarget $budgetTarget, BudgetTargetService $budgetTargetService) { $postData = request()->post(); - $budgetTarget->update(array_merge( - $postData, [ - 'team_id' => request()->user()->current_team_id, - 'name' => $category->name, - 'category_id' => $budgetTarget->category_id, - ])); - + $budgetTargetService->update($category, $budgetTarget, request()->user(), $postData); return redirect()->back(); } diff --git a/app/Domains/Budget/Models/BudgetMonth.php b/app/Domains/Budget/Models/BudgetMonth.php index 77b764d3..fab0a043 100644 --- a/app/Domains/Budget/Models/BudgetMonth.php +++ b/app/Domains/Budget/Models/BudgetMonth.php @@ -2,14 +2,14 @@ namespace App\Domains\Budget\Models; -use App\Domains\AppCore\Models\Category; -use App\Domains\Budget\Data\BudgetReservedNames; use DateTime; -use Illuminate\Database\Eloquent\Factories\HasFactory; -use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; +use Illuminate\Database\Eloquent\Model; use Insane\Journal\Models\Core\Account; +use App\Domains\AppCore\Models\Category; +use App\Domains\Budget\Data\BudgetReservedNames; +use Illuminate\Database\Eloquent\Factories\HasFactory; class BudgetMonth extends Model { @@ -27,7 +27,7 @@ class BudgetMonth extends Model 'funded_spending', 'payments', 'left_from_last_month', - 'funded_spending_previous_month', + 'overspending_previous_month', ]; public function category() diff --git a/app/Domains/Budget/Models/BudgetMovement.php b/app/Domains/Budget/Models/BudgetMovement.php index 97c63418..557a3f77 100644 --- a/app/Domains/Budget/Models/BudgetMovement.php +++ b/app/Domains/Budget/Models/BudgetMovement.php @@ -2,8 +2,9 @@ namespace App\Domains\Budget\Models; -use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use App\Domains\AppCore\Models\Category; +use Illuminate\Database\Eloquent\Factories\HasFactory; class BudgetMovement extends Model { @@ -17,4 +18,23 @@ class BudgetMovement extends Model 'amount', 'date', ]; + + public function source() + { + return $this->belongsTo(Category::class, 'source_category_id'); + } + + public function destination() + { + return $this->belongsTo(Category::class, 'destination_category_id'); + } + + protected static function booted() + { + + static::saving(function ($movement) { + $movement->destination_category_name = $movement->destination->name; + $movement->source_category_name = $movement->source->name; + }); + } } diff --git a/app/Domains/Budget/Models/BudgetTarget.php b/app/Domains/Budget/Models/BudgetTarget.php index df79ccdb..fa727378 100644 --- a/app/Domains/Budget/Models/BudgetTarget.php +++ b/app/Domains/Budget/Models/BudgetTarget.php @@ -39,15 +39,16 @@ public function getExpensesByPeriod($startDate, $endDate = null) ->get()[0]->total; } - public static function getNextTargets($teamId) + public static function getNextTargets($teamId, $targetTypes = ['spending']) { return DB::query() - ->whereIn('budget_targets.target_type', ['spending']) + ->whereIn('budget_targets.target_type', $targetTypes) ->where([ 'frequency' => 'monthly', 'budget_targets.team_id' => $teamId, ]) ->whereRaw("concat(date_format(now(), '%Y-%m'), '-', frequency_month_date) >= now()") + ->addSelect(DB::raw("budget_targets.*, concat(date_format(now(), '%Y-%m'), '-', frequency_month_date) as due_date")) ->from('budget_targets') ->get(); } diff --git a/app/Domains/Budget/Services/BudgetCategoryService.php b/app/Domains/Budget/Services/BudgetCategoryService.php index 7ea71820..d1e5a396 100644 --- a/app/Domains/Budget/Services/BudgetCategoryService.php +++ b/app/Domains/Budget/Services/BudgetCategoryService.php @@ -12,8 +12,6 @@ use Insane\Journal\Models\Core\Category; use App\Domains\Budget\Data\CategoryData; use App\Domains\Budget\Models\BudgetMonth; -use App\Domains\Budget\Models\BudgetTarget; -use App\Domains\Budget\Data\BudgetReservedNames; class BudgetCategoryService { @@ -21,34 +19,6 @@ public function __construct() { } - public function addTarget(Category $category, mixed $postData) - { - return $category->budget()->create(array_merge($postData, [ - 'name' => $category->name ?? $category->display_id, - 'team_id' => $category->team_id, - ])); - } - - public function updateTarget(Category $category, mixed $postData) - { - return $category->budget()->update(array_merge($postData, [ - 'name' => $category->name ?? $category->display_id, - ])); - } - - public static function getSavingsBalance($teamId, $startDate, $endDate) - { - $endMonth = substr((string) $endDate, 0, 7); - - return DB::query() - ->where('budget_targets.team_id', $teamId) - ->whereIn('budget_targets.target_type', ['saving_balance']) - ->whereRaw("date_format(month, '%Y-%m') <= '$endMonth'") - ->from('budget_months') - ->join('budget_targets', 'budget_targets.category_id', 'budget_months.category_id') - ->sum(DB::raw('budgeted + activity')); - } - public function getLastTransactionMonth($category) { return DB::query() @@ -62,46 +32,6 @@ public function getLastTransactionMonth($category) ->first(); } - public function assignBudget(Category $category, string $month, mixed $postData) - { - $amount = (float) $postData['budgeted']; - $type = $postData['type'] ?? 'budgeted'; - $shouldAggregate = $category->name === BudgetReservedNames::READY_TO_ASSIGN->value || $type === 'movement'; - - $budgetMonth = BudgetMonth::updateOrCreate([ - 'category_id' => $category->id, - 'team_id' => $category->team_id, - 'month' => $month, - 'name' => $month, - ], [ - 'user_id' => $category->user_id, - 'budgeted' => $shouldAggregate ? DB::raw("budgeted + $amount") : $amount, - ]); - - BudgetAssigned::dispatch($budgetMonth, $postData); - - return $budgetMonth; - } - - public function moveBudget(Category $category, string $month, mixed $postData) - { - $amount = (float) $postData['budgeted']; - - $budgetMonth = BudgetMonth::updateOrCreate([ - 'category_id' => $category->id, - 'team_id' => $category->team_id, - 'month' => $month, - 'name' => $month, - ], [ - 'user_id' => $category->user_id, - 'budgeted' => DB::raw("budgeted + $amount"), - ]); - - BudgetAssigned::dispatch($budgetMonth, $postData); - - return $budgetMonth; - } - public function getBudgetInfo($category, string $month) { $yearMonth = substr((string) $month, 0, 7); @@ -112,8 +42,6 @@ public function getBudgetInfo($category, string $month) $funded = $monthBudget ? $monthBudget->funded_spending : 0; $monthPayment = $monthBudget ? $monthBudget->payments : 0; - + $monthBudget->activity; - $monthBalance = Money::of($funded, $category->account->currency_code)->minus($monthPayment)->getAmount()->toFloat(); $available = Money::of($monthBalance, 'USD') ->plus(($monthBudget->left_from_last_month * -1)) @@ -131,7 +59,6 @@ public function getBudgetInfo($category, string $month) if ($category->display_id == 'ready_to_assign') { $available = $monthBudget?->available; $monthBalance = $monthBudget?->activity; - // dd($monthBalance, $available, $monthBalance, $monthBudget); } } @@ -149,6 +76,27 @@ public function getBudgetInfo($category, string $month) return $data; } + public function getBudgetData($category, string $month) + { + $yearMonth = substr((string) $month, 0, 7); + $monthBudget = (new BudgetMonthService())->getMonthByCategory($category, $yearMonth.'-01'); + + + $data = [ + 'budgeted' => $monthBudget?->budgeted, + 'activity' => $monthBudget?->activity, + 'available' => $monthBudget?->available, + 'payments' => $monthBudget?->payments ?? 0, + 'left_from_last_month' => $monthBudget?->left_from_last_month ?? 0, + 'funded_spending_previous_month' => 0, + 'funded_spending' => $monthBudget?->funded_spending ?? 0, + 'name' => $category->name, + 'month' => $yearMonth, + ]; + + return $data; + } + public static function getBudgetSubcategories($teamId) { return Category::where([ @@ -212,11 +160,6 @@ public function getPrevMonthPaymentsLeftOver($category, $yearMonth) ->sum(DB::raw('funded_spending - payments')); } - public static function getNextBudgetItems($teamId) - { - BudgetTarget::getNextTargets($teamId); - } - public static function withBudgetInfo(Category $category) { $queryParams = request()->query(); @@ -236,6 +179,7 @@ public function updateActivity(Category $category, string $month) if ($category->account) { $transactions = $category->account->getMonthBalance($monthDate->format('Y-m'))->balance; + echo "$category->name:$transactions"; } else { $activity = $category->getMonthBalance($monthDate->format('Y-m'))?->balance; } @@ -249,8 +193,23 @@ public function updateActivity(Category $category, string $month) 'user_id' => $category->user_id, 'activity' => ($activity + $transactions) ?? 0, ]); + } - echo "{$category->name} updated to {$activity}".PHP_EOL; + public function updateMonthBalances(Category $category, string $month) + { + $monthBudgetInfo = $this->getBudgetInfo($category, $month); + BudgetMonth::updateOrCreate([ + 'category_id' => $category->id, + 'team_id' => $category->team_id, + 'month' => $month, + 'name' => $month, + ], [ + 'available' => $monthBudgetInfo['available'], + 'payments' => $monthBudgetInfo['payments'], + 'left_from_last_month' => $monthBudgetInfo['left_from_last_month'], + 'funded_spending_previous_month' => 0, + 'funded_spending' => $monthBudgetInfo['funded_spending'], + ]); } public function getCategoryActivity(Category $category, string $month) @@ -281,6 +240,7 @@ public function updateFundedSpending(Category $category, string $month) $monthDate = Carbon::createFromFormat('Y-m-d', $month); $transactions = 0; $activity = 0; + $fromBudgets = 0; if ($category->account) { $yearMonth = $monthDate->format('Y-m'); @@ -298,12 +258,11 @@ public function updateFundedSpending(Category $category, string $month) 'user_id' => $category->user_id, 'funded_spending' => $fundedSpending, 'payments' => $payments ?? 0, - 'available' => DB::raw("$fundedSpending + available - $payments"), + 'available' => DB::raw("($fundedSpending + available) - $payments"), ]); - - echo "{$category->name} updated to {$activity}".PHP_EOL; + $fromBudgets = $fundedSpending - $payments; } - + return $fromBudgets; } public static function findOrCreateByName(CategoryData $params) diff --git a/app/Domains/Budget/Services/BudgetMonthService.php b/app/Domains/Budget/Services/BudgetMonthService.php index 429fd1b4..97f15bad 100644 --- a/app/Domains/Budget/Services/BudgetMonthService.php +++ b/app/Domains/Budget/Services/BudgetMonthService.php @@ -2,11 +2,8 @@ namespace App\Domains\Budget\Services; -use App\Domains\AppCore\Models\Category; -use App\Domains\Budget\Data\BudgetReservedNames; -use App\Domains\Budget\Models\BudgetMonth; -use App\Events\BudgetAssigned; use Illuminate\Support\Facades\DB; +use App\Domains\Budget\Models\BudgetMonth; class BudgetMonthService { @@ -14,56 +11,21 @@ public function __construct() { } - public function addTarget(Category $category, mixed $postData) - { - return $category->budget()->create(array_merge($postData, [ - 'name' => $category->name ?? $category->display_id, - 'team_id' => $category->team_id, - ])); - } - - public function updateTarget(Category $category, mixed $postData) - { - return $category->budget()->update(array_merge($postData, [ - 'name' => $category->name ?? $category->display_id, - ])); - } - - public static function getSavingsBalance($teamId, $startDate, $endDate) + public static function getSavingsBalance($teamId, $until, $from = null) { - $startMonth = substr((string) $startDate, 0, 7); - $endMonth = substr((string) $endDate, 0, 7); + $startDate = $from ? substr((string) $from, 0, 7) : null; + $endDate = substr((string) $until, 0, 7); return DB::query() ->where('budget_targets.team_id', $teamId) - ->whereIn('budget_targets.target_type', ['saving_balance']) - ->whereRaw("date_format(month, '%Y-%m') <= '$endMonth'") + ->whereIn('budget_targets.target_type', ['saving_balance', 'savings_monthly']) + ->whereRaw("date_format(month, '%Y-%m') <= '$endDate'") + ->when($from, fn ($q) => $q->whereRaw("date_format(month, '%Y-%m') >= '$startDate'")) ->from('budget_months') ->join('budget_targets', 'budget_targets.category_id', 'budget_months.category_id') ->sum(DB::raw('budgeted + activity')); } - public function assignBudget(Category $category, string $month, mixed $postData) - { - $amount = (float) $postData['budgeted']; - $type = $postData['type'] ?? 'budgeted'; - $shouldAggregate = $category->name === BudgetReservedNames::READY_TO_ASSIGN->value || $type === 'movement'; - - $month = BudgetMonth::updateOrCreate([ - 'category_id' => $category->id, - 'team_id' => $category->team_id, - 'month' => $month, - 'name' => $month, - ], [ - 'user_id' => $category->user_id, - 'budgeted' => $shouldAggregate ? DB::raw("budgeted + $amount") : $amount, - ]); - - BudgetAssigned::dispatch($month, $postData); - - return $month; - } - public function getMonthByCategory($category, string $month) { return BudgetMonth::where([ diff --git a/app/Domains/Budget/Services/BudgetMovementService.php b/app/Domains/Budget/Services/BudgetMovementService.php index 8dc3f8e7..71127e47 100644 --- a/app/Domains/Budget/Services/BudgetMovementService.php +++ b/app/Domains/Budget/Services/BudgetMovementService.php @@ -2,13 +2,14 @@ namespace App\Domains\Budget\Services; +use App\Events\BudgetAssigned; +use Illuminate\Support\Facades\DB; +use Insane\Journal\Models\Core\Category; +use App\Domains\Budget\Models\BudgetMonth; use App\Domains\Budget\Data\BudgetAssignData; +use App\Domains\Budget\Models\BudgetMovement; use App\Domains\Budget\Data\BudgetMovementData; use App\Domains\Budget\Data\BudgetReservedNames; -use App\Domains\Budget\Models\BudgetMonth; -use App\Domains\Budget\Models\BudgetMovement; -use Illuminate\Support\Facades\DB; -use Insane\Journal\Models\Core\Category; class BudgetMovementService { @@ -16,10 +17,11 @@ class BudgetMovementService const MODE_SUBTRACT = 'subtract'; - public function __construct(public BudgetCategoryService $budgetService) - { - } + public function __construct(public BudgetCategoryService $budgetService) {} + /** + * Based on the direction of the movement for each account add or subtract the amount + */ public function updateBalances(int $categoryId, BudgetMovement $movement, string $date, float $amount, $mode = null) { $month = substr($date, 0, 7).'-01'; @@ -31,10 +33,13 @@ public function updateBalances(int $categoryId, BudgetMovement $movement, string 'category_id' => $categoryId, 'month' => $month, 'name' => $month, - ], ['budgeted' => $mode ? DB::raw("budgeted $modeSign $amount") : $amount]); + ], [ + 'budgeted' => $mode ? DB::raw("budgeted $modeSign $amount") : $amount + ]); } - public function registerMovement(BudgetMovementData $data) + + public function registerMovement(BudgetMovementData $data, $quietly = false, $fromReadyToAssign = false) { $session = [ 'team_id' => $data->team_id, @@ -43,28 +48,32 @@ public function registerMovement(BudgetMovementData $data) $sourceId = $data->source_category_id ?? Category::findOrCreateByName($session, BudgetReservedNames::READY_TO_ASSIGN->value); $destinationId = $data->destination_category_id; + $destinationBalance = self::getBalanceOfCategory($destinationId, $data->date); $sourceCategoryBalance = self::getBalanceOfCategory($sourceId, $data->date); // 2000 - $amount = $data->amount > $sourceCategoryBalance->available ? $sourceCategoryBalance->available : $data->amount; // 2096 > 2000 ? 2000 : 2096 + $amount = $data->amount > $sourceCategoryBalance->available && !$fromReadyToAssign ? $sourceCategoryBalance->available : $data->amount; // 2096 > 2000 ? 2000 : 2096 + + $isPositiveTransaction = $amount > 0; $formData = array_merge($session, [ - 'source_category_id' => $sourceId, - 'destination_category_id' => $destinationId, - 'amount' => $amount, + 'source_category_id' => $isPositiveTransaction ? $sourceId : $destinationId, + 'destination_category_id' => $isPositiveTransaction ? $destinationId : $sourceId, + 'amount' => $destinationBalance->available - $amount, 'date' => $data->date, ]); DB::beginTransaction(); $savedMovement = BudgetMovement::create($formData); - if ($savedMovement->source_category_id) { + if ($savedMovement->source_category_id && !$quietly) { $this->updateBalances($destinationId, $savedMovement, $savedMovement->date, $amount, self::MODE_ADD); $this->updateBalances($sourceId, $savedMovement, $savedMovement->date, $amount, self::MODE_SUBTRACT); } DB::commit(); + BudgetAssigned::dispatch($data, $formData); } - public function registerAssignment(BudgetAssignData $data) + public function registerAssignment(BudgetAssignData $data, $quietly = false) { DB::beginTransaction(); $session = [ @@ -85,22 +94,16 @@ public function registerAssignment(BudgetAssignData $data) ]); $savedMovement = BudgetMovement::create($formData); - if ($savedMovement->source_category_id) { + if ($savedMovement->source_category_id && !$quietly) { $this->updateBalances($destinationId, $savedMovement, $savedMovement->date, $amount); $this->updateBalances($sourceId, $savedMovement, $savedMovement->date, $amount, self::MODE_SUBTRACT); } DB::commit(); + BudgetAssigned::dispatch($data, $formData); } public function getBalanceOfCategory($categoryId, string $month) { return (object) $this->budgetService->getBudgetInfo(Category::find($categoryId), $month); } - // public function getBalanceOfCategory($categoryId) { - // return DB::table('budget_movements') - // ->selectRaw("SUM(CASE WHEN source_category_id=$categoryId THEN amount * -1 ELSE amount * 1 END) as balance") - // ->where("source_category_id", $categoryId) - // ->orWhere("destination_category_id", $categoryId) - // ->first()->balance; - // } } diff --git a/app/Domains/Budget/Services/BudgetRolloverService.php b/app/Domains/Budget/Services/BudgetRolloverService.php index 2c12da8b..9c530df5 100644 --- a/app/Domains/Budget/Services/BudgetRolloverService.php +++ b/app/Domains/Budget/Services/BudgetRolloverService.php @@ -2,10 +2,12 @@ namespace App\Domains\Budget\Services; +use Brick\Money\Money; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; use Insane\Journal\Models\Core\Category; use App\Domains\Budget\Models\BudgetMonth; +use App\Domains\Budget\Data\BudgetAssignData; use App\Domains\Budget\Data\BudgetReservedNames; class BudgetRolloverService { @@ -19,32 +21,48 @@ public function rollMonth($teamId, $month, $categories = null) { ->whereNot('name', BudgetReservedNames::READY_TO_ASSIGN->value) ->get(); } - + $overspending = 0; + $fundedFromBudgets = 0; foreach ($categories as $category) { if($category->account_id) { - $this->budgetCategoryService->updateFundedSpending($category, $month); + $fundedFromBudgets += $this->budgetCategoryService->updateFundedSpending($category, $month); + } + $overspendingInCat = $this->setMonthBudget($category, $month); + if ($overspendingInCat < 0) { + $overspending += abs($overspendingInCat); } - $this->setNewMonthBudget($category, $month); } - $this->moveReadyToAssign($teamId, $month); + $this->moveReadyToAssign($teamId, $month, $overspending, $fundedFromBudgets); } - private function setNewMonthBudget($category, $month) { + private function setMonthBudget($category, $month) { $activity = (new BudgetCategoryService($category))->getCategoryActivity($category, $month); + $budgetMonth = BudgetMonth::where([ 'category_id' => $category->id, 'team_id' => $category->team_id, 'month' => $month, 'name' => $month, ])->first(); - $available = ($budgetMonth?->budgeted ?? 0) + ($budgetMonth->left_from_last_month ?? 0) + $activity; - $this->movePositiveAmounts($category, $month, $available); - } + if ($budgetMonth->category->account_id) { + $available = (- $budgetMonth->payments); + $available = Money::of($budgetMonth->funded_spending, $category->account->currency_code) + ->plus($budgetMonth->left_from_last_month) + ->minus(($budgetMonth->payments)) + ->getAmount() + ->toFloat(); + } else { + $available = ($budgetMonth?->budgeted ?? 0) + ($budgetMonth->left_from_last_month ?? 0) - abs($activity); + } + // close current month + $budgetMonth->update([ + 'activity' => $activity, + 'available' => $available, + ]); - private function movePositiveAmounts($category, $oldMonth, $available) { - $nextMonth = Carbon::createFromFormat("Y-m-d", $oldMonth)->addMonthsWithNoOverflow(1)->format('Y-m-d'); + $nextMonth = Carbon::createFromFormat("Y-m-d", $month)->addMonthsWithNoOverflow(1)->format('Y-m-d'); BudgetMonth::updateOrCreate([ 'category_id' => $category->id, 'team_id' => $category->team_id, @@ -53,11 +71,12 @@ private function movePositiveAmounts($category, $oldMonth, $available) { ], [ 'user_id' => $category->user_id, 'left_from_last_month' => $available ?? 0, - 'funded_spending_previous_month' => 0 ]); + return $available; + } - private function moveReadyToAssign($teamId, $month) { + private function moveReadyToAssign($teamId, $month, $overspending = 0, $fundedFromBudgets = 0) { $readyToAssignCategory = Category::where([ "name" => BudgetReservedNames::READY_TO_ASSIGN->value, "team_id" => $teamId @@ -65,15 +84,16 @@ private function moveReadyToAssign($teamId, $month) { $results = DB::table('budget_months') ->where([ - 'team_id' => $teamId, + 'budget_months.team_id' => $teamId, 'month' => $month, ])->whereNot('category_id', $readyToAssignCategory->id) + ->join('categories', 'categories.id', 'budget_months.category_id') ->selectRaw(" coalesce(sum(budgeted), 0) as budgeted, + coalesce(sum(activity), 0) as budgetsActivity, coalesce(sum(payments), 0) as payments, - coalesce(sum(funded_spending), 0) as funded_spending, - coalesce(sum(funded_spending_previous_month), 0) as funded_spending_previous_month - ") + coalesce(sum(funded_spending), 0) as funded_spending + ")->groupBy('month') ->first(); $budgetMonth = BudgetMonth::where([ @@ -83,9 +103,9 @@ private function moveReadyToAssign($teamId, $month) { 'name' => $month, ])->first(); - $activity = (new BudgetCategoryService($readyToAssignCategory))->getCategoryActivity($readyToAssignCategory, $month); - $activityPlusLeft = $activity + $budgetMonth->left_from_last_month; - $available = $activityPlusLeft - ($results?->budgeted ?? 0) ; + $inflow = (new BudgetCategoryService($readyToAssignCategory))->getCategoryActivity($readyToAssignCategory, $month); + $positiveAmount = $budgetMonth->left_from_last_month + $inflow + $results?->funded_spending; + $available = $positiveAmount - $results?->budgeted ; $nextMonth = Carbon::createFromFormat("Y-m-d", $month)->addMonthsWithNoOverflow(1)->format('Y-m-d'); @@ -98,8 +118,8 @@ private function moveReadyToAssign($teamId, $month) { ], [ 'user_id' => $readyToAssignCategory->user_id, 'budgeted' => $results?->budgeted, - 'activity' => $activity, - 'available' => $activityPlusLeft, + 'activity' => $inflow, + 'available' => $available, 'funded_spending' => $results?->funded_spending ?? 0, 'payments' => $results?->payments ?? 0, ]); @@ -112,11 +132,21 @@ private function moveReadyToAssign($teamId, $month) { 'name' => $nextMonth, ], [ 'user_id' => $readyToAssignCategory->user_id, - 'left_from_last_month' => $available ?? 0, - 'funded_spending_previous_month' => 0, + 'left_from_last_month' => $inflow - $results?->budgeted, + 'overspending_previous_month' => $available < 0 ? $available : $available, ]); } + private function fixBudgetMovements($budgetMonth) { + (new BudgetMovementService(new BudgetCategoryService()))->registerAssignment(new BudgetAssignData( + $budgetMonth->team_id, + $budgetMonth->user_id, + $budgetMonth->month, + $budgetMonth->category_id, + $budgetMonth->budgeted, + ), true); + } + private function reduceOverspent() { // If your category had been overspent in cash (negative red Available), that amount will be deducted from Ready to Assign in the new month. @@ -126,5 +156,30 @@ private function reduceOverspent() { // Not seeing an Underfunded Alert in your Credit Card Payment category? We're testing this new feature in stages and releasing it to everyone soon. } + public function startFrom($teamId, $yearMonth) { + $categories = Category::where([ + 'team_id' => $teamId, + ]) + ->whereNot('name', BudgetReservedNames::READY_TO_ASSIGN->value) + ->get(); + + + $monthsWithTransactions = DB::table('transaction_lines') + ->selectRaw("date_format(transaction_lines.date, '%Y-%m') AS date") + ->groupBy(DB::raw("date_format(transaction_lines.date, '%Y-%m')")) + ->whereRaw("date_format(transaction_lines.date, '%Y-%m') >= ?", [$yearMonth]) + ->get() + ->pluck('date'); + + $total = count($monthsWithTransactions); + $count = 0; + foreach ($monthsWithTransactions as $month) { + $count++; + $this->rollMonth($teamId, $month."-01", $categories); + echo "updated month {$month}".PHP_EOL; + echo "{$count} of {$total}".PHP_EOL.PHP_EOL; + } + } + // transactions with more than 3 days prior to the las recinciled transaction are not imported } diff --git a/app/Domains/Budget/Services/BudgetTargetService.php b/app/Domains/Budget/Services/BudgetTargetService.php index 9cdb471b..b9dac7d0 100644 --- a/app/Domains/Budget/Services/BudgetTargetService.php +++ b/app/Domains/Budget/Services/BudgetTargetService.php @@ -2,6 +2,8 @@ namespace App\Domains\Budget\Services; +use Exception; +use App\Models\User; use App\Domains\AppCore\Models\Category; use App\Domains\Budget\Models\BudgetTarget; use App\Domains\Integration\Concerns\PlannedTransactionDTO; @@ -31,6 +33,11 @@ public function createPlannedTransactions(int $teamId) } } + public function getNextBudgetItems($teamId) + { + return BudgetTarget::getNextTargets($teamId); + } + private function buildPlanned(BudgetTarget $target, $month) { $date = $month.'-'.$target->frequency_month_date; @@ -39,11 +46,37 @@ private function buildPlanned(BudgetTarget $target, $month) } public function complete(BudgetTarget $budgetTarget, Category $category, array $postData) { - $budgetTarget->update(array_merge( - $postData, [ - 'completed_at' => $postData['completed_at'] - ?? $this->budgetCategoryService->getLastTransactionMonth($category)?->month, - ])); + $budgetTarget->update([ + ...$postData, + 'completed_at' => $postData['completed_at'] ?? $this->budgetCategoryService->getLastTransactionMonth($category)?->month, + ]); + } + + public function update(Category $category, BudgetTarget $budgetTarget, User $user, $postData) { + if ($category->id !== $budgetTarget->category->id){ + throw new Exception(__("This target doent belongs to this category")); + } + + $budgetTarget->update([ + ...$postData, + 'team_id' => $user->current_team_id, + 'user_id' => $user->id, + 'name' => $category->name, + 'category_id' => $budgetTarget->category_id, + ]); + } + + public function add(Category $category,User $user, mixed $postData) + { + if ($category->team_id !== $user->current_team_id){ + throw new Exception(__("This category doent belongs to this team")); + } + return $category->budget()->create([ + ...$postData, + 'name' => $category->name ?? $category->display_id, + 'team_id' => $user->current_team_id, + "user_id" => $user->id + ]); } } diff --git a/app/Events/BudgetAssigned.php b/app/Events/BudgetAssigned.php index 7fe7eddb..4f9f391b 100644 --- a/app/Events/BudgetAssigned.php +++ b/app/Events/BudgetAssigned.php @@ -2,16 +2,18 @@ namespace App\Events; +use Illuminate\Queue\SerializesModels; use App\Domains\Budget\Models\BudgetMonth; -use Illuminate\Broadcasting\InteractsWithSockets; +use App\Domains\Budget\Data\BudgetAssignData; use Illuminate\Foundation\Events\Dispatchable; -use Illuminate\Queue\SerializesModels; +use App\Domains\Budget\Data\BudgetMovementData; +use Illuminate\Broadcasting\InteractsWithSockets; class BudgetAssigned { use Dispatchable, InteractsWithSockets, SerializesModels; - public BudgetMonth $monthBudget; + public BudgetAssignData|BudgetMovementData $budgetMonth; public mixed $postData; @@ -20,9 +22,9 @@ class BudgetAssigned * * @return void */ - public function __construct(BudgetMonth $monthBudget, mixed $postData = []) + public function __construct(BudgetAssignData|BudgetMovementData $budgetMonth, mixed $postData = []) { - $this->monthBudget = $monthBudget; + $this->budgetMonth = $budgetMonth; $this->postData = $postData; } } diff --git a/app/Http/Controllers/Finance/FinanceController.php b/app/Http/Controllers/Finance/FinanceController.php index 334dd050..0ffef160 100644 --- a/app/Http/Controllers/Finance/FinanceController.php +++ b/app/Http/Controllers/Finance/FinanceController.php @@ -2,17 +2,17 @@ namespace App\Http\Controllers\Finance; -use App\Domains\Budget\Models\BudgetMonth; -use App\Domains\Budget\Services\BudgetCategoryService; -use App\Domains\Transaction\Models\Transaction; -use App\Domains\Transaction\Services\PlannedTransactionService; -use App\Domains\Transaction\Services\TransactionService; -use App\Models\Setting; use Carbon\Carbon; -use Freesgen\Atmosphere\Http\InertiaController; -use Freesgen\Atmosphere\Http\Querify; +use App\Models\Setting; use Illuminate\Http\Request; use Laravel\Jetstream\Jetstream; +use Freesgen\Atmosphere\Http\Querify; +use App\Domains\Budget\Models\BudgetMonth; +use App\Domains\Transaction\Models\Transaction; +use Freesgen\Atmosphere\Http\InertiaController; +use App\Domains\Budget\Services\BudgetMonthService; +use App\Domains\Transaction\Services\TransactionService; +use App\Domains\Transaction\Services\PlannedTransactionService; class FinanceController extends InertiaController { @@ -48,7 +48,8 @@ public function index(Request $request) $lastMonthExpenses = TransactionService::getExpensesTotal($teamId, $lastMonthStartDate, $lastMonthEndDate); $income = TransactionService::getIncome($teamId, $startDate, $endDate); $lastMonthIncome = TransactionService::getIncome($teamId, $lastMonthStartDate, $lastMonthEndDate); - $savings = BudgetCategoryService::getSavingsBalance($teamId, $startDate, $endDate); + $savings = BudgetMonthService::getSavingsBalance($teamId, $endDate); + $savingsInMonth = BudgetMonthService::getSavingsBalance($teamId, $endDate, $startDate); return Jetstream::inertia()->render($request, 'Finance/Index', [ 'sectionTitle' => 'Finance', @@ -60,6 +61,7 @@ public function index(Request $request) 'lastMonthExpenses' => $lastMonthExpenses->total_amount, 'income' => $income, 'savings' => $savings, + 'savingsInMonth' => $savingsInMonth, 'lastMonthIncome' => $lastMonthIncome, 'transactions' => $transactions->map(function ($transaction) { return Transaction::parser($transaction); diff --git a/app/Http/Controllers/Finance/FinanceTransactionController.php b/app/Http/Controllers/Finance/FinanceTransactionController.php index 110fa5f5..8b3b87ce 100644 --- a/app/Http/Controllers/Finance/FinanceTransactionController.php +++ b/app/Http/Controllers/Finance/FinanceTransactionController.php @@ -2,18 +2,18 @@ namespace App\Http\Controllers\Finance; -use App\Domains\Transaction\Actions\FindLinkedTransactions; -use App\Domains\Transaction\Exports\TransactionExport; -use App\Domains\Transaction\Models\Transaction; -use App\Domains\Transaction\Resources\TransactionResource; -use App\Domains\Transaction\Services\PlannedTransactionService; -use App\Domains\Transaction\Services\TransactionService; -use App\Http\Controllers\Traits\QuerifySlim; -use Freesgen\Atmosphere\Http\InertiaController; -use Freesgen\Atmosphere\Http\Querify; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Maatwebsite\Excel\Facades\Excel; +use Freesgen\Atmosphere\Http\Querify; +use App\Http\Controllers\Traits\QuerifySlim; +use App\Domains\Transaction\Models\Transaction; +use Freesgen\Atmosphere\Http\InertiaController; +use App\Domains\Transaction\Exports\TransactionExport; +use App\Domains\Transaction\Services\TransactionService; +use App\Domains\Transaction\Resources\TransactionResource; +use App\Domains\Transaction\Actions\FindLinkedTransactions; +use App\Domains\Transaction\Services\PlannedTransactionService; class FinanceTransactionController extends InertiaController { @@ -27,7 +27,7 @@ public function __construct(Transaction $transaction, private PlannedTransaction $this->templates = [ 'index' => 'Finance/Transactions', ]; - $this->searchable = ['id', 'date']; + $this->searchable = ['transactions.description', 'date']; $this->sorts = ['-date']; $this->includes = [ 'mainLine', @@ -47,7 +47,7 @@ protected function list(Request $request) { $dates = $this->getFilterDates(); $query = new QuerifySlim([ - 'searchable' => ['id', 'date', 'description'], + 'searchable' => ['transactions.date', 'transactions.description'], 'sorts' => ['-date'], 'includes' => [ 'mainLine', diff --git a/app/Http/Controllers/System/DashboardController.php b/app/Http/Controllers/System/DashboardController.php index 3e7cfb37..1e201da9 100644 --- a/app/Http/Controllers/System/DashboardController.php +++ b/app/Http/Controllers/System/DashboardController.php @@ -9,6 +9,7 @@ use App\Http\Resources\PlannedMealResource; use App\Domains\Transaction\Services\ReportService; use App\Http\Controllers\Traits\HasEnrichedRequest; +use App\Domains\Budget\Services\BudgetTargetService; use App\Domains\Budget\Services\BudgetCategoryService; use App\Domains\Transaction\Services\TransactionService; @@ -16,7 +17,7 @@ class DashboardController { use HasEnrichedRequest; - public function __construct(private MealService $mealService) + public function __construct(private MealService $mealService, private BudgetTargetService $budgetTargetService) { } @@ -30,8 +31,7 @@ public function __invoke() $budget = BudgetMonth::getMonthAssignmentTotal($teamId, $startDate); $transactionsTotal = TransactionService::getExpensesTotal($teamId, $startDate, $endDate); $plannedMeals = $this->mealService->getMealSchedule($teamId); - - $nextPayments = BudgetCategoryService::getNextBudgetItems($teamId); + $nextPayments = $this->budgetTargetService->getNextBudgetItems($teamId); return inertia('Dashboard', [ 'sectionTitle' => 'Dashboard', diff --git a/app/Http/Controllers/Traits/QuerifySlim.php b/app/Http/Controllers/Traits/QuerifySlim.php index 98dc0ae0..579f0f0d 100644 --- a/app/Http/Controllers/Traits/QuerifySlim.php +++ b/app/Http/Controllers/Traits/QuerifySlim.php @@ -40,6 +40,8 @@ public function __construct(mixed $params) $this->includes = $params['includes']; $this->sorts = $params['sorts']; $this->filters = $params['filters']; + $this->searchable = $params['searchable']; + $this->validationRules = $params['validationRules'] ?? []; } public function getModelQuery($request, $tableName, $extendFunction = null) @@ -105,6 +107,7 @@ private function getSearch($search) $whereRaw = ''; // handle search foreach ($this->searchable as $field) { + // add search fields to where clause if (! $whereRaw) { $whereRaw .= "$field like '%$search%'"; diff --git a/app/Http/Resources/CategoryCollection.php b/app/Http/Resources/CategoryCollection.php index e93cc3f8..17d20ad7 100644 --- a/app/Http/Resources/CategoryCollection.php +++ b/app/Http/Resources/CategoryCollection.php @@ -27,7 +27,7 @@ public function toArray($request) return [ ...$normalArray, - ...$this->service->getBudgetInfo($this->resource, $month), + ...$this->service->getBudgetData($this->resource, $month), 'month' => $month ]; } diff --git a/app/Listeners/CreateBudgetMovement.php b/app/Listeners/CreateBudgetMovement.php index da99181b..2edd4c52 100644 --- a/app/Listeners/CreateBudgetMovement.php +++ b/app/Listeners/CreateBudgetMovement.php @@ -3,29 +3,22 @@ namespace App\Listeners; use App\Events\BudgetAssigned; +use App\Domains\Budget\Services\BudgetCategoryService; +use App\Domains\Budget\Services\BudgetRolloverService; class CreateBudgetMovement { protected $formData; - /** - * Create the event listener. - * - * @return void - */ + public function __construct() { } - /** - * Handle the event. - * - * @param object $event - * @return void - */ public function handle(BudgetAssigned $event) { // BudgetMovement::registerMovement($event->monthBudget, $this->formData); + (new BudgetRolloverService(new BudgetCategoryService()))->startFrom($event->budgetMonth->team_id, substr($event->budgetMonth->date, 0, 7)); } } diff --git a/app/Listeners/CreateBudgetTransactionMovement.php b/app/Listeners/CreateBudgetTransactionMovement.php index 3c84c81f..4cebd03c 100644 --- a/app/Listeners/CreateBudgetTransactionMovement.php +++ b/app/Listeners/CreateBudgetTransactionMovement.php @@ -2,9 +2,9 @@ namespace App\Listeners; -use App\Domains\Budget\Services\BudgetCategoryService; use Illuminate\Contracts\Queue\ShouldQueue; use Insane\Journal\Events\TransactionCreated; +use App\Domains\Budget\Services\BudgetCategoryService; class CreateBudgetTransactionMovement implements ShouldQueue { @@ -12,12 +12,6 @@ public function __construct(private BudgetCategoryService $budgetCategoryService { } - /** - * Handle the event. - * - * @param object $event - * @return void - */ public function handle(TransactionCreated $event) { $transaction = $event->transaction; @@ -32,5 +26,10 @@ public function handle(TransactionCreated $event) if ($transaction->category_id) { $this->budgetCategoryService->updateActivity($transaction->category, $month); } + + // if ($transaction->category) { + // $this->budgetCategoryService->updateMonthBalances($transaction->category, $month); + // } + } } diff --git a/app/Listeners/TrashTeamSettings.php b/app/Listeners/TrashTeamSettings.php index 612c85c0..8027d76c 100644 --- a/app/Listeners/TrashTeamSettings.php +++ b/app/Listeners/TrashTeamSettings.php @@ -14,7 +14,6 @@ use App\Domains\LogerProfile\Models\LogerProfileEntity; use App\Domains\Meal\Models\Ingredient; use App\Domains\Meal\Models\Meal; -use App\Domains\Meal\Models\MealMenu; use App\Domains\Meal\Models\MealPlan; use App\Domains\Meal\Models\MealType; use App\Domains\Meal\Models\Product; diff --git a/resources/js/Components/widgets/ChartComparison.vue b/resources/js/Components/widgets/ChartComparison.vue index 998748ea..ac3c404f 100644 --- a/resources/js/Components/widgets/ChartComparison.vue +++ b/resources/js/Components/widgets/ChartComparison.vue @@ -1,5 +1,5 @@