package models import ( "time" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) // User repräsentiert einen Benutzer des Systems type User struct { ID uint `gorm:"primaryKey" json:"id"` Name string `json:"name" gorm:"not null"` Email string `json:"email" gorm:"uniqueIndex;not null"` PasswordHash string `json:"-" gorm:"not null"` // JSON-Tag "-" versteckt das Feld in API-Responses BankAccounts []BankAccount `gorm:"foreignKey:UserID" json:"bank_accounts,omitempty"` Depots []Depot `gorm:"foreignKey:UserID" json:"depots,omitempty"` Transactions []Transaction `gorm:"foreignKey:UserID" json:"transactions,omitempty"` Categories []Category `gorm:"foreignKey:UserID" json:"categories,omitempty"` RecurrenceRules []RecurrenceRule `gorm:"foreignKey:UserID" json:"recurrence_rules,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // BankAccount repräsentiert ein Bankkonto eines Benutzers type BankAccount struct { ID uint `gorm:"primaryKey" json:"id"` UserID uint `json:"user_id" gorm:"not null"` User User `gorm:"foreignKey:UserID" json:"user,omitempty"` Name string `json:"name" gorm:"not null"` Bank string `json:"bank" gorm:"not null"` IBAN string `json:"iban"` Balance float64 `json:"balance" gorm:"default:0"` AccountType string `json:"account_type" gorm:"not null"` // "checking", "savings", "credit" Transactions []Transaction `gorm:"foreignKey:BankAccountID" json:"transactions,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // Depot repräsentiert ein Wertpapierdepot eines Benutzers type Depot struct { ID uint `gorm:"primaryKey" json:"id"` UserID uint `json:"user_id" gorm:"not null"` User User `gorm:"foreignKey:UserID" json:"user,omitempty"` Name string `json:"name" gorm:"not null"` Broker string `json:"broker" gorm:"not null"` DepotNumber string `json:"depot_number"` TotalValue float64 `json:"total_value" gorm:"default:0"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // Category repräsentiert eine Transaktionskategorie type Category struct { ID uint `gorm:"primaryKey" json:"id"` UserID uint `json:"user_id" gorm:"not null"` User User `gorm:"foreignKey:UserID" json:"user,omitempty"` Name string `json:"name" gorm:"not null"` Description string `json:"description"` Color string `json:"color" gorm:"default:'#6B7280'"` // Default-Farbe Icon string `json:"icon" gorm:"default:'💰'"` // Default-Icon Transactions []Transaction `gorm:"foreignKey:CategoryID" json:"transactions,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // Transaction repräsentiert eine Finanztransaktion type Transaction struct { ID uint `gorm:"primaryKey" json:"id"` UserID uint `json:"user_id" gorm:"not null"` User User `gorm:"foreignKey:UserID" json:"user,omitempty"` BankAccountID *uint `json:"bank_account_id"` // Optional: kann NULL sein für Depot-Transaktionen BankAccount *BankAccount `gorm:"foreignKey:BankAccountID" json:"bank_account,omitempty"` CategoryID *uint `json:"category_id"` // Optional Category *Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"` Amount float64 `json:"amount" gorm:"not null"` Description string `json:"description" gorm:"not null"` Type string `json:"type" gorm:"not null"` // "income" oder "expense" Date time.Time `json:"date" gorm:"not null"` IsRecurring bool `json:"is_recurring" gorm:"default:false"` RecurrenceRuleID *uint `json:"recurrence_rule_id"` // Optional: Link zur Wiederholungsregel RecurrenceRule *RecurrenceRule `gorm:"foreignKey:RecurrenceRuleID" json:"recurrence_rule,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // RecurrenceRule definiert die Regeln für wiederkehrende Transaktionen type RecurrenceRule struct { ID uint `gorm:"primaryKey" json:"id"` UserID uint `json:"user_id" gorm:"not null"` User User `gorm:"foreignKey:UserID" json:"user,omitempty"` // Template-Informationen für neue Transaktionen Amount float64 `json:"amount" gorm:"not null"` Description string `json:"description" gorm:"not null"` CategoryID *uint `json:"category_id"` Category *Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"` Type string `json:"type" gorm:"not null"` // "income" or "expense" BankAccountID *uint `json:"bank_account_id"` BankAccount *BankAccount `gorm:"foreignKey:BankAccountID" json:"bank_account,omitempty"` // Wiederholungsregeln Interval string `json:"interval" gorm:"not null"` // "daily", "weekly", "monthly", "yearly" IntervalCount int `json:"interval_count" gorm:"default:1"` // z.B. alle 2 Wochen = weekly + 2 StartDate time.Time `json:"start_date" gorm:"not null"` EndDate *time.Time `json:"end_date"` // Optional: NULL bedeutet unbegrenzt DayOfWeek *int `json:"day_of_week"` // 0=Sonntag, 1=Montag, etc. (für wöchentlich) DayOfMonth *int `json:"day_of_month"` // 1-31 (für monatlich) IsActive bool `json:"is_active" gorm:"default:true"` LastGenerated *time.Time `json:"last_generated"` // Letztes generiertes Datum // Beziehungen zu generierten Transaktionen Transactions []Transaction `gorm:"foreignKey:RecurrenceRuleID" json:"transactions,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // Dashboard-spezifische Strukturen für Auswertungen // AssetOverview stellt eine Vermögensübersicht dar type AssetOverview struct { TotalBankBalance float64 `json:"total_bank_balance"` TotalDepotValue float64 `json:"total_depot_value"` TotalAssets float64 `json:"total_assets"` BankAccounts []BankAccountSummary `json:"bank_accounts"` Depots []DepotSummary `json:"depots"` } // BankAccountSummary für Dashboard-Übersicht type BankAccountSummary struct { ID uint `json:"id"` Name string `json:"name"` Bank string `json:"bank"` AccountType string `json:"account_type"` Balance float64 `json:"balance"` } // DepotSummary für Dashboard-Übersicht type DepotSummary struct { ID uint `json:"id"` Name string `json:"name"` Broker string `json:"broker"` TotalValue float64 `json:"total_value"` } // MonthlyStats für monatliche Statistiken type MonthlyStats struct { Month string `json:"month"` Income float64 `json:"income"` Expenses float64 `json:"expenses"` NetChange float64 `json:"net_change"` Balance float64 `json:"balance"` } // WeeklyStats für wöchentliche Statistiken type WeeklyStats struct { Week string `json:"week"` Income float64 `json:"income"` Expenses float64 `json:"expenses"` NetChange float64 `json:"net_change"` StartDate time.Time `json:"start_date"` EndDate time.Time `json:"end_date"` } // CategoryStats für Kategorie-Auswertungen type CategoryStats struct { CategoryID uint `json:"category_id"` CategoryName string `json:"category_name"` TotalAmount float64 `json:"total_amount"` Count int `json:"count"` Percentage float64 `json:"percentage"` } // TransactionTrend für Verlaufsdaten type TransactionTrend struct { Date time.Time `json:"date"` Income float64 `json:"income"` Expense float64 `json:"expense"` Balance float64 `json:"balance"` } // DashboardData kombiniert alle Dashboard-Daten type DashboardData struct { UserName string `json:"user_name"` AssetOverview AssetOverview `json:"asset_overview"` MonthlyStats []MonthlyStats `json:"monthly_stats"` WeeklyStats []WeeklyStats `json:"weekly_stats"` CategoryStats []CategoryStats `json:"category_stats"` TransactionTrend []TransactionTrend `json:"transaction_trend"` RecentTransactions []Transaction `json:"recent_transactions"` } // HashPassword erstellt einen bcrypt Hash des Passworts func (u *User) HashPassword(password string) error { hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return err } u.PasswordHash = string(hashedBytes) return nil } // CheckPassword vergleicht das eingegebene Passwort mit dem Hash func (u *User) CheckPassword(password string) bool { err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)) return err == nil } // IsRecurringTransaction prüft, ob eine Transaktion wiederkehrend ist func (t *Transaction) IsRecurringTransaction() bool { return t.IsRecurring && t.RecurrenceRule != nil } // GetNextOccurrence berechnet das nächste Auftreten einer wiederkehrenden Transaktion func (r *RecurrenceRule) GetNextOccurrence(from time.Time) *time.Time { if !r.IsActive { return nil } var next time.Time switch r.Interval { case "daily": next = from.AddDate(0, 0, r.IntervalCount) case "weekly": next = from.AddDate(0, 0, 7*r.IntervalCount) // Wenn ein bestimmter Wochentag gewünscht ist if r.DayOfWeek != nil { daysUntilTargetDay := (*r.DayOfWeek - int(from.Weekday()) + 7) % 7 if daysUntilTargetDay == 0 && from.Equal(r.StartDate) { daysUntilTargetDay = 7 * r.IntervalCount } next = from.AddDate(0, 0, daysUntilTargetDay) } case "monthly": next = from.AddDate(0, r.IntervalCount, 0) // Wenn ein bestimmter Tag im Monat gewünscht ist if r.DayOfMonth != nil && *r.DayOfMonth >= 1 && *r.DayOfMonth <= 31 { next = time.Date(next.Year(), next.Month(), *r.DayOfMonth, next.Hour(), next.Minute(), next.Second(), next.Nanosecond(), next.Location()) } case "yearly": next = from.AddDate(r.IntervalCount, 0, 0) default: return nil } // Prüfen, ob das nächste Datum nach dem Enddatum liegt if r.EndDate != nil && next.After(*r.EndDate) { return nil } return &next } // CanGenerate prüft, ob eine neue Instanz der wiederkehrenden Transaktion generiert werden kann func (r *RecurrenceRule) CanGenerate() bool { if !r.IsActive { return false } now := time.Now() // Prüfen, ob wir noch vor dem Enddatum sind if r.EndDate != nil && now.After(*r.EndDate) { return false } // Prüfen, ob es Zeit für die nächste Generierung ist lastDate := r.StartDate if r.LastGenerated != nil { lastDate = *r.LastGenerated } nextOccurrence := r.GetNextOccurrence(lastDate) if nextOccurrence == nil { return false } // Geändert: Generiere Transaktionen für die nächsten 12 Monate (für Dashboard-Trend) future12Months := now.AddDate(1, 0, 0) if nextOccurrence.After(future12Months) { return false } return true } // GenerateTransaction erstellt eine neue Transaktion basierend auf der Wiederholungsregel func (r *RecurrenceRule) GenerateTransaction(db *gorm.DB) (*Transaction, error) { if !r.CanGenerate() { return nil, nil // Keine Transaktion zu generieren } // Nächstes Datum berechnen lastDate := r.StartDate if r.LastGenerated != nil { lastDate = *r.LastGenerated } nextDate := r.GetNextOccurrence(lastDate) if nextDate == nil { return nil, nil } // Neue Transaktion erstellen transaction := &Transaction{ UserID: r.UserID, BankAccountID: r.BankAccountID, Amount: r.Amount, Description: r.Description, CategoryID: r.CategoryID, Type: r.Type, Date: *nextDate, IsRecurring: true, RecurrenceRuleID: &r.ID, } // Transaktion in der Datenbank speichern if err := db.Create(transaction).Error; err != nil { return nil, err } // LastGenerated aktualisieren r.LastGenerated = nextDate if err := db.Save(r).Error; err != nil { return nil, err } return transaction, nil } // Migrate führt die Datenbank-Migration durch func Migrate(db *gorm.DB) error { return db.AutoMigrate(&User{}, &Category{}, &Transaction{}, &BankAccount{}, &Depot{}, &RecurrenceRule{}) }