'decimal:2', 'planned_amount' => 'decimal:2', 'effective_date' => 'date', 'planned_date' => 'date', 'is_recurring' => 'boolean', 'is_split_child' => 'boolean', 'is_split_parent' => 'boolean', 'is_transfer' => 'boolean', 'is_refund_pair' => 'boolean', ]; // ========================================================================= // RELACIONAMENTOS // ========================================================================= public function user(): BelongsTo { return $this->belongsTo(User::class); } public function account(): BelongsTo { return $this->belongsTo(Account::class); } public function category(): BelongsTo { return $this->belongsTo(Category::class); } public function costCenter(): BelongsTo { return $this->belongsTo(CostCenter::class); } public function recurringParent(): BelongsTo { return $this->belongsTo(Transaction::class, 'recurring_parent_id'); } public function recurringChildren(): HasMany { return $this->hasMany(Transaction::class, 'recurring_parent_id'); } /** * Instância de recorrência vinculada */ public function recurringInstance(): BelongsTo { return $this->belongsTo(RecurringInstance::class, 'recurring_instance_id'); } public function importLog(): BelongsTo { return $this->belongsTo(ImportLog::class); } /** * Transação par de transferência (débito ↔ crédito) */ public function transferPair(): BelongsTo { return $this->belongsTo(Transaction::class, 'transfer_pair_id'); } /** * Transação pai (quando esta é uma divisão) */ public function parentTransaction(): BelongsTo { return $this->belongsTo(Transaction::class, 'parent_transaction_id'); } /** * Transações filhas (divisões desta transação) */ public function splitChildren(): HasMany { return $this->hasMany(Transaction::class, 'parent_transaction_id'); } // ========================================================================= // SCOPES // ========================================================================= public function scopeOfUser($query, $userId) { return $query->where('user_id', $userId); } public function scopePending($query) { return $query->where('status', 'pending'); } public function scopeCompleted($query) { // Incluir 'completed' e 'effective' como transações efetivadas return $query->whereIn('status', ['completed', 'effective']); } public function scopeCancelled($query) { return $query->where('status', 'cancelled'); } public function scopeCredits($query) { return $query->where('type', 'credit'); } public function scopeDebits($query) { return $query->where('type', 'debit'); } public function scopeInPeriod($query, $startDate, $endDate, $dateField = 'planned_date') { return $query->whereBetween($dateField, [$startDate, $endDate]); } public function scopeOfAccount($query, $accountId) { return $query->where('account_id', $accountId); } public function scopeOfCategory($query, $categoryId) { return $query->where('category_id', $categoryId); } public function scopeOfCostCenter($query, $costCenterId) { return $query->where('cost_center_id', $costCenterId); } // ========================================================================= // ATRIBUTOS COMPUTADOS // ========================================================================= /** * Retorna o valor final (efetivo se existir, senão planejado) */ public function getFinalAmountAttribute(): float { return $this->amount ?? $this->planned_amount; } /** * Retorna a data final (efetiva se existir, senão planejada) */ public function getFinalDateAttribute() { return $this->effective_date ?? $this->planned_date; } /** * Verifica se a transação está atrasada (pendente e data planejada passou) */ public function getIsOverdueAttribute(): bool { if ($this->status !== 'pending') { return false; } return $this->planned_date < now()->startOfDay(); } /** * Retorna o valor com sinal (positivo para crédito, negativo para débito) */ public function getSignedAmountAttribute(): float { $amount = $this->final_amount; return $this->type === 'credit' ? $amount : -$amount; } // ========================================================================= // MÉTODOS // ========================================================================= /** * Marca a transação como concluída */ public function markAsCompleted(?float $amount = null, ?string $effectiveDate = null): self { $this->status = 'completed'; $this->amount = $amount ?? $this->planned_amount; $this->effective_date = $effectiveDate ?? now()->toDateString(); $this->save(); return $this; } /** * Marca a transação como cancelada */ public function markAsCancelled(): self { $this->status = 'cancelled'; $this->save(); return $this; } /** * Reverte para pendente */ public function markAsPending(): self { $this->status = 'pending'; $this->amount = null; $this->effective_date = null; $this->save(); return $this; } /** * Gera hash único para evitar duplicidade na importação * Baseado em: data + valor + descrição original + saldo (se disponível no extrato) * * O saldo é usado APENAS para diferenciar transações idênticas no hash, * mas NÃO é armazenado na BD para não interferir no cálculo dinâmico de saldo. */ public static function generateImportHash( string $date, float $amount, ?string $originalDescription, ?float $balance = null ): string { // Normaliza os valores para garantir consistência $normalizedDate = date('Y-m-d', strtotime($date)); $normalizedAmount = number_format($amount, 2, '.', ''); $normalizedDescription = trim(strtolower($originalDescription ?? '')); // Prepara os componentes do hash $components = [ $normalizedDate, $normalizedAmount, $normalizedDescription, ]; // Se o saldo foi fornecido no extrato, usa para diferenciar transações idênticas if ($balance !== null) { $components[] = number_format($balance, 2, '.', ''); } // Concatena os valores e gera hash SHA-256 return hash('sha256', implode('|', $components)); } /** * Verifica se já existe transação com este hash para o usuário */ public static function existsByHash(int $userId, string $hash): bool { return self::where('user_id', $userId) ->where('import_hash', $hash) ->exists(); } /** * Scope para buscar por hash de importação */ public function scopeByImportHash($query, string $hash) { return $query->where('import_hash', $hash); } /** * Verifica se a transação foi importada */ public function getIsImportedAttribute(): bool { return !empty($this->import_hash); } }