diff --git a/app/Models/Attachment.php b/app/Models/Attachment.php index 3bfefbc3..1f8de3db 100644 --- a/app/Models/Attachment.php +++ b/app/Models/Attachment.php @@ -3,6 +3,7 @@ namespace App\Models; use Exception; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Facades\Storage; @@ -32,23 +33,27 @@ public function transaction() } // Accessors - public function getFileB64Attribute() + protected function fileB64(): Attribute { - $file = Storage::get($this->file_path); + return Attribute::make(function () { + $file = Storage::get($this->file_path); - if (!$file) { - return null; - } + if (!$file) { + return null; + } - $type = pathinfo($this->file_path, PATHINFO_EXTENSION); + $type = pathinfo($this->file_path, PATHINFO_EXTENSION); - return 'data:image/' . $type . ';base64,' . base64_encode($file); + return 'data:image/' . $type . ';base64,' . base64_encode($file); + }); } - public function getFileTypeAttribute() + protected function fileType(): Attribute { - $parts = explode('.', $this->file_path); + return Attribute::make(function () { + $parts = explode('.', $this->file_path); - return $parts[count($parts) - 1]; + return $parts[count($parts) - 1]; + }); } } diff --git a/app/Models/Budget.php b/app/Models/Budget.php index 099d59c8..1ef53f6c 100644 --- a/app/Models/Budget.php +++ b/app/Models/Budget.php @@ -4,6 +4,7 @@ use App\Helper; use App\Repositories\BudgetRepository; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; @@ -32,18 +33,18 @@ public function tag() } // Accessors - public function getFormattedAmountAttribute() + protected function formattedAmount(): Attribute { - return Helper::formatNumber($this->amount / 100); + return Attribute::make(fn () => Helper::formatNumber($this->amount / 100)); } - public function getSpentAttribute() + protected function spent(): Attribute { - return (new BudgetRepository())->getSpentById($this->id); + return Attribute::make(fn () => (new BudgetRepository())->getSpentById($this->id)); } - public function getFormattedSpentAttribute() + protected function formattedSpent(): Attribute { - return Helper::formatNumber($this->spent / 100); + return Attribute::make(fn () => Helper::formatNumber($this->spent / 100)); } } diff --git a/app/Models/Currency.php b/app/Models/Currency.php index 0c48e842..34c9a281 100644 --- a/app/Models/Currency.php +++ b/app/Models/Currency.php @@ -19,7 +19,7 @@ class Currency extends Model // Accessors protected function isoLowercased(): Attribute { - return Attribute::make(fn (mixed $value, array $attributes) => strtolower($attributes['iso'])); + return Attribute::make(fn () => strtolower($this->iso)); } // Scopes diff --git a/app/Models/Earning.php b/app/Models/Earning.php index d0e84ccd..cec99160 100644 --- a/app/Models/Earning.php +++ b/app/Models/Earning.php @@ -5,6 +5,8 @@ use App\Events\TransactionCreated; use App\Events\TransactionDeleted; use App\Helper; +use Carbon\Carbon; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; @@ -28,16 +30,14 @@ class Earning extends Model ]; // Accessors - public function getFormattedAmountAttribute() + protected function formattedAmount(): Attribute { - return Helper::formatNumber($this->amount / 100); + return Attribute::make(fn () => Helper::formatNumber($this->amount / 100)); } - public function getFormattedHappenedOnAttribute() + protected function formattedHappenedOn(): Attribute { - $secondsDifference = strtotime(date('Y-m-d')) - strtotime($this->happened_on); - - return ($secondsDifference / 60 / 60 / 24) . ' days ago'; + return Attribute::make(fn () => Carbon::now()->diffInDays(Carbon::parse($this->happened_on)) . ' days ago'); } // Relations diff --git a/app/Models/Recurring.php b/app/Models/Recurring.php index bad4211c..b9d1e4fd 100644 --- a/app/Models/Recurring.php +++ b/app/Models/Recurring.php @@ -4,6 +4,7 @@ use App\Events\RecurringCreated; use App\Events\RecurringDeleted; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; @@ -33,22 +34,26 @@ class Recurring extends Model ]; // Accessors - public function getDueDaysAttribute() + protected function dueDays(): Attribute { - if ($this->starts_on <= date('Y-m-d') && ($this->ends_on >= date('Y-m-d') || !$this->ends_on)) { - if (date('j') > $this->day) { - return date('t') - date('j') + $this->day; - } + return Attribute::make(function () { + if ($this->starts_on <= date('Y-m-d') && ($this->ends_on >= date('Y-m-d') || !$this->ends_on)) { + if (date('j') > $this->day) { + return date('t') - date('j') + $this->day; + } - return $this->day - date('j'); - } + return $this->day - date('j'); + } - return 0; + return 0; + }); } - public function getStatusAttribute() + protected function status(): Attribute { - return $this->starts_on <= date('Y-m-d') && ($this->ends_on >= date('Y-m-d') || !$this->ends_on); + return Attribute::make(function () { + return $this->starts_on <= date('Y-m-d') && ($this->ends_on >= date('Y-m-d') || !$this->ends_on); + }); } // Relations diff --git a/app/Models/Space.php b/app/Models/Space.php index 8adc0ade..e0872784 100644 --- a/app/Models/Space.php +++ b/app/Models/Space.php @@ -2,6 +2,7 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; @@ -65,9 +66,9 @@ public function activities() } // Accessors - public function getAbbreviatedNameAttribute(): string + protected function abbreviatedName(): Attribute { - return Str::limit($this->name, 3); + return Attribute::make(fn () => Str::limit($this->name, 3)); } // diff --git a/app/Models/SpaceInvite.php b/app/Models/SpaceInvite.php index 26214109..262bb378 100644 --- a/app/Models/SpaceInvite.php +++ b/app/Models/SpaceInvite.php @@ -2,6 +2,7 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -38,20 +39,22 @@ public function inviter() } // Accessors - public function getStatusAttribute(): string + protected function status(): Attribute { - if ($this->accepted === null) { - return 'Pending'; - } + return Attribute::make(function () { + if ($this->accepted === null) { + return 'Pending'; + } - if ($this->accepted === true) { - return 'Accepted (' . date('d-m', strtotime($this->updated_at)) . ')'; - } + if ($this->accepted === true) { + return 'Accepted (' . date('d-m', strtotime($this->updated_at)) . ')'; + } - if ($this->accepted === false) { - return 'Denied (' . date('d-m', strtotime($this->updated_at)) . ')'; - } + if ($this->accepted === false) { + return 'Denied (' . date('d-m', strtotime($this->updated_at)) . ')'; + } - return 'Unknown'; + return 'Unknown'; + }); } } diff --git a/app/Models/Spending.php b/app/Models/Spending.php index 4aebf795..46c5da37 100644 --- a/app/Models/Spending.php +++ b/app/Models/Spending.php @@ -5,6 +5,8 @@ use App\Events\TransactionCreated; use App\Events\TransactionDeleted; use App\Helper; +use Carbon\Carbon; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; @@ -30,16 +32,14 @@ class Spending extends Model ]; // Accessors - public function getFormattedAmountAttribute() + protected function formattedAmount(): Attribute { - return Helper::formatNumber($this->amount / 100); + return Attribute::make(fn () => Helper::formatNumber($this->amount / 100)); } - public function getFormattedHappenedOnAttribute() + protected function formattedHappenedOn(): Attribute { - $secondsDifference = strtotime(date('Y-m-d')) - strtotime($this->happened_on); - - return ($secondsDifference / 60 / 60 / 24) . ' days ago'; + return Attribute::make(fn () => Carbon::now()->diffInDays(Carbon::parse($this->happened_on)) . ' days ago'); } // Relations diff --git a/app/Models/User.php b/app/Models/User.php index ff4d6c18..1802c9db 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,6 +2,7 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Notifications\Notifiable; use Illuminate\Foundation\Auth\User as Authenticatable; @@ -49,9 +50,11 @@ public static function getValidationRulesForPasswordReset(): array } // Accessors - public function getAvatarAttribute($avatar) + protected function avatar(): Attribute { - return $avatar ? '/storage/avatars/' . $avatar : 'https://via.placeholder.com/250'; + return Attribute::make( + fn (?string $value) => $value ? '/storage/avatars/' . $value : 'https://via.placeholder.com/250' + ); } // Relations diff --git a/app/Models/Widget.php b/app/Models/Widget.php index acb50c61..c8162520 100644 --- a/app/Models/Widget.php +++ b/app/Models/Widget.php @@ -5,6 +5,7 @@ use App\Exceptions\WidgetInvalidPropertyValueException; use App\Exceptions\WidgetMissingPropertyException; use App\Exceptions\WidgetUnknownTypeException; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; @@ -30,23 +31,17 @@ public function user() * Accessors */ - public function getPropertiesAttribute($value) - { - return json_decode($value); - } - - /** - * Mutators - */ - - public function setPropertiesAttribute($value) + protected function properties(): Attribute { /** * Using accessor and mutator for this attribute because the "object" cast * doesn't use JSON_FORCE_OBJECT */ - $this->attributes['properties'] = json_encode($value, JSON_FORCE_OBJECT); + return Attribute::make( + get: fn ($value) => json_decode($value), + set: fn ($value) => json_encode($value, JSON_FORCE_OBJECT), + ); } /** diff --git a/tests/Unit/Models/BudgetTest.php b/tests/Unit/Models/BudgetTest.php new file mode 100644 index 00000000..1f5f7a84 --- /dev/null +++ b/tests/Unit/Models/BudgetTest.php @@ -0,0 +1,82 @@ +create(['amount' => 750]); + + $this->assertEquals('7.50', $budget->formatted_amount); + } + + public function testSpentAttribute(): void + { + $space = Space::factory() + ->create(); + + $tag = Tag::factory() + ->create([ + 'space_id' => $space->id, + ]); + + $budget = Budget::factory() + ->create([ + 'tag_id' => $tag->id, + 'period' => 'monthly', + 'amount' => 750, + ]); + + Spending::factory() + ->create([ + 'space_id' => $space->id, + 'tag_id' => $tag->id, + 'happened_on' => date('Y-m-d'), + 'amount' => 100, + ]); + + // Set session accordingly + $this->withSession(['space_id' => $space->id]); + + $this->assertEquals(100, $budget->spent); + } + + public function testFormattedSpentAttribute(): void + { + $space = Space::factory() + ->create(); + + $tag = Tag::factory() + ->create([ + 'space_id' => $space->id, + ]); + + $budget = Budget::factory() + ->create([ + 'tag_id' => $tag->id, + 'period' => 'monthly', + 'amount' => 750, + ]); + + Spending::factory() + ->create([ + 'space_id' => $space->id, + 'tag_id' => $tag->id, + 'happened_on' => date('Y-m-d'), + 'amount' => 100, + ]); + + // Set session accordingly + $this->withSession(['space_id' => $space->id]); + + $this->assertEquals('1.00', $budget->formatted_spent); + } +} diff --git a/tests/Unit/Models/EarningTest.php b/tests/Unit/Models/EarningTest.php index 7cc4d770..b3295997 100644 --- a/tests/Unit/Models/EarningTest.php +++ b/tests/Unit/Models/EarningTest.php @@ -2,18 +2,31 @@ namespace Tests\Unit\Models; -use App\Helper; +use Carbon\Carbon; use Tests\TestCase; use App\Models\Earning; class EarningTest extends TestCase { - public function testFormattedAmount() + public function testFormattedAmountAttribute(): void { - $earning = Earning::factory()->make([ - 'amount' => Helper::rawNumberToInteger(39) - ]); + $earning = Earning::factory() + ->make([ + 'amount' => 3900, + ]); $this->assertEquals('39.00', $earning->formatted_amount); } + + public function testFormattedHappenedOnAttribute(): void + { + $earning = Earning::factory() + ->make([ + 'happened_on' => '2020-01-01', + ]); + + Carbon::setTestNow('2024-01-01'); + + $this->assertEquals('1461 days ago', $earning->formatted_happened_on); + } } diff --git a/tests/Unit/Models/SpaceInviteTest.php b/tests/Unit/Models/SpaceInviteTest.php new file mode 100644 index 00000000..e3950333 --- /dev/null +++ b/tests/Unit/Models/SpaceInviteTest.php @@ -0,0 +1,33 @@ +make([ + 'accepted' => null, + ]); + + $this->assertEquals('Pending', $pendingSpaceInvite->status); + + $acceptedSpaceInvite = SpaceInvite::factory() + ->make([ + 'accepted' => true, + ]); + + $this->assertStringStartsWith('Accepted', $acceptedSpaceInvite->status); + + $deniedSpaceInvite = SpaceInvite::factory() + ->make([ + 'accepted' => false, + ]); + + $this->assertStringStartsWith('Denied', $deniedSpaceInvite->status); + } +} diff --git a/tests/Unit/Models/SpaceTest.php b/tests/Unit/Models/SpaceTest.php index 1b680ca2..357cfa0f 100644 --- a/tests/Unit/Models/SpaceTest.php +++ b/tests/Unit/Models/SpaceTest.php @@ -10,6 +10,16 @@ class SpaceTest extends TestCase { + public function testAbbreviatedNameAttribute(): void + { + $space = Space::factory() + ->create([ + 'name' => 'Hello world', + ]); + + $this->assertEquals('Hel...', $space->abbreviated_name); + } + public function testMonthlyBalance() { $space = Space::factory()->create(); diff --git a/tests/Unit/Models/SpendingTest.php b/tests/Unit/Models/SpendingTest.php index 104ee568..f97bc61b 100644 --- a/tests/Unit/Models/SpendingTest.php +++ b/tests/Unit/Models/SpendingTest.php @@ -2,18 +2,31 @@ namespace Tests\Unit\Models; -use App\Helper; +use Carbon\Carbon; use Tests\TestCase; use App\Models\Spending; class SpendingTest extends TestCase { - public function testFormattedAmount() + public function testFormattedAmountAttribute(): void { - $spending = Spending::factory()->make([ - 'amount' => Helper::rawNumberToInteger(92.35) - ]); + $spending = Spending::factory() + ->make([ + 'amount' => 9235, + ]); $this->assertEquals('92.35', $spending->formatted_amount); } + + public function testFormattedHappenedOnAttribute(): void + { + $spending = Spending::factory() + ->make([ + 'happened_on' => '2020-01-01', + ]); + + Carbon::setTestNow('2024-01-01'); + + $this->assertEquals('1461 days ago', $spending->formatted_happened_on); + } } diff --git a/tests/Unit/Models/UserTest.php b/tests/Unit/Models/UserTest.php new file mode 100644 index 00000000..00d53b10 --- /dev/null +++ b/tests/Unit/Models/UserTest.php @@ -0,0 +1,26 @@ +create([ + 'avatar' => null, + ]); + + $this->assertEquals('https://via.placeholder.com/250', $userWithoutAvatar->avatar); + + $userWithAvatar = User::factory() + ->create([ + 'avatar' => 'foo.png', + ]); + + $this->assertEquals('/storage/avatars/foo.png', $userWithAvatar->avatar); + } +}