All checks were successful
Deploy / build-and-deploy (push) Successful in 2m37s
Combines the decimal migration (int cents → decimal.Decimal via shopspring/decimal) with main's handler refactor (split space.go into domain handlers, WithTx/Paginate helpers, recurring deposit removal). - Repository layer: WithTx pattern + decimal column names/types - Handler layer: decimal arithmetic (.Sub/.Add) instead of int operators - Models: deprecated amount_cents fields kept for SELECT * compatibility - INSERT statements: old columns set to literal 0 for NOT NULL constraints Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
191 lines
4.7 KiB
Go
191 lines
4.7 KiB
Go
package service
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.juancwu.dev/juancwu/budgit/internal/model"
|
|
"git.juancwu.dev/juancwu/budgit/internal/repository"
|
|
"github.com/google/uuid"
|
|
"github.com/shopspring/decimal"
|
|
)
|
|
|
|
type CreateMoneyAccountDTO struct {
|
|
SpaceID string
|
|
Name string
|
|
CreatedBy string
|
|
}
|
|
|
|
type UpdateMoneyAccountDTO struct {
|
|
ID string
|
|
Name string
|
|
}
|
|
|
|
type CreateTransferDTO struct {
|
|
AccountID string
|
|
Amount decimal.Decimal
|
|
Direction model.TransferDirection
|
|
Note string
|
|
CreatedBy string
|
|
}
|
|
|
|
type MoneyAccountService struct {
|
|
accountRepo repository.MoneyAccountRepository
|
|
}
|
|
|
|
func NewMoneyAccountService(accountRepo repository.MoneyAccountRepository) *MoneyAccountService {
|
|
return &MoneyAccountService{
|
|
accountRepo: accountRepo,
|
|
}
|
|
}
|
|
|
|
func (s *MoneyAccountService) CreateAccount(dto CreateMoneyAccountDTO) (*model.MoneyAccount, error) {
|
|
name := strings.TrimSpace(dto.Name)
|
|
if name == "" {
|
|
return nil, fmt.Errorf("account name cannot be empty")
|
|
}
|
|
|
|
now := time.Now()
|
|
account := &model.MoneyAccount{
|
|
ID: uuid.NewString(),
|
|
SpaceID: dto.SpaceID,
|
|
Name: name,
|
|
CreatedBy: dto.CreatedBy,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
err := s.accountRepo.Create(account)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return account, nil
|
|
}
|
|
|
|
func (s *MoneyAccountService) GetAccountsForSpace(spaceID string) ([]model.MoneyAccountWithBalance, error) {
|
|
accounts, err := s.accountRepo.GetBySpaceID(spaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := make([]model.MoneyAccountWithBalance, len(accounts))
|
|
for i, acct := range accounts {
|
|
balance, err := s.accountRepo.GetAccountBalance(acct.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result[i] = model.MoneyAccountWithBalance{
|
|
MoneyAccount: *acct,
|
|
Balance: balance,
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (s *MoneyAccountService) GetAccount(id string) (*model.MoneyAccount, error) {
|
|
return s.accountRepo.GetByID(id)
|
|
}
|
|
|
|
func (s *MoneyAccountService) UpdateAccount(dto UpdateMoneyAccountDTO) (*model.MoneyAccount, error) {
|
|
name := strings.TrimSpace(dto.Name)
|
|
if name == "" {
|
|
return nil, fmt.Errorf("account name cannot be empty")
|
|
}
|
|
|
|
account, err := s.accountRepo.GetByID(dto.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
account.Name = name
|
|
|
|
err = s.accountRepo.Update(account)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return account, nil
|
|
}
|
|
|
|
func (s *MoneyAccountService) DeleteAccount(id string) error {
|
|
return s.accountRepo.Delete(id)
|
|
}
|
|
|
|
func (s *MoneyAccountService) CreateTransfer(dto CreateTransferDTO, availableSpaceBalance decimal.Decimal) (*model.AccountTransfer, error) {
|
|
if dto.Amount.LessThanOrEqual(decimal.Zero) {
|
|
return nil, fmt.Errorf("amount must be positive")
|
|
}
|
|
|
|
if dto.Direction != model.TransferDirectionDeposit && dto.Direction != model.TransferDirectionWithdrawal {
|
|
return nil, fmt.Errorf("invalid transfer direction")
|
|
}
|
|
|
|
if dto.Direction == model.TransferDirectionDeposit {
|
|
if dto.Amount.GreaterThan(availableSpaceBalance) {
|
|
return nil, fmt.Errorf("insufficient available balance")
|
|
}
|
|
}
|
|
|
|
if dto.Direction == model.TransferDirectionWithdrawal {
|
|
accountBalance, err := s.accountRepo.GetAccountBalance(dto.AccountID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if dto.Amount.GreaterThan(accountBalance) {
|
|
return nil, fmt.Errorf("insufficient account balance")
|
|
}
|
|
}
|
|
|
|
transfer := &model.AccountTransfer{
|
|
ID: uuid.NewString(),
|
|
AccountID: dto.AccountID,
|
|
Amount: dto.Amount,
|
|
Direction: dto.Direction,
|
|
Note: strings.TrimSpace(dto.Note),
|
|
CreatedBy: dto.CreatedBy,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
err := s.accountRepo.CreateTransfer(transfer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return transfer, nil
|
|
}
|
|
|
|
func (s *MoneyAccountService) GetTransfersForAccount(accountID string) ([]*model.AccountTransfer, error) {
|
|
return s.accountRepo.GetTransfersByAccountID(accountID)
|
|
}
|
|
|
|
func (s *MoneyAccountService) DeleteTransfer(id string) error {
|
|
return s.accountRepo.DeleteTransfer(id)
|
|
}
|
|
|
|
func (s *MoneyAccountService) GetAccountBalance(accountID string) (decimal.Decimal, error) {
|
|
return s.accountRepo.GetAccountBalance(accountID)
|
|
}
|
|
|
|
func (s *MoneyAccountService) GetTotalAllocatedForSpace(spaceID string) (decimal.Decimal, error) {
|
|
return s.accountRepo.GetTotalAllocatedForSpace(spaceID)
|
|
}
|
|
|
|
const TransfersPerPage = 25
|
|
|
|
func (s *MoneyAccountService) GetTransfersForSpacePaginated(spaceID string, page int) ([]*model.AccountTransferWithAccount, int, error) {
|
|
total, err := s.accountRepo.CountTransfersBySpaceID(spaceID)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
page, totalPages, offset := Paginate(page, total, TransfersPerPage)
|
|
transfers, err := s.accountRepo.GetTransfersBySpaceIDPaginated(spaceID, TransfersPerPage, offset)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
return transfers, totalPages, nil
|
|
}
|