vibeStonk/server/services/ledgerService.go
2025-06-12 16:57:42 -04:00

184 lines
4.8 KiB
Go

package services
import (
"errors"
"fmt"
"google.golang.org/protobuf/types/known/timestamppb"
"math"
"sync"
"time"
models "vibeStonk/server/models/v1"
"vibeStonk/server/repository"
"vibeStonk/server/util"
)
var (
ErrInsufficientHoldings = errors.New("insufficient stocks in holdings")
)
func NewLedgerService(config *repository.Config, user *models.User) (LedgerService, error) {
connector, err := repository.GetUserConnector(config, user)
if err != nil {
return nil, fmt.Errorf("failed to get connector for user[%s]: %w", user.UserName, err)
}
purchases, err := repository.NewPurchaseRepo(config, connector)
if err != nil {
return nil, fmt.Errorf("failed to initialize purcahses repo: %w", err)
}
sales, err := repository.NewSaleRepo(config, connector)
if err != nil {
return nil, fmt.Errorf("failed to initialize sales repo: %w", err)
}
transactions, err := repository.NewTransactionRepo(config, connector)
if err != nil {
return nil, fmt.Errorf("failed to initialize transactions repo: %w", err)
}
holdings, err := repository.NewHoldingRepo(config, connector)
if err != nil {
return nil, fmt.Errorf("failed to initialize holdings repo: %w", err)
}
salesFragments, err := repository.NewSaleFragmentRepo(config, connector)
if err != nil {
return nil, fmt.Errorf("failed to initialize sales fragment repo: %w", err)
}
return &ledgerService{
holdings: holdings,
lock: &sync.Mutex{},
purchases: purchases,
sales: sales,
salesFragments: salesFragments,
transactions: transactions,
}, nil
}
type LedgerService interface {
NewPurchase(stockID int64, purchaseDate time.Time, qty, price float64) (*models.Purchase, error)
NewSale(stockID int, saleDate time.Time, qty, price float64) (*models.Sale, error)
DeleteSale(sale *models.Sale) error
// GetTransactionsByPurchase(purchaseID int) ([]models.Transaction, error)
// GetTransactionsBySale(saleID int) ([]models.Transaction, error)
}
type ledgerService struct {
holdings repository.HoldingRepo
lock *sync.Mutex
purchases repository.PurchaseRepo
sales repository.SaleRepo
salesFragments repository.SaleFragmentRepo
transactions repository.TransactionRepo
}
func (l *ledgerService) DeleteSale(sale *models.Sale) error {
l.lock.Lock()
defer l.lock.Unlock()
tx, err := l.sales.BeginDelete(sale)
if err != nil {
return fmt.Errorf("failed to begin delete transaction: %w", err)
}
err = l.transactions.DeleteBySaleID(sale.Id)
if err != nil {
tx.Rollback()
return fmt.Errorf("failed to delete associated sales transactions: %w", err)
}
// todo we need to rebuild the transactions table since the order of transactions matters. this might affect downstream
// transactions
return nil
}
func (l *ledgerService) NewPurchase(stockID int64, purchaseDate time.Time, qty, price float64) (*models.Purchase, error) {
l.lock.Lock()
defer l.lock.Unlock()
purchase := &models.Purchase{
Id: -1,
PurchaseDate: timestamppb.New(purchaseDate),
StockID: stockID,
Qty: qty,
Price: price,
}
purchase, err := l.purchases.Create(purchase)
if err != nil {
return nil, fmt.Errorf("failed to record purchase: %w", err)
}
return purchase, nil
}
func (l *ledgerService) NewSale(stockID int, saleDate time.Time, qty, price float64) (*models.Sale, error) {
l.lock.Lock()
defer l.lock.Unlock()
holdings, err := l.holdings.GetUnsoldByStockID(int64(stockID))
if err != nil {
return nil, fmt.Errorf("failed to get unsold holdings: %w", err)
}
remaining := util.SumSlice(holdings, func(h *models.Holding) float64 {
return h.Qty - h.Sold
})
if remaining < qty {
return nil, ErrInsufficientHoldings
}
toSell := qty
sale, tx, err := l.sales.BeginSale(stockID, saleDate, qty, price)
if err != nil {
return nil, fmt.Errorf("failed to create sale: %w", err)
}
for _, holding := range holdings {
txnAmount := math.Min(holding.Qty-holding.Sold, toSell)
txn := &models.Transaction{
Id: -1,
PurchaseID: holding.Id,
SaleID: sale.Id,
Qty: txnAmount,
}
toSell -= txnAmount
txn, err = l.transactions.Create(txn)
if err != nil {
tx.Rollback()
return nil, fmt.Errorf("failed to record sub transaction: %w", err)
}
if toSell <= 0 {
break
}
}
err = tx.Commit()
if err != nil {
tx.Rollback()
return nil, fmt.Errorf("failed to commit transactions: %w", err)
}
return sale, nil
}
// Holdings methods implementation
func (l *ledgerService) GetHoldingsByStockID(stockID int64) ([]*models.Holding, error) {
return l.holdings.GetByStockID(stockID)
}
func (l *ledgerService) GetUnsoldHoldingsByStockID(stockID int64) ([]*models.Holding, error) {
return l.holdings.GetUnsoldByStockID(stockID)
}
func (l *ledgerService) GetSoldHoldingsByStockID(stockID int64) ([]*models.Holding, error) {
return l.holdings.GetSoldByStockID(stockID)
}