*/ use HasFactory; protected $fillable = [ 'scenario_id', 'name', 'priority', 'sort_order', 'allocation_type', 'allocation_value', ]; protected $casts = [ 'priority' => 'integer', 'sort_order' => 'integer', 'allocation_value' => 'decimal:2', ]; // TODO Extract to Enum const string TYPE_FIXED_LIMIT = 'fixed_limit'; const string TYPE_PERCENTAGE = 'percentage'; const string TYPE_UNLIMITED = 'unlimited'; public function scenario(): BelongsTo { return $this->belongsTo(Scenario::class); } /** * 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); } /** * Scope to get buckets ordered by priority. */ public function scopeOrderedByPriority($query) { return $query->orderBy('priority'); } /** * Scope to get buckets ordered by sort order for UI display. */ public function scopeOrderedBySortOrder($query) { return $query->orderBy('sort_order')->orderBy('priority'); } /** * Get the current balance of the bucket. * For MVP, this will always return 0 as we don't have transactions yet. */ public function getCurrentBalance(): float { // TODO: Calculate from draws minus outflows when those features are implemented return 0.0; } /** * Check if the bucket can accept more money (for fixed_limit buckets). */ public function hasAvailableSpace(): bool { if ($this->allocation_type !== self::TYPE_FIXED_LIMIT) { return true; } return $this->getCurrentBalance() < $this->allocation_value; } /** * Get available space for fixed_limit buckets. */ public function getAvailableSpace(): float { if ($this->allocation_type !== self::TYPE_FIXED_LIMIT) { return PHP_FLOAT_MAX; } return max(0, $this->allocation_value - $this->getCurrentBalance()); } /** * Get display label for allocation type. */ public function getAllocationTypeLabel(): string { return match($this->allocation_type) { self::TYPE_FIXED_LIMIT => 'Fixed Limit', self::TYPE_PERCENTAGE => 'Percentage', self::TYPE_UNLIMITED => 'Unlimited', default => 'Unknown', }; } /** * Get formatted allocation value for display. */ public function getFormattedAllocationValue(): string { return match($this->allocation_type) { self::TYPE_FIXED_LIMIT => '$' . number_format($this->allocation_value, 2), self::TYPE_PERCENTAGE => number_format($this->allocation_value, 2) . '%', self::TYPE_UNLIMITED => 'All remaining', default => '-', }; } /** * Validation rules for bucket creation/update. */ public static function validationRules($scenarioId = null): array { $rules = [ 'name' => 'required|string|max:255', 'allocation_type' => 'required|in:' . implode(',', [ self::TYPE_FIXED_LIMIT, self::TYPE_PERCENTAGE, self::TYPE_UNLIMITED, ]), 'priority' => 'required|integer|min:1', ]; // Add scenario-specific priority uniqueness if scenario ID provided if ($scenarioId) { $rules['priority'] .= '|unique:buckets,priority,NULL,id,scenario_id,' . $scenarioId; } return $rules; } /** * Get allocation value validation rules based on type. */ public static function allocationValueRules($allocationType): array { return match($allocationType) { self::TYPE_FIXED_LIMIT => ['required', 'numeric', 'min:0'], self::TYPE_PERCENTAGE => ['required', 'numeric', 'min:0.01', 'max:100'], self::TYPE_UNLIMITED => ['nullable'], default => ['nullable'], }; } }