337 lines
12 KiB
Go
337 lines
12 KiB
Go
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{})
|
|
}
|