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) }