feat: update models

This commit is contained in:
juancwu 2026-04-06 15:18:14 +00:00
commit 735d9e810c
20 changed files with 91 additions and 586 deletions

View file

@ -1,54 +0,0 @@
package model
import (
"strings"
"time"
"github.com/shopspring/decimal"
)
type BudgetPeriod string
const (
BudgetPeriodWeekly BudgetPeriod = "weekly"
BudgetPeriodMonthly BudgetPeriod = "monthly"
BudgetPeriodYearly BudgetPeriod = "yearly"
)
type BudgetStatus string
const (
BudgetStatusOnTrack BudgetStatus = "on_track"
BudgetStatusWarning BudgetStatus = "warning"
BudgetStatusOver BudgetStatus = "over"
)
type Budget struct {
ID string `db:"id"`
SpaceID string `db:"space_id"`
Amount decimal.Decimal `db:"amount"`
AmountCents int `db:"amount_cents"` // deprecated: kept for SELECT * compatibility
Period BudgetPeriod `db:"period"`
StartDate time.Time `db:"start_date"`
EndDate *time.Time `db:"end_date"`
IsActive bool `db:"is_active"`
CreatedBy string `db:"created_by"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
type BudgetWithSpent struct {
Budget
Tags []*Tag
Spent decimal.Decimal
Percentage float64
Status BudgetStatus
}
func (b *BudgetWithSpent) TagNames() string {
names := make([]string, len(b.Tags))
for i, t := range b.Tags {
names[i] = t.Name
}
return strings.Join(names, ", ")
}

View file

@ -1,57 +0,0 @@
package model
import (
"time"
"github.com/shopspring/decimal"
)
type ExpenseType string
const (
ExpenseTypeExpense ExpenseType = "expense"
ExpenseTypeTopup ExpenseType = "topup"
)
type Expense struct {
ID string `db:"id"`
SpaceID string `db:"space_id"`
CreatedBy string `db:"created_by"`
Description string `db:"description"`
Amount decimal.Decimal `db:"amount"`
AmountCents int `db:"amount_cents"` // deprecated: kept for SELECT * compatibility
Type ExpenseType `db:"type"`
Date time.Time `db:"date"`
PaymentMethodID *string `db:"payment_method_id"`
RecurringExpenseID *string `db:"recurring_expense_id"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
type ExpenseWithTags struct {
Expense
Tags []*Tag
}
type ExpenseWithTagsAndMethod struct {
Expense
Tags []*Tag
PaymentMethod *PaymentMethod
}
type ExpenseTag struct {
ExpenseID string `db:"expense_id"`
TagID string `db:"tag_id"`
}
type ExpenseItem struct {
ExpenseID string `db:"expense_id"`
ItemID string `db:"item_id"`
}
type TagExpenseSummary struct {
TagID string `db:"tag_id"`
TagName string `db:"tag_name"`
TagColor *string `db:"tag_color"`
TotalAmount decimal.Decimal `db:"total_amount"`
}

View file

@ -1,23 +0,0 @@
package model
import (
"time"
)
const (
FileTypeAvatar = "avatar"
)
type File struct {
ID string `db:"id"`
UserID string `db:"user_id"` // Who owns/created this file
OwnerType string `db:"owner_type"` // "user", "profile", etc. - the entity that owns the file
OwnerID string `db:"owner_id"` // Polymorphic FK
Type string `db:"type"`
Filename string `db:"filename"`
OriginalName string `db:"original_name"`
MimeType string `db:"mime_type"`
Size int64 `db:"size"`
StoragePath string `db:"storage_path"`
CreatedAt time.Time `db:"created_at"`
}

View file

@ -0,0 +1,41 @@
package model
import (
"time"
"github.com/shopspring/decimal"
)
type Account struct {
ID string `db:"id"`
Name string `db:"name"`
SpaceID string `db:"space_id"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
type TransactionType string
const (
TransactionTypeDeposit TransactionType = "deposit"
TransactionTypeWithdrawal TransactionType = "withdrawal"
)
type Transaction struct {
ID string `db:"id"`
Value decimal.Decimal `db:"value"`
Type TransactionType `db:"type"`
AccountID string `db:"account_id"`
Description *string `db:"description"`
RelatedTransactionID *string `db:"related_transaction_id"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
type Tag struct {
ID string `db:"id"`
Name string `db:"name"`
SpaceID string `db:"space_id"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}

View file

@ -1,8 +1,22 @@
package model package model
import ( import "time"
"time"
) type User struct {
ID string `db:"id"`
Email string `db:"email"`
Name *string `db:"name"`
// Allow null for passwordless users
PasswordHash *string `db:"password_hash"`
PendingEmail *string `db:"pending_email"`
EmailVerifiedAt *time.Time `db:"email_verified_at"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
func (u *User) HasPassword() bool {
return u.PasswordHash != nil && *u.PasswordHash != ""
}
type Token struct { type Token struct {
ID string `db:"id"` ID string `db:"id"`

View file

@ -1,22 +0,0 @@
package model
import "time"
type InvitationStatus string
const (
InvitationStatusPending InvitationStatus = "pending"
InvitationStatusAccepted InvitationStatus = "accepted"
InvitationStatusExpired InvitationStatus = "expired"
)
type SpaceInvitation struct {
Token string `db:"token"`
SpaceID string `db:"space_id"`
InviterID string `db:"inviter_id"`
Email string `db:"email"`
Status InvitationStatus `db:"status"`
ExpiresAt time.Time `db:"expires_at"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}

View file

@ -1,30 +0,0 @@
package model
import (
"time"
"github.com/shopspring/decimal"
)
type Loan struct {
ID string `db:"id"`
SpaceID string `db:"space_id"`
Name string `db:"name"`
Description string `db:"description"`
OriginalAmount decimal.Decimal `db:"original_amount"`
OriginalAmountCents int `db:"original_amount_cents"` // deprecated: kept for SELECT * compatibility
InterestRateBps int `db:"interest_rate_bps"`
StartDate time.Time `db:"start_date"`
EndDate *time.Time `db:"end_date"`
IsPaidOff bool `db:"is_paid_off"`
CreatedBy string `db:"created_by"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
type LoanWithPaymentSummary struct {
Loan
TotalPaid decimal.Decimal
Remaining decimal.Decimal
ReceiptCount int
}

View file

@ -1,17 +0,0 @@
package model
import (
"fmt"
"github.com/shopspring/decimal"
)
// FormatMoney formats a decimal as a dollar string like "$12.50"
func FormatMoney(d decimal.Decimal) string {
return fmt.Sprintf("$%s", d.StringFixed(2))
}
// FormatDecimal formats a decimal for form input values like "12.50"
func FormatDecimal(d decimal.Decimal) string {
return d.StringFixed(2)
}

View file

@ -1,45 +0,0 @@
package model
import (
"time"
"github.com/shopspring/decimal"
)
type TransferDirection string
const (
TransferDirectionDeposit TransferDirection = "deposit"
TransferDirectionWithdrawal TransferDirection = "withdrawal"
)
type MoneyAccount struct {
ID string `db:"id"`
SpaceID string `db:"space_id"`
Name string `db:"name"`
CreatedBy string `db:"created_by"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
type AccountTransfer struct {
ID string `db:"id"`
AccountID string `db:"account_id"`
Amount decimal.Decimal `db:"amount"`
AmountCents int `db:"amount_cents"` // deprecated: kept for SELECT * compatibility
Direction TransferDirection `db:"direction"`
Note string `db:"note"`
RecurringDepositID *string `db:"recurring_deposit_id"`
CreatedBy string `db:"created_by"`
CreatedAt time.Time `db:"created_at"`
}
type MoneyAccountWithBalance struct {
MoneyAccount
Balance decimal.Decimal
}
type AccountTransferWithAccount struct {
AccountTransfer
AccountName string `db:"account_name"`
}

View file

@ -1,21 +0,0 @@
package model
import "time"
type PaymentMethodType string
const (
PaymentMethodTypeCredit PaymentMethodType = "credit"
PaymentMethodTypeDebit PaymentMethodType = "debit"
)
type PaymentMethod struct {
ID string `db:"id"`
SpaceID string `db:"space_id"`
Name string `db:"name"`
Type PaymentMethodType `db:"type"`
LastFour *string `db:"last_four"`
CreatedBy string `db:"created_by"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}

View file

@ -1,25 +0,0 @@
package model
import "time"
type Profile struct {
ID string `db:"id"`
UserID string `db:"user_id"`
Name string `db:"name"`
Timezone *string `db:"timezone"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
// Location returns the *time.Location for this profile's timezone.
// Returns UTC if timezone is nil or invalid.
func (p *Profile) Location() *time.Location {
if p.Timezone == nil || *p.Timezone == "" {
return time.UTC
}
loc, err := time.LoadLocation(*p.Timezone)
if err != nil {
return time.UTC
}
return loc
}

View file

@ -1,54 +0,0 @@
package model
import (
"time"
"github.com/shopspring/decimal"
)
type FundingSourceType string
const (
FundingSourceBalance FundingSourceType = "balance"
FundingSourceAccount FundingSourceType = "account"
)
type Receipt struct {
ID string `db:"id"`
LoanID string `db:"loan_id"`
SpaceID string `db:"space_id"`
Description string `db:"description"`
TotalAmount decimal.Decimal `db:"total_amount"`
TotalAmountCents int `db:"total_amount_cents"` // deprecated: kept for SELECT * compatibility
Date time.Time `db:"date"`
RecurringReceiptID *string `db:"recurring_receipt_id"`
CreatedBy string `db:"created_by"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
type ReceiptFundingSource struct {
ID string `db:"id"`
ReceiptID string `db:"receipt_id"`
SourceType FundingSourceType `db:"source_type"`
AccountID *string `db:"account_id"`
Amount decimal.Decimal `db:"amount"`
AmountCents int `db:"amount_cents"` // deprecated: kept for SELECT * compatibility
LinkedExpenseID *string `db:"linked_expense_id"`
LinkedTransferID *string `db:"linked_transfer_id"`
}
type ReceiptWithSources struct {
Receipt
Sources []ReceiptFundingSource
}
type ReceiptFundingSourceWithAccount struct {
ReceiptFundingSource
AccountName string
}
type ReceiptWithSourcesAndAccounts struct {
Receipt
Sources []ReceiptFundingSourceWithAccount
}

View file

@ -1,46 +0,0 @@
package model
import (
"time"
"github.com/shopspring/decimal"
)
type Frequency string
const (
FrequencyDaily Frequency = "daily"
FrequencyWeekly Frequency = "weekly"
FrequencyBiweekly Frequency = "biweekly"
FrequencyMonthly Frequency = "monthly"
FrequencyYearly Frequency = "yearly"
)
type RecurringExpense struct {
ID string `db:"id"`
SpaceID string `db:"space_id"`
CreatedBy string `db:"created_by"`
Description string `db:"description"`
Amount decimal.Decimal `db:"amount"`
AmountCents int `db:"amount_cents"` // deprecated: kept for SELECT * compatibility
Type ExpenseType `db:"type"`
PaymentMethodID *string `db:"payment_method_id"`
Frequency Frequency `db:"frequency"`
StartDate time.Time `db:"start_date"`
EndDate *time.Time `db:"end_date"`
NextOccurrence time.Time `db:"next_occurrence"`
IsActive bool `db:"is_active"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
type RecurringExpenseWithTags struct {
RecurringExpense
Tags []*Tag
}
type RecurringExpenseWithTagsAndMethod struct {
RecurringExpense
Tags []*Tag
PaymentMethod *PaymentMethod
}

View file

@ -1,44 +0,0 @@
package model
import (
"time"
"github.com/shopspring/decimal"
)
type RecurringReceipt struct {
ID string `db:"id"`
LoanID string `db:"loan_id"`
SpaceID string `db:"space_id"`
Description string `db:"description"`
TotalAmount decimal.Decimal `db:"total_amount"`
TotalAmountCents int `db:"total_amount_cents"` // deprecated: kept for SELECT * compatibility
Frequency Frequency `db:"frequency"`
StartDate time.Time `db:"start_date"`
EndDate *time.Time `db:"end_date"`
NextOccurrence time.Time `db:"next_occurrence"`
IsActive bool `db:"is_active"`
CreatedBy string `db:"created_by"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
type RecurringReceiptSource struct {
ID string `db:"id"`
RecurringReceiptID string `db:"recurring_receipt_id"`
SourceType FundingSourceType `db:"source_type"`
AccountID *string `db:"account_id"`
Amount decimal.Decimal `db:"amount"`
AmountCents int `db:"amount_cents"` // deprecated: kept for SELECT * compatibility
}
type RecurringReceiptWithSources struct {
RecurringReceipt
Sources []RecurringReceiptSource
}
type RecurringReceiptWithLoan struct {
RecurringReceipt
LoanName string
Sources []RecurringReceiptSource
}

View file

@ -1,35 +0,0 @@
package model
import (
"time"
"github.com/shopspring/decimal"
)
type DailySpending struct {
Date time.Time `db:"date"`
Total decimal.Decimal `db:"total"`
}
type MonthlySpending struct {
Month string `db:"month"`
Total decimal.Decimal `db:"total"`
}
type PaymentMethodExpenseSummary struct {
PaymentMethodID string `db:"payment_method_id"`
PaymentMethodName string `db:"payment_method_name"`
PaymentMethodType string `db:"payment_method_type"`
TotalAmount decimal.Decimal `db:"total_amount"`
}
type SpendingReport struct {
ByTag []*TagExpenseSummary
ByPaymentMethod []*PaymentMethodExpenseSummary
DailySpending []*DailySpending
MonthlySpending []*MonthlySpending
TopExpenses []*ExpenseWithTagsAndMethod
TotalIncome decimal.Decimal
TotalExpenses decimal.Decimal
NetBalance decimal.Decimal
}

View file

@ -1,33 +0,0 @@
package model
import "time"
type ShoppingList struct {
ID string `db:"id"`
SpaceID string `db:"space_id"`
Name string `db:"name"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
type ListItem struct {
ID string `db:"id"`
ListID string `db:"list_id"`
Name string `db:"name"`
IsChecked bool `db:"is_checked"`
CreatedBy string `db:"created_by"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
type ListWithUncheckedItems struct {
List *ShoppingList
Items []*ListItem
}
type ListCardData struct {
List *ShoppingList
Items []*ListItem
CurrentPage int
TotalPages int
}

View file

@ -1,48 +0,0 @@
package model
import "time"
type Role string
const (
RoleOwner Role = "owner"
RoleMember Role = "member"
)
type Space struct {
ID string `db:"id"`
Name string `db:"name"`
OwnerID string `db:"owner_id"`
Timezone *string `db:"timezone"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
// Location returns the *time.Location for this space's timezone.
// Returns nil if timezone is not set, so callers can distinguish "not set" from "UTC".
func (s *Space) Location() *time.Location {
if s.Timezone == nil || *s.Timezone == "" {
return nil
}
loc, err := time.LoadLocation(*s.Timezone)
if err != nil {
return nil
}
return loc
}
type SpaceMember struct {
SpaceID string `db:"space_id"`
UserID string `db:"user_id"`
Role Role `db:"role"`
JoinedAt time.Time `db:"joined_at"`
}
type SpaceMemberWithProfile struct {
SpaceID string `db:"space_id"`
UserID string `db:"user_id"`
Role Role `db:"role"`
JoinedAt time.Time `db:"joined_at"`
Name string `db:"name"`
Email string `db:"email"`
}

View file

@ -1,12 +0,0 @@
package model
import "time"
type Tag struct {
ID string `db:"id"`
SpaceID string `db:"space_id"`
Name string `db:"name"`
Color *string `db:"color"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}

View file

@ -1,17 +0,0 @@
package model
import "time"
type User struct {
ID string `db:"id"`
Email string `db:"email"`
// Allow null for passwordless users
PasswordHash *string `db:"password_hash"`
PendingEmail *string `db:"pending_email"`
EmailVerifiedAt *time.Time `db:"email_verified_at"`
CreatedAt time.Time `db:"created_at"`
}
func (u *User) HasPassword() bool {
return u.PasswordHash != nil && *u.PasswordHash != ""
}

View file

@ -0,0 +1,33 @@
package model
import "time"
type Role string
const (
RoleOwner Role = "owner"
RoleMember Role = "member"
)
type Space struct {
ID string `db:"id"`
Name string `db:"name"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
type SpaceMember struct {
SpaceID string `db:"space_id"`
UserID string `db:"user_id"`
Role Role `db:"role"`
JoinedAt time.Time `db:"joined_at"`
}
type SpaceInvitation struct {
Token string `db:"token"`
SpaceID string `db:"space_id"`
InviterID string `db:"inviter_id"`
InviteeEmail string `db:"invitee_email"`
ExpiresAt time.Time `db:"expires_at"`
CreatedAt time.Time `db:"created_at"`
}