184 lines
4.8 KiB
Go
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)
|
|
}
|