334 lines
8.5 KiB
Go
334 lines
8.5 KiB
Go
package repository
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
models "vibeStonk/server/models/v1"
|
|
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
)
|
|
|
|
var (
|
|
ErrHoldingNotFound = errors.New("holding not found")
|
|
)
|
|
|
|
func newSqliteHoldingRepo(db *sql.DB) (HoldingRepo, error) {
|
|
repo := &sqliteHoldingRepo{db: db}
|
|
return repo, nil
|
|
}
|
|
|
|
type sqliteHoldingRepo struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
// Helper function to convert a purchase row to a holding
|
|
func (s *sqliteHoldingRepo) purchaseToHolding(
|
|
id int64,
|
|
purchaseDate time.Time,
|
|
stockID int64,
|
|
qty float64,
|
|
price float64,
|
|
sold sql.NullFloat64,
|
|
) *models.Holding {
|
|
holding := &models.Holding{
|
|
Id: id,
|
|
PurchaseDate: timestamppb.New(purchaseDate),
|
|
StockID: stockID,
|
|
Qty: qty,
|
|
Price: price,
|
|
Sold: 0, // Default to 0 if no transactions
|
|
}
|
|
|
|
// If sold is not NULL, use its value
|
|
if sold.Valid {
|
|
holding.Sold = sold.Float64
|
|
}
|
|
|
|
return holding
|
|
}
|
|
|
|
// GetAll returns all holdings (purchases with sold quantity information)
|
|
func (s *sqliteHoldingRepo) GetAll() ([]*models.Holding, error) {
|
|
query := `
|
|
SELECT p.id, p.purchase_date, p.stock_id, p.qty, p.price,
|
|
COALESCE(SUM(t.qty), 0) as sold_qty
|
|
FROM purchases p
|
|
LEFT JOIN transactions t ON p.id = t.purchase_id
|
|
GROUP BY p.id
|
|
ORDER BY p.purchase_date DESC
|
|
`
|
|
rows, err := s.db.Query(query)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get all holdings: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var holdings []*models.Holding
|
|
for rows.Next() {
|
|
var id int64
|
|
var purchaseDate time.Time
|
|
var stockID int64
|
|
var qty float64
|
|
var price float64
|
|
var sold sql.NullFloat64
|
|
|
|
err := rows.Scan(&id, &purchaseDate, &stockID, &qty, &price, &sold)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan holding row: %w", err)
|
|
}
|
|
|
|
holding := s.purchaseToHolding(id, purchaseDate, stockID, qty, price, sold)
|
|
holdings = append(holdings, holding)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("error iterating holding rows: %w", err)
|
|
}
|
|
|
|
return holdings, nil
|
|
}
|
|
|
|
// GetUnsold returns holdings that are not fully sold (including partially sold)
|
|
func (s *sqliteHoldingRepo) GetUnsold() ([]*models.Holding, error) {
|
|
query := `
|
|
SELECT p.id, p.purchase_date, p.stock_id, p.qty, p.price,
|
|
COALESCE(SUM(t.qty), 0) as sold_qty
|
|
FROM purchases p
|
|
LEFT JOIN transactions t ON p.id = t.purchase_id
|
|
GROUP BY p.id
|
|
HAVING p.qty > COALESCE(SUM(t.qty), 0)
|
|
ORDER BY p.purchase_date DESC
|
|
`
|
|
rows, err := s.db.Query(query)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get unsold holdings: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var holdings []*models.Holding
|
|
for rows.Next() {
|
|
var id int64
|
|
var purchaseDate time.Time
|
|
var stockID int64
|
|
var qty float64
|
|
var price float64
|
|
var sold sql.NullFloat64
|
|
|
|
err := rows.Scan(&id, &purchaseDate, &stockID, &qty, &price, &sold)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan holding row: %w", err)
|
|
}
|
|
|
|
holding := s.purchaseToHolding(id, purchaseDate, stockID, qty, price, sold)
|
|
holdings = append(holdings, holding)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("error iterating holding rows: %w", err)
|
|
}
|
|
|
|
return holdings, nil
|
|
}
|
|
|
|
// GetSold returns holdings that are fully sold
|
|
func (s *sqliteHoldingRepo) GetSold() ([]*models.Holding, error) {
|
|
query := `
|
|
SELECT p.id, p.purchase_date, p.stock_id, p.qty, p.price,
|
|
COALESCE(SUM(t.qty), 0) as sold_qty
|
|
FROM purchases p
|
|
LEFT JOIN transactions t ON p.id = t.purchase_id
|
|
GROUP BY p.id
|
|
HAVING p.qty <= COALESCE(SUM(t.qty), 0)
|
|
ORDER BY p.purchase_date DESC
|
|
`
|
|
rows, err := s.db.Query(query)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get sold holdings: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var holdings []*models.Holding
|
|
for rows.Next() {
|
|
var id int64
|
|
var purchaseDate time.Time
|
|
var stockID int64
|
|
var qty float64
|
|
var price float64
|
|
var sold sql.NullFloat64
|
|
|
|
err := rows.Scan(&id, &purchaseDate, &stockID, &qty, &price, &sold)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan holding row: %w", err)
|
|
}
|
|
|
|
holding := s.purchaseToHolding(id, purchaseDate, stockID, qty, price, sold)
|
|
holdings = append(holdings, holding)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("error iterating holding rows: %w", err)
|
|
}
|
|
|
|
return holdings, nil
|
|
}
|
|
|
|
// GetByID returns a specific holding by ID
|
|
func (s *sqliteHoldingRepo) GetByID(id int64) (*models.Holding, error) {
|
|
query := `
|
|
SELECT p.id, p.purchase_date, p.stock_id, p.qty, p.price,
|
|
COALESCE(SUM(t.qty), 0) as sold_qty
|
|
FROM purchases p
|
|
LEFT JOIN transactions t ON p.id = t.purchase_id
|
|
WHERE p.id = ?
|
|
GROUP BY p.id
|
|
`
|
|
row := s.db.QueryRow(query, id)
|
|
|
|
var purchaseDate time.Time
|
|
var stockID int64
|
|
var qty float64
|
|
var price float64
|
|
var sold sql.NullFloat64
|
|
|
|
err := row.Scan(&id, &purchaseDate, &stockID, &qty, &price, &sold)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ErrHoldingNotFound
|
|
}
|
|
return nil, fmt.Errorf("failed to get holding: %w", err)
|
|
}
|
|
|
|
holding := s.purchaseToHolding(id, purchaseDate, stockID, qty, price, sold)
|
|
return holding, nil
|
|
}
|
|
|
|
// GetByStockID returns holdings for a specific stock
|
|
func (s *sqliteHoldingRepo) GetByStockID(stockID int64) ([]*models.Holding, error) {
|
|
query := `
|
|
SELECT p.id, p.purchase_date, p.stock_id, p.qty, p.price,
|
|
COALESCE(SUM(t.qty), 0) as sold_qty
|
|
FROM purchases p
|
|
LEFT JOIN transactions t ON p.id = t.purchase_id
|
|
WHERE p.stock_id = ?
|
|
GROUP BY p.id
|
|
ORDER BY p.purchase_date DESC
|
|
`
|
|
rows, err := s.db.Query(query, stockID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get holdings by stock ID: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var holdings []*models.Holding
|
|
for rows.Next() {
|
|
var id int64
|
|
var purchaseDate time.Time
|
|
var pStockID int64
|
|
var qty float64
|
|
var price float64
|
|
var sold sql.NullFloat64
|
|
|
|
err := rows.Scan(&id, &purchaseDate, &pStockID, &qty, &price, &sold)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan holding row: %w", err)
|
|
}
|
|
|
|
holding := s.purchaseToHolding(id, purchaseDate, pStockID, qty, price, sold)
|
|
holdings = append(holdings, holding)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("error iterating holding rows: %w", err)
|
|
}
|
|
|
|
return holdings, nil
|
|
}
|
|
|
|
// GetUnsoldByStockID returns unsold holdings for a specific stock ID
|
|
func (s *sqliteHoldingRepo) GetUnsoldByStockID(stockID int64) ([]*models.Holding, error) {
|
|
query := `
|
|
SELECT p.id, p.purchase_date, p.stock_id, p.qty, p.price,
|
|
COALESCE(SUM(t.qty), 0) as sold_qty
|
|
FROM purchases p
|
|
LEFT JOIN transactions t ON p.id = t.purchase_id
|
|
WHERE p.stock_id = ?
|
|
GROUP BY p.id
|
|
HAVING p.qty > COALESCE(SUM(t.qty), 0)
|
|
ORDER BY p.purchase_date ASC
|
|
`
|
|
rows, err := s.db.Query(query, stockID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get unsold holdings by stock ID: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var holdings []*models.Holding
|
|
for rows.Next() {
|
|
var id int64
|
|
var purchaseDate time.Time
|
|
var stockID int64
|
|
var qty float64
|
|
var price float64
|
|
var sold sql.NullFloat64
|
|
|
|
err := rows.Scan(&id, &purchaseDate, &stockID, &qty, &price, &sold)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan holding row: %w", err)
|
|
}
|
|
|
|
holding := s.purchaseToHolding(id, purchaseDate, stockID, qty, price, sold)
|
|
holdings = append(holdings, holding)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("error iterating holding rows: %w", err)
|
|
}
|
|
|
|
return holdings, nil
|
|
}
|
|
|
|
// GetSoldByStockID returns sold holdings for a specific stock ID
|
|
func (s *sqliteHoldingRepo) GetSoldByStockID(stockID int64) ([]*models.Holding, error) {
|
|
query := `
|
|
SELECT p.id, p.purchase_date, p.stock_id, p.qty, p.price,
|
|
COALESCE(SUM(t.qty), 0) as sold_qty
|
|
FROM purchases p
|
|
LEFT JOIN transactions t ON p.id = t.purchase_id
|
|
WHERE p.stock_id = ?
|
|
GROUP BY p.id
|
|
HAVING p.qty <= COALESCE(SUM(t.qty), 0)
|
|
ORDER BY p.purchase_date DESC
|
|
`
|
|
rows, err := s.db.Query(query, stockID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get sold holdings by stock ID: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var holdings []*models.Holding
|
|
for rows.Next() {
|
|
var id int64
|
|
var purchaseDate time.Time
|
|
var stockID int64
|
|
var qty float64
|
|
var price float64
|
|
var sold sql.NullFloat64
|
|
|
|
err := rows.Scan(&id, &purchaseDate, &stockID, &qty, &price, &sold)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan holding row: %w", err)
|
|
}
|
|
|
|
holding := s.purchaseToHolding(id, purchaseDate, stockID, qty, price, sold)
|
|
holdings = append(holdings, holding)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("error iterating holding rows: %w", err)
|
|
}
|
|
|
|
return holdings, nil
|
|
}
|