-
Notifications
You must be signed in to change notification settings - Fork 1
fix(backend): Correctly calculate spendable potato #345
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -48,6 +48,8 @@ class User extends Entity | |
| public const ROLE_USER = 'user'; | ||
| public const ROLE_SERVICE = 'service'; | ||
|
|
||
| public const SPENDING_LIMIT_90_DAYS = 500; | ||
|
|
||
| /** | ||
| * @param array<string, bool>|null $notifications The user's notification settings. | ||
| * @return array<string, bool> | ||
|
|
@@ -209,24 +211,46 @@ public function potatoResetInMinutes(): string | |
| */ | ||
| public function spendablePotato(): int | ||
| { | ||
| $pruchasesTable = $this->fetchTable('Purchases'); | ||
| $purchasesTable = $this->fetchTable('Purchases'); | ||
|
|
||
| $query = $pruchasesTable->find(); | ||
| $result = $query | ||
| // Calculate total spent by this user (all time) | ||
| $totalSpentQuery = $purchasesTable->find(); | ||
| $totalSpentResult = $totalSpentQuery | ||
| ->select([ | ||
| 'spent' => $query->func()->sum('price'), | ||
| 'spent' => $totalSpentQuery->func()->sum('price'), | ||
| ]) | ||
| ->where([ | ||
| 'user_id' => $this->id, | ||
| ]) | ||
| ->first(); | ||
| $totalSpent = (int)$totalSpentResult->spent; | ||
|
|
||
| // @FIXME make this based on 90 days | ||
| if ((int)$result->spent >= 500) { | ||
| // Calculate spent in the last 90 days | ||
| $ninetyDaysAgo = DateTime::now()->subDays(90); | ||
| $last90DaysQuery = $purchasesTable->find(); | ||
| $last90DaysResult = $last90DaysQuery | ||
| ->select([ | ||
| 'spent' => $last90DaysQuery->func()->sum('price'), | ||
| ]) | ||
| ->where([ | ||
| 'user_id' => $this->id, | ||
| 'created >=' => $ninetyDaysAgo, | ||
| ]) | ||
| ->first(); | ||
| $spentLast90Days = (int)$last90DaysResult->spent; | ||
|
|
||
| // Check if the user has reached the spending limit in the last 90 days | ||
| if ($spentLast90Days >= self::SPENDING_LIMIT_90_DAYS) { | ||
| return 0; | ||
| } | ||
|
|
||
| return $this->potatoReceived() - (int)$result->spent; | ||
| // Calculate available balance (total received minus total spent) | ||
| $availableBalance = $this->potatoReceived() - $totalSpent; | ||
|
|
||
| // Return the minimum of available balance or remaining 90-day limit | ||
| $remainingLimit = self::SPENDING_LIMIT_90_DAYS - $spentLast90Days; | ||
|
|
||
| return min($availableBalance, $remainingLimit); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Negative Spendable Balance ErrorThe |
||
| } | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| <?php | ||
| declare(strict_types=1); | ||
|
|
||
| namespace App\Test\Fixture; | ||
|
|
||
| use Cake\TestSuite\Fixture\TestFixture; | ||
|
|
||
| /** | ||
| * PurchasesFixture | ||
| */ | ||
| class PurchasesFixture extends TestFixture | ||
| { | ||
| /** | ||
| * Records | ||
| * | ||
| * @var array<array<string, mixed>> | ||
| */ | ||
| public array $records = [ | ||
| // User 1 purchases | ||
| [ | ||
| 'id' => 1, | ||
| 'user_id' => '00000000-0000-0000-0000-000000000001', | ||
| 'presentee_id' => null, | ||
| 'name' => 'Old Purchase', | ||
| 'description' => 'Purchase from 120 days ago', | ||
| 'image_link' => 'https://example.com/old.jpg', | ||
| 'message' => null, | ||
| 'price' => 100, | ||
| 'code' => null, | ||
| 'created' => '2025-05-15 10:00:00', // ~120 days ago from Sept 12, 2025 | ||
| ], | ||
| [ | ||
| 'id' => 2, | ||
| 'user_id' => '00000000-0000-0000-0000-000000000001', | ||
| 'presentee_id' => null, | ||
| 'name' => 'Recent Purchase 1', | ||
| 'description' => 'Purchase from 30 days ago', | ||
| 'image_link' => 'https://example.com/recent1.jpg', | ||
| 'message' => null, | ||
| 'price' => 200, | ||
| 'code' => null, | ||
| 'created' => '2025-08-13 10:00:00', // ~30 days ago | ||
| ], | ||
| [ | ||
| 'id' => 3, | ||
| 'user_id' => '00000000-0000-0000-0000-000000000001', | ||
| 'presentee_id' => null, | ||
| 'name' => 'Recent Purchase 2', | ||
| 'description' => 'Purchase from 10 days ago', | ||
| 'image_link' => 'https://example.com/recent2.jpg', | ||
| 'message' => null, | ||
| 'price' => 150, | ||
| 'code' => null, | ||
| 'created' => '2025-09-02 10:00:00', // ~10 days ago | ||
| ], | ||
| // User 2 purchases (at spending limit) | ||
| [ | ||
| 'id' => 4, | ||
| 'user_id' => '00000000-0000-0000-0000-000000000002', | ||
| 'presentee_id' => null, | ||
| 'name' => 'Max Purchase 1', | ||
| 'description' => 'Purchase from 60 days ago', | ||
| 'image_link' => 'https://example.com/max1.jpg', | ||
| 'message' => null, | ||
| 'price' => 300, | ||
| 'code' => null, | ||
| 'created' => '2025-07-14 10:00:00', // ~60 days ago | ||
| ], | ||
| [ | ||
| 'id' => 5, | ||
| 'user_id' => '00000000-0000-0000-0000-000000000002', | ||
| 'presentee_id' => null, | ||
| 'name' => 'Max Purchase 2', | ||
| 'description' => 'Purchase from 20 days ago', | ||
| 'image_link' => 'https://example.com/max2.jpg', | ||
| 'message' => null, | ||
| 'price' => 200, | ||
| 'code' => null, | ||
| 'created' => '2025-08-23 10:00:00', // ~20 days ago | ||
| ], | ||
| // User 3 has no purchases | ||
| ]; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Timezone Mismatch in 90-Day Calculation
The
spendablePotatomethod calculates the 90-day spending window usingDateTime::now()->subDays(90). This implicitly uses the server's default timezone, which can cause incorrect 90-day period calculations against UTC database timestamps. Other methods in this class explicitly handle timezones, making this an inconsistency.