webmoney/backend/app/Models/Budget.php
marcoitaloesp-ai 19dcdce262
feat: Add daily due payments notification system with user preferences
## New Features
- Email notifications for overdue and upcoming payments
- User preferences page for notification settings
- Daily scheduler to send alerts at user-configured time
- Smart analysis: payable items, transfer suggestions between accounts

## Backend
- Migration for user_preferences table
- SendDuePaymentsAlert Artisan command
- DuePaymentsAlert Mailable with HTML/text templates
- UserPreferenceController with test-notification endpoint
- Scheduler config for notify:due-payments command

## Frontend
- Preferences.jsx page with notification toggle
- API service for preferences
- Route and menu link for settings
- Translations (PT-BR, EN, ES)

## Server
- Cron configured for Laravel scheduler

Version: 1.44.5
2025-12-17 09:57:40 +00:00

255 lines
7.7 KiB
PHP

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon;
class Budget extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'category_id',
'subcategory_id',
'cost_center_id',
'name',
'amount',
'currency',
'year',
'month',
'period_type',
'is_cumulative',
'is_active',
'notes',
];
protected $casts = [
'amount' => 'decimal:2',
'is_active' => 'boolean',
'is_cumulative' => 'boolean',
];
protected $appends = [
'spent_amount',
'remaining_amount',
'usage_percentage',
'is_exceeded',
'period_label',
];
// ============================================
// Relaciones
// ============================================
public function user()
{
return $this->belongsTo(User::class);
}
public function category()
{
return $this->belongsTo(Category::class);
}
public function subcategory()
{
return $this->belongsTo(Category::class, 'subcategory_id');
}
public function costCenter()
{
return $this->belongsTo(CostCenter::class);
}
// ============================================
// Accessors
// ============================================
public function getSpentAmountAttribute()
{
// Calcular el gasto real de las transacciones
$query = Transaction::where('user_id', $this->user_id)
->where('type', 'debit');
// Definir el rango de fechas según el tipo de período
$startDate = Carbon::create($this->year, $this->month, 1);
if ($this->is_cumulative) {
// Cumulativo: desde el inicio del año hasta el final del período actual
$endDate = $this->getPeriodEndDate();
$query->whereYear('effective_date', $this->year)
->whereDate('effective_date', '<=', $endDate);
} else {
// No cumulativo: solo el período específico
switch ($this->period_type) {
case 'monthly':
$query->whereYear('effective_date', $this->year)
->whereMonth('effective_date', $this->month);
break;
case 'bimestral':
// 2 meses: mes actual + anterior (o siguiente según configuración)
$endDate = $startDate->copy()->addMonths(2)->subDay();
$query->whereBetween('effective_date', [$startDate, $endDate]);
break;
case 'trimestral':
// 3 meses
$endDate = $startDate->copy()->addMonths(3)->subDay();
$query->whereBetween('effective_date', [$startDate, $endDate]);
break;
case 'semestral':
// 6 meses
$endDate = $startDate->copy()->addMonths(6)->subDay();
$query->whereBetween('effective_date', [$startDate, $endDate]);
break;
case 'yearly':
$query->whereYear('effective_date', $this->year);
break;
}
}
// Se tem subcategoria específica, usa apenas ela
if ($this->subcategory_id) {
$query->where('category_id', $this->subcategory_id);
}
// Se tem apenas categoria, inclui todas as subcategorias
elseif ($this->category_id) {
$categoryIds = [$this->category_id];
$subcategories = Category::where('parent_id', $this->category_id)->pluck('id')->toArray();
$categoryIds = array_merge($categoryIds, $subcategories);
$query->whereIn('category_id', $categoryIds);
}
// Filtrar por centro de custos se especificado
if ($this->cost_center_id) {
$query->where('cost_center_id', $this->cost_center_id);
}
return abs($query->sum('amount'));
}
/**
* Obtém a data final do período
*/
private function getPeriodEndDate()
{
$startDate = Carbon::create($this->year, $this->month, 1);
switch ($this->period_type) {
case 'monthly':
return $startDate->endOfMonth();
case 'bimestral':
return $startDate->copy()->addMonths(2)->subDay();
case 'trimestral':
return $startDate->copy()->addMonths(3)->subDay();
case 'semestral':
return $startDate->copy()->addMonths(6)->subDay();
case 'yearly':
return $startDate->endOfYear();
default:
return $startDate->endOfMonth();
}
}
public function getRemainingAmountAttribute()
{
return $this->amount - $this->spent_amount;
}
public function getUsagePercentageAttribute()
{
if ($this->amount <= 0) return 0;
return round(($this->spent_amount / $this->amount) * 100, 1);
}
public function getIsExceededAttribute()
{
return $this->spent_amount > $this->amount;
}
public function getPeriodLabelAttribute()
{
$months = [
1 => 'Enero', 2 => 'Febrero', 3 => 'Marzo', 4 => 'Abril',
5 => 'Mayo', 6 => 'Junio', 7 => 'Julio', 8 => 'Agosto',
9 => 'Septiembre', 10 => 'Octubre', 11 => 'Noviembre', 12 => 'Diciembre'
];
$monthName = $months[$this->month] ?? '';
switch ($this->period_type) {
case 'yearly':
return $this->year;
case 'bimestral':
return $monthName . ' ' . $this->year . ' (Bimestral)';
case 'trimestral':
return $monthName . ' ' . $this->year . ' (Trimestral)';
case 'semestral':
return $monthName . ' ' . $this->year . ' (Semestral)';
default:
return $monthName . ' ' . $this->year;
}
}
// ============================================
// Methods
// ============================================
public static function copyToNextMonth($userId, $fromYear, $fromMonth)
{
$nextMonth = $fromMonth === 12 ? 1 : $fromMonth + 1;
$nextYear = $fromMonth === 12 ? $fromYear + 1 : $fromYear;
$budgets = self::where('user_id', $userId)
->where('year', $fromYear)
->where('month', $fromMonth)
->where('is_active', true)
->get();
foreach ($budgets as $budget) {
self::firstOrCreate([
'user_id' => $userId,
'category_id' => $budget->category_id,
'year' => $nextYear,
'month' => $nextMonth,
], [
'name' => $budget->name,
'amount' => $budget->amount,
'currency' => $budget->currency,
'period_type' => 'monthly',
'is_active' => true,
]);
}
}
// ============================================
// Scopes
// ============================================
public function scopeActive($query)
{
return $query->where('is_active', true);
}
public function scopeForPeriod($query, $year, $month = null)
{
$query->where('year', $year);
if ($month) {
$query->where('month', $month);
}
return $query;
}
public function scopeForUser($query, $userId)
{
return $query->where('user_id', $userId);
}
}