diff --git a/app/Models/Bucket.php b/app/Models/Bucket.php index 152f0ed..4b7ae38 100644 --- a/app/Models/Bucket.php +++ b/app/Models/Bucket.php @@ -27,12 +27,15 @@ class Bucket extends Model 'sort_order', 'allocation_type', 'allocation_value', + 'starting_amount', ]; protected $casts = [ 'priority' => 'integer', 'sort_order' => 'integer', 'allocation_value' => 'decimal:2', + 'starting_amount' => 'integer', + 'allocation_type' => BucketAllocationType::class, ]; public function scenario(): BelongsTo @@ -42,21 +45,17 @@ public function scenario(): BelongsTo /** * Get the draws for the bucket. - * (Will be implemented when Draw model is created) */ public function draws(): HasMany { - // TODO: Implement when Draw model is created return $this->hasMany(Draw::class); } /** * Get the outflows for the bucket. - * (Will be implemented when Outflow model is created) */ public function outflows(): HasMany { - // TODO: Implement when Outflow model is created return $this->hasMany(Outflow::class); } @@ -78,12 +77,14 @@ public function scopeOrderedBySortOrder($query) /** * Get the current balance of the bucket. - * For MVP, this will always return 0 as we don't have transactions yet. + * Calculates starting amount plus total draws (money allocated to bucket) minus total outflows (money spent from bucket). */ public function getCurrentBalance(): float { - // TODO: Calculate from draws minus outflows when those features are implemented - return 0.0; + $totalDrawsCents = $this->draws()->sum('amount'); + $totalOutflowsCents = $this->outflows()->sum('amount'); + + return ($this->starting_amount + $totalDrawsCents - $totalOutflowsCents) / 100; } /** diff --git a/app/Models/Stream.php b/app/Models/Stream.php index 66860c9..0e68039 100644 --- a/app/Models/Stream.php +++ b/app/Models/Stream.php @@ -2,11 +2,13 @@ namespace App\Models; +use App\Models\Traits\HasAmount; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; class Stream extends Model { + use HasAmount; const TYPE_INCOME = 'income'; const TYPE_EXPENSE = 'expense'; @@ -31,7 +33,7 @@ class Stream extends Model ]; protected $casts = [ - 'amount' => 'decimal:2', + 'amount' => 'integer', 'start_date' => 'date', 'end_date' => 'date', 'is_active' => 'boolean', @@ -42,6 +44,7 @@ public function scenario(): BelongsTo return $this->belongsTo(Scenario::class); } + public function bucket(): BelongsTo { return $this->belongsTo(Bucket::class); diff --git a/app/Models/Traits/HasAmount.php b/app/Models/Traits/HasAmount.php new file mode 100644 index 0000000..0ad6fa4 --- /dev/null +++ b/app/Models/Traits/HasAmount.php @@ -0,0 +1,31 @@ +amount / 100; + } + + /** + * Set amount from currency units (stores as minor units/cents). + */ + public function setAmountCurrencyAttribute($value): void + { + $this->attributes['amount'] = round($value * 100); + } + + /** + * Format amount for display with proper currency formatting. + * This can be extended later to support different currencies. + */ + public function getFormattedAmountAttribute(): string + { + return number_format($this->amount / 100, 2); + } +} \ No newline at end of file diff --git a/database/factories/BucketFactory.php b/database/factories/BucketFactory.php index daa2e1f..3235250 100644 --- a/database/factories/BucketFactory.php +++ b/database/factories/BucketFactory.php @@ -40,6 +40,7 @@ public function definition(): array 'sort_order' => $this->faker->numberBetween(0, 10), 'allocation_type' => $allocationType, 'allocation_value' => $this->getAllocationValueForType($allocationType), + 'starting_amount' => $this->faker->numberBetween(0, 100000), // $0 to $1000 in cents ]; } @@ -92,6 +93,7 @@ public function defaultSet(): array 'sort_order' => 1, 'allocation_type' => BucketAllocationType::FIXED_LIMIT, 'allocation_value' => 0, + 'starting_amount' => 0, ]), $this->state([ 'name' => 'Emergency Fund', @@ -99,6 +101,7 @@ public function defaultSet(): array 'sort_order' => 2, 'allocation_type' => BucketAllocationType::FIXED_LIMIT, 'allocation_value' => 0, + 'starting_amount' => 0, ]), $this->state([ 'name' => 'Investments', @@ -106,6 +109,7 @@ public function defaultSet(): array 'sort_order' => 3, 'allocation_type' => BucketAllocationType::UNLIMITED, 'allocation_value' => null, + 'starting_amount' => 0, ]), ]; } diff --git a/database/migrations/2025_12_29_205724_create_buckets_table.php b/database/migrations/2025_12_29_205724_create_buckets_table.php index 03e74cd..b1dd390 100644 --- a/database/migrations/2025_12_29_205724_create_buckets_table.php +++ b/database/migrations/2025_12_29_205724_create_buckets_table.php @@ -17,6 +17,8 @@ public function up(): void $table->enum('allocation_type', ['fixed_limit', 'percentage', 'unlimited']); $table->decimal('allocation_value', 10, 2)->nullable() ->comment('Limit amount for fixed_limit, percentage for percentage type, NULL for unlimited'); + $table->unsignedBigInteger('starting_amount')->default(0) + ->comment('Initial amount in bucket in cents before any draws or outflows'); $table->timestamps(); // Indexes for performance diff --git a/database/migrations/2025_12_30_202750_create_streams_table.php b/database/migrations/2025_12_30_202750_create_streams_table.php index 2dddef2..8f5463f 100644 --- a/database/migrations/2025_12_30_202750_create_streams_table.php +++ b/database/migrations/2025_12_30_202750_create_streams_table.php @@ -14,7 +14,7 @@ public function up(): void $table->foreignId('bucket_id')->nullable()->constrained()->nullOnDelete(); $table->string('name'); $table->boolean('is_active')->default(true); - $table->decimal('amount', 12, 2); + $table->unsignedBigInteger('amount'); $table->enum('type', ['income', 'expense']); $table->enum('frequency', ['once', 'weekly', 'biweekly', 'monthly', 'quarterly', 'yearly']); $table->date('start_date');