@@ -0,0 +1,15 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type Account struct {
|
||||
ID string `db:"id" json:"id"`
|
||||
UserID string `db:"user_id" json:"user_id"`
|
||||
Mode string `db:"mode" json:"mode"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Currency string `db:"currency" json:"currency"`
|
||||
LedgerStatus string `db:"ledger_status" json:"ledger_status"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
}
|
||||
|
||||
func (Account) TableName() string { return "veyra_account" }
|
||||
@@ -0,0 +1,27 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Credential struct {
|
||||
ID string `db:"id"`
|
||||
UserID string `db:"user_id"`
|
||||
LoginName string `db:"login_name"`
|
||||
PasswordHash string `db:"password_hash" json:"-"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
}
|
||||
|
||||
func (Credential) TableName() string { return "veyra_credential" }
|
||||
|
||||
type Session struct {
|
||||
ID string `db:"id"`
|
||||
UserID string `db:"user_id"`
|
||||
TokenHash string `db:"token_hash" json:"-"`
|
||||
Audience string `db:"audience"`
|
||||
ExpiresAt time.Time `db:"expires_at"`
|
||||
RevokedAt *time.Time `db:"revoked_at"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
}
|
||||
|
||||
func (Session) TableName() string { return "veyra_session" }
|
||||
@@ -0,0 +1,19 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// CashEntry amounts are integer CNY cents; no floating-point accounting.
|
||||
type CashEntry struct {
|
||||
ID string `db:"id"`
|
||||
AccountID string `db:"account_id"`
|
||||
EntryType string `db:"entry_type"`
|
||||
AmountMinor int64 `db:"amount_minor"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
}
|
||||
|
||||
func (CashEntry) TableName() string { return "veyra_cash_entry" }
|
||||
|
||||
type AccountSummary struct {
|
||||
Account
|
||||
CashBalance *string `json:"cash_balance"`
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.sundynix.cn/Blizzard/Veyra/internal/apperror"
|
||||
)
|
||||
|
||||
var moneyPattern = regexp.MustCompile(`^(0|[1-9][0-9]{0,9})(\.[0-9]{1,2})?$`)
|
||||
|
||||
// ParseMoney converts decimal text into exact cents, rejecting rounding and exponents.
|
||||
func ParseMoney(value string) (int64, error) {
|
||||
if !moneyPattern.MatchString(value) {
|
||||
return 0, apperror.ErrInvalidInput
|
||||
}
|
||||
whole, fraction, _ := strings.Cut(value, ".")
|
||||
units, err := strconv.ParseInt(whole, 10, 64)
|
||||
if err != nil {
|
||||
return 0, apperror.ErrInvalidInput
|
||||
}
|
||||
fraction += strings.Repeat("0", 2-len(fraction))
|
||||
cents, err := strconv.ParseInt(fraction, 10, 64)
|
||||
if err != nil {
|
||||
return 0, apperror.ErrInvalidInput
|
||||
}
|
||||
amount := units*100 + cents
|
||||
if amount > 100000000000 {
|
||||
return 0, apperror.ErrInvalidInput
|
||||
}
|
||||
return amount, nil
|
||||
}
|
||||
|
||||
func FormatMoney(minor int64) string { return fmt.Sprintf("%d.%02d", minor/100, minor%100) }
|
||||
@@ -0,0 +1,21 @@
|
||||
package model
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestExactMoney(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
input string
|
||||
minor int64
|
||||
formatted string
|
||||
}{{"0", 0, "0.00"}, {"0.01", 1, "0.01"}, {"100000.10", 10000010, "100000.10"}, {"3.1", 310, "3.10"}, {"1000000000.00", 100000000000, "1000000000.00"}} {
|
||||
amount, err := ParseMoney(tc.input)
|
||||
if err != nil || amount != tc.minor || FormatMoney(amount) != tc.formatted {
|
||||
t.Fatalf("%s: amount=%d err=%v", tc.input, amount, err)
|
||||
}
|
||||
}
|
||||
for _, input := range []string{"-1", "1.001", "1e3", "NaN", " 1", "01", "1.", "1000000000.01", "999999999999999999999", ""} {
|
||||
if _, err := ParseMoney(input); err == nil {
|
||||
t.Fatalf("accepted invalid amount %q", input)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type SchemaMigration struct {
|
||||
ID string `db:"id"`
|
||||
Version string `db:"version"`
|
||||
Checksum string `db:"checksum"`
|
||||
AppliedAt time.Time `db:"applied_at"`
|
||||
}
|
||||
|
||||
func (SchemaMigration) TableName() string { return "veyra_schema_migration" }
|
||||
@@ -0,0 +1,15 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// User maps explicitly to veyra_user. IDs are canonical UUID strings.
|
||||
type User struct {
|
||||
ID string `db:"id" json:"id"`
|
||||
DisplayName string `db:"display_name" json:"display_name"`
|
||||
Status string `db:"status" json:"status"`
|
||||
Role string `db:"role" json:"-"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (User) TableName() string { return "veyra_user" }
|
||||
Reference in New Issue
Block a user