From d7702c334a46e3daf07a841984a2cac653764244 Mon Sep 17 00:00:00 2001 From: Rob Jackson Date: Fri, 3 Jan 2025 03:16:50 +0000 Subject: [PATCH 1/3] Add commands to recalculate all user balances --- .../Commands/RecalculateAllBalances.php | 39 ++++++ app/Entities/Payment.php | 16 ++- app/Jobs/RecalculateBalance.php | 34 +++++ database/factories/PaymentFactory.php | 60 +++++++++ tests/unit/Jobs/RecalculateBalanceTest.php | 121 ++++++++++++++++++ 5 files changed, 268 insertions(+), 2 deletions(-) create mode 100644 app/Console/Commands/RecalculateAllBalances.php create mode 100644 app/Jobs/RecalculateBalance.php create mode 100644 database/factories/PaymentFactory.php create mode 100644 tests/unit/Jobs/RecalculateBalanceTest.php diff --git a/app/Console/Commands/RecalculateAllBalances.php b/app/Console/Commands/RecalculateAllBalances.php new file mode 100644 index 00000000..b83c0399 --- /dev/null +++ b/app/Console/Commands/RecalculateAllBalances.php @@ -0,0 +1,39 @@ +active(); + }) + ->orWhere(function ($query) { + $query->recentlyLapsed(); + }) + ->get(); + + foreach ($users as $user) { + if ($this->option('apply')) { + RecalculateBalanceJob::dispatch($user); + $this->info("Queued a job to recalculate the cash_balance of {$user->name} ({$user->id})."); + } else { + $this->info("Would have queued a job to recalculate the cash_balance of {$user->name} ({$user->id})."); + } + } + } +} diff --git a/app/Entities/Payment.php b/app/Entities/Payment.php index 9b61503a..065a9bc4 100755 --- a/app/Entities/Payment.php +++ b/app/Entities/Payment.php @@ -1,4 +1,6 @@ -user = $user; + } + + public function handle(Credit $bbCredit) + { + $oldBalance = $this->user->cash_balance; + + $bbCredit->setUserId($this->user->id); + $bbCredit->recalculate(); + + $newBlalance = $this->user->fresh()->cash_balance; + \Log::debug("Recalculated balance for user {$this->user->id}. Balance changed from {$oldBalance} to {$newBlalance}"); + } +} diff --git a/database/factories/PaymentFactory.php b/database/factories/PaymentFactory.php new file mode 100644 index 00000000..c0ff1f4f --- /dev/null +++ b/database/factories/PaymentFactory.php @@ -0,0 +1,60 @@ +define(Payment::class, function (Faker $faker) { + return [ + 'source' => $faker->randomElement([ + 'gocardless', + 'gocardless-variable', + 'snackspace', + 'balance', + 'cash', + 'gift', + ]), + 'source_id' => $faker->randomNumber(), + 'user_id' => $faker->randomNumber(), + 'amount' => $faker->randomFloat(2, 1, 1000), + 'fee' => $faker->randomFloat(2, 0, 100), + 'amount_minus_fee' => function (array $payment) { + return $payment['amount'] - $payment['fee']; + }, + 'status' => $faker->randomElement([ + Payment::STATUS_PENDING, + Payment::STATUS_PENDING_SUBMISSION, + Payment::STATUS_CANCELLED, + Payment::STATUS_PAID, + Payment::STATUS_WITHDRAWN + ]), + 'reason' => $faker->randomElement(array_keys(Payment::getPaymentReasons())), + 'created_at' => $faker->dateTime, + 'reference' => $faker->uuid, + 'paid_at' => $faker->dateTime, + ]; +}); + +$factory->state(Payment::class, 'pending', [ + 'status' => Payment::STATUS_PENDING, +]); + +$factory->state(Payment::class, 'pending_submission', [ + 'status' => Payment::STATUS_PENDING_SUBMISSION, +]); + +$factory->state(Payment::class, 'cancelled', [ + 'status' => Payment::STATUS_CANCELLED, +]); + +$factory->state(Payment::class, 'paid', [ + 'status' => Payment::STATUS_PAID, +]); +$factory->state(Payment::class, 'fromCash', [ + 'source' => 'cash', +]); + +$factory->state(Payment::class, 'fromBalance', [ + 'source' => 'balance', +]); \ No newline at end of file diff --git a/tests/unit/Jobs/RecalculateBalanceTest.php b/tests/unit/Jobs/RecalculateBalanceTest.php new file mode 100644 index 00000000..848c0c1b --- /dev/null +++ b/tests/unit/Jobs/RecalculateBalanceTest.php @@ -0,0 +1,121 @@ +create(); + + RecalculateBalance::dispatchNow($user); + + $this->assertEquals(0, $user->fresh()->cash_balance); + } + + public function testUserBalanceZeroWithPayments() + { + $user = factory(User::class)->create(); + + factory(Payment::class, 2)->states(['fromCash', 'paid'])->create([ + 'reason' => 'balance', + 'user_id' => $user->id, + 'amount' => 20, + ]); + factory(Payment::class, 4)->states(['fromBalance', 'paid'])->create([ + + 'reason' => 'not-balance', + 'user_id' => $user->id, + 'amount' => 10, + ]); + + RecalculateBalance::dispatchNow($user); + + $this->assertEquals(0, $user->fresh()->cash_balance); + } + + public function testUserWithPositiveBalance() + { + $user = factory(User::class)->create(); + + factory(Payment::class, 2)->states(['fromCash', 'paid'])->create([ + 'reason' => 'balance', + 'user_id' => $user->id, + 'amount' => 20, + ]); + factory(Payment::class, 3)->states(['fromBalance', 'paid'])->create([ + + 'reason' => 'not-balance', + 'user_id' => $user->id, + 'amount' => 10, + ]); + RecalculateBalance::dispatchNow($user); + + $this->assertEquals(1000, $user->fresh()->cash_balance); + } + + public function testUserWithNegativeBalance() + { + $user = factory(User::class)->create(); + + factory(Payment::class, 2)->states(['fromCash', 'paid'])->create([ + 'reason' => 'balance', + 'user_id' => $user->id, + 'amount' => 20, + ]); + factory(Payment::class, 6)->states(['fromBalance', 'paid'])->create([ + + 'reason' => 'not-balance', + 'user_id' => $user->id, + 'amount' => 10, + ]); + RecalculateBalance::dispatchNow($user); + + $this->assertEquals(-2000, $user->fresh()->cash_balance); + } + + public function testIncludesPendingPaymentsCancelledPayments() + { + $user = factory(User::class)->create(); + + factory(Payment::class)->states(['fromCash', 'pending'])->create([ + 'reason' => 'balance', + 'user_id' => $user->id, + 'amount' => 20, + ]); + + factory(Payment::class)->states(['fromBalance', 'paid'])->create([ + 'reason' => 'not-balance', + 'user_id' => $user->id, + 'amount' => 20, + ]); + + RecalculateBalance::dispatchNow($user); + + $this->assertEquals(0, $user->fresh()->cash_balance); + } + + public function testIgnoresCancelledPayments() + { + $user = factory(User::class)->create(); + + factory(Payment::class)->states(['fromCash', 'cancelled'])->create([ + 'reason' => 'balance', + 'user_id' => $user->id, + 'amount' => 20, + ]); + + factory(Payment::class)->states(['fromBalance', 'paid'])->create([ + 'reason' => 'not-balance', + 'user_id' => $user->id, + 'amount' => 20, + ]); + + RecalculateBalance::dispatchNow($user); + + $this->assertEquals(-2000, $user->fresh()->cash_balance); + } +} From e45db07f6d197ea404ed1442c0bc89fcc2ac2c1e Mon Sep 17 00:00:00 2001 From: Rob Jackson Date: Fri, 3 Jan 2025 04:10:51 +0000 Subject: [PATCH 2/3] Add a summary of member balance states --- .../Controllers/PaymentBalancesController.php | 36 +++++++++ .../views/payment_balances/index.blade.php | 77 +++++++++++++++++++ .../views/payments/partials/tabs.blade.php | 3 + routes/web.php | 1 + 4 files changed, 117 insertions(+) create mode 100644 app/Http/Controllers/PaymentBalancesController.php create mode 100644 resources/views/payment_balances/index.blade.php diff --git a/app/Http/Controllers/PaymentBalancesController.php b/app/Http/Controllers/PaymentBalancesController.php new file mode 100644 index 00000000..9e789606 --- /dev/null +++ b/app/Http/Controllers/PaymentBalancesController.php @@ -0,0 +1,36 @@ +where('cash_balance', '>', 0)->count(); + $activeUsersInCreditSum = User::where('active', true)->where('cash_balance', '>', 0)->sum('cash_balance') / 100; + + $activeUsersInDebtQty = User::where('active', true)->where('cash_balance', '<', 0)->count(); + $activeUsersInDebtSum = User::where('active', true)->where('cash_balance', '<', 0)->sum('cash_balance') / 100; + + $inactiveUsersInCreditQty = User::where('active', false)->where('cash_balance', '>', 0)->count(); + $inactiveUsersInCreditSum = User::where('active', false)->where('cash_balance', '>', 0)->sum('cash_balance') / 100; + $inactiveUsersInDebtQty = User::where('active', false)->where('cash_balance', '<', 0)->count(); + $inactiveUsersInDebtSum = User::where('active', false)->where('cash_balance', '<', 0)->sum('cash_balance') / 100; + + $users = User::where('cash_balance', '!=', 0)->get(); + + return \View::make('payment_balances.index')->with([ + 'activeUsersInCreditQty' => $activeUsersInCreditQty, + 'activeUsersInCreditSum' => $activeUsersInCreditSum, + 'activeUsersInDebtQty' => $activeUsersInDebtQty, + 'activeUsersInDebtSum' => $activeUsersInDebtSum, + 'inactiveUsersInCreditQty' => $inactiveUsersInCreditQty, + 'inactiveUsersInCreditSum' => $inactiveUsersInCreditSum, + 'inactiveUsersInDebtQty' => $inactiveUsersInDebtQty, + 'inactiveUsersInDebtSum' => $inactiveUsersInDebtSum, + 'users' => $users, + ]); + } +} diff --git a/resources/views/payment_balances/index.blade.php b/resources/views/payment_balances/index.blade.php new file mode 100644 index 00000000..3ed09a96 --- /dev/null +++ b/resources/views/payment_balances/index.blade.php @@ -0,0 +1,77 @@ +@extends('layouts.main') + +@section('meta-title') + Balances overview +@stop + +@section('page-title') + Balances overview +@stop + +@section('main-tab-bar') + @include('payments.partials.tabs') +@stop + +@section('content') + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 In creditIn debt
 No. usersSum of balancesNo. usersSum of balances
Active members{{ $activeUsersInCreditQty }}£{{ number_format($activeUsersInCreditSum, 2) }}{{ $activeUsersInDebtQty }}£{{ number_format($activeUsersInDebtSum, 2) }}
Left members{{ $inactiveUsersInCreditQty }}£{{ number_format($inactiveUsersInCreditSum, 2) }}{{ $inactiveUsersInDebtQty }}£{{ number_format($inactiveUsersInDebtSum, 2) }}
+ + + + + + + + + + + + @foreach ($users as $user) + + + + + + + @endforeach + +
Member nameMember StatusBalance status (in debt or in credit)Balance amount
+ {{ $user->name }} + {!! HTML::statusLabel($user->status) !!} + @if ($user->cash_balance < 0) + In debt + @else + In credit + @endif + {{ $user->present()->cashBalance }}
+@stop diff --git a/resources/views/payments/partials/tabs.blade.php b/resources/views/payments/partials/tabs.blade.php index 5a04be6d..2bc569d7 100644 --- a/resources/views/payments/partials/tabs.blade.php +++ b/resources/views/payments/partials/tabs.blade.php @@ -12,5 +12,8 @@
  • {!! link_to_route('payments.possible-duplicates', 'Possible Duplicates') !!}
  • +
  • + {!! link_to_route('payments.balances', 'Balances') !!} +
  • \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index e96d0bed..e3f01f1c 100755 --- a/routes/web.php +++ b/routes/web.php @@ -90,6 +90,7 @@ Route::group(array('middleware' => 'role:finance'), function () { Route::resource('payments', 'PaymentController', ['only' => ['index', 'destroy', 'update']]); Route::get('payments/overview', ['uses' => 'PaymentOverviewController@index', 'as' => 'payments.overview']); + Route::get('payments/balances', ['uses' => 'PaymentBalancesController@index', 'as' => 'payments.balances']); Route::get('payments/sub-charges', ['as' => 'payments.sub-charges', 'uses' => 'SubscriptionController@listCharges']); Route::get('payments/possible-duplicates', ['as' => 'payments.possible-duplicates', 'uses' => 'PaymentController@possibleDuplicates']); }); From 46c8985897d7e9f4430fd6ec9a182ad13aba3ba4 Mon Sep 17 00:00:00 2001 From: Rob Jackson Date: Fri, 3 Jan 2025 04:11:05 +0000 Subject: [PATCH 3/3] Fix whoops not always rendering exceptions correctly --- app/Exceptions/Handler.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 9b1157a9..6a7da482 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -145,8 +145,7 @@ public function render($request, Exception $e) } if (config('app.debug') && $this->shouldReport($e) && !$request->wantsJson()) { - // @phpstan-ignore-next-line - return $this->renderExceptionWithWhoops($e); + return parent::render($request, $e); } if ($request->wantsJson()) {