Skip to content

Commit

Permalink
Merge pull request #73 from hisabi-app/feat/budget-system
Browse files Browse the repository at this point in the history
[beta] feature: add budget backend logic
  • Loading branch information
saleem-hadad authored Apr 22, 2024
2 parents 009b19d + d21cb1f commit 9a33e1c
Show file tree
Hide file tree
Showing 8 changed files with 269 additions and 3 deletions.
86 changes: 86 additions & 0 deletions app/Models/Budget.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

namespace App\Models;

use Carbon\CarbonPeriod;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Budget extends Model
{
use HasFactory;

const CUSTOM = "CUSTOM";
const DAILY = "DAILY";
const WEEKLY = "WEEKLY";
const MONTHLY = "MONTHLY";
const YEARLY = "YEARLY";

protected $guarded = [];

protected $casts = [
'start_at' => 'datetime',
'end_at' => 'datetime',
];

/**
* @return BelongsToMany
*/
public function categories(): BelongsToMany
{
return $this->belongsToMany(Category::class);
}

public function getStartAtDateAttribute()
{
return $this->getCurrentWindowStartAndEndDates()[0]->format('Y-m-d');
}

public function getEndAtDateAttribute()
{
return $this->getCurrentWindowStartAndEndDates()[1]->format('Y-m-d');
}

public function getTotalAccumulatedTransactionsAmountAttribute()
{
$categories = $this->categories()->with('transactions')->get();
[$startAt, $endAt] = $this->getCurrentWindowStartAndEndDates();

return $categories->sum(function ($category) use ($startAt, $endAt){
return $category->transactions()
->whereBetween('transactions.created_at', [$startAt, $endAt])
->sum('amount');
});
}

private function getCurrentWindowStartAndEndDates()
{
if($this->reoccurrence === self::CUSTOM) {
return [$this->start_at, $this->end_at];
}

$unit = $this->getUnitMapping();
$intervalString = $this->period . ' ' . $unit;
$ranges = CarbonPeriod::create($this->start_at->startOfDay(), $intervalString, now()->copy()->add($unit, $this->period))->toArray();

foreach (array_reverse($ranges) as $range) {
if(now()->isAfter($range)) {
return [$range->copy(), $range->copy()->add($unit, $this->period)];
}
}
}

/**
* @return string
*/
private function getUnitMapping(): string
{
return [
self::DAILY => 'day',
self::WEEKLY => 'week',
self::MONTHLY => 'month',
self::YEARLY => 'year',
][$this->reoccurrence];
}
}
20 changes: 18 additions & 2 deletions app/Models/Category.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
use App\Contracts\Searchable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;

class Category extends Model implements Searchable
{
Expand All @@ -18,18 +20,32 @@ class Category extends Model implements Searchable

protected $guarded = [];

protected static function booted()
/**
* @return void
*/
protected static function booted(): void
{
static::deleted(function ($category) {
$category->brands->each->delete();
});
}

public function brands()
/**
* @return HasMany
*/
public function brands(): HasMany
{
return $this->hasMany(Brand::class);
}

/**
* @return HasManyThrough
*/
public function transactions(): HasManyThrough
{
return $this->hasManyThrough(Transaction::class, Brand::class);
}

/**
* @param $query
* @return Builder
Expand Down
1 change: 0 additions & 1 deletion app/Models/Sms.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use function PHPUnit\Framework\isEmpty;

class Sms extends Model implements Searchable
{
Expand Down
28 changes: 28 additions & 0 deletions database/factories/BudgetFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Database\Factories;

use App\Models\Budget;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Budget>
*/
class BudgetFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => $this->faker->name(),
'amount' => $this->faker->numberBetween(),
'start_at' => now(),
'reoccurrence' => Budget::DAILY,
'period' => 1,
];
}
}
34 changes: 34 additions & 0 deletions database/migrations/2024_04_21_173031_create_budgets_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('budgets', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->decimal('amount');
$table->dateTime('start_at');
$table->dateTime('end_at')->nullable();
$table->boolean('saving')->default(false);
$table->unsignedInteger('period')->default(1);
$table->string('reoccurrence');
$table->timestamps();
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('budgets');
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('budget_category', function (Blueprint $table) {
$table->id();
$table->foreignId('budget_id')->constrained()->onDelete('cascade');
$table->foreignId('category_id')->constrained()->onDelete('cascade');
$table->timestamps();
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('budget_category');
}
};
9 changes: 9 additions & 0 deletions graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ type Category {
color: String
}

type Budget {
id: ID!
amount: Float!
totalAccumulatedTransactionsAmount: Float!
startAtDate: String!
endAtDate: String!
}

type Brand {
id: ID!
name: String!
Expand Down Expand Up @@ -48,6 +56,7 @@ type Query {
@orderBy(column: id direction: DESC)

allBrands: [Brand!]! @all
budgets: [Budget!]! @all
brands(search: String @search): [Brand!]!
@paginate(defaultCount: 50)
@lazyLoad(relations: ["category"])
Expand Down
65 changes: 65 additions & 0 deletions tests/Unit/Models/Budgets/BudgetTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace Tests\Unit\Models\Budgets;

use Tests\TestCase;
use App\Models\Brand;
use App\Models\Budget;
use App\Models\Category;
use Illuminate\Foundation\Testing\RefreshDatabase;

class BudgetTest extends TestCase
{
use RefreshDatabase;

/** @test */
public function it_has_name()
{
$sut = Budget::factory()->create(['name' => 'test']);

$this->assertEquals("test", $sut->name);
}

/** @test */
public function it_belongs_to_categories()
{
$categories = Category::factory()->createMany(3);
$sut = Budget::factory()->create();
$sut->categories()->attach($categories);

$this->assertCount(3, $sut->categories);
}

/** @test */
public function it_has_total_categories_transactions_amount()
{
$category = Category::factory()->create();
$brand = Brand::factory()->create(['category_id' => $category->id]);
$sut = Budget::factory()->create(['start_at' => now()->subDays(1), 'end_at' => now()->addDays(1)]);
$sut->categories()->attach($category);

$category->transactions()->create(['amount' => 100, 'brand_id' => $brand->id]);
$category->transactions()->create(['amount' => 200, 'brand_id' => $brand->id]);
$category->transactions()->create(['amount' => 200, 'brand_id' => $brand->id, 'created_at' => now()->addDays(2)]);

$this->assertEquals(300, $sut->totalAccumulatedTransactionsAmount);
}

/** @test */
public function it_has_start_and_end_at()
{
$sut = Budget::factory()->create(['start_at' => now()->subDays(1), 'end_at' => now()->addDays(1)]);

$this->assertEquals(now()->subDays(1)->format('Y-m-d'), $sut->start_at->format('Y-m-d'));
$this->assertEquals(now()->addDays(1)->format('Y-m-d'), $sut->end_at->format('Y-m-d'));
}

/** @test */
public function it_has_start_and_end_dates_window()
{
$sut = Budget::factory()->create(['start_at' => now()->subDays(1), 'period' => 1, 'reoccurrence' => Budget::DAILY]);

$this->assertEquals(now()->format('Y-m-d'), $sut->startAtDate);
$this->assertEquals(now()->addDays(1)->format('Y-m-d'), $sut->endAtDate);
}
}

0 comments on commit 9a33e1c

Please sign in to comment.