package repository import ( "database/sql" "errors" "fmt" "time" models "vibeStonk/server/models/v1" "google.golang.org/protobuf/types/known/timestamppb" ) var ( ErrPurchaseNotFound = errors.New("purchase not found") ) func newSqlitePurchaseRepo(db *sql.DB) (PurchaseRepo, error) { repo := &sqlitePurchaseRepo{db: db} if err := repo.initialize(); err != nil { return nil, err } return repo, nil } type sqlitePurchaseRepo struct { db *sql.DB } // initialize creates the purchases table if it doesn't exist func (s *sqlitePurchaseRepo) initialize() error { query := ` CREATE TABLE IF NOT EXISTS purchases ( id INTEGER PRIMARY KEY AUTOINCREMENT, purchase_date TIMESTAMP NOT NULL, stock_id INTEGER NOT NULL, qty REAL NOT NULL, price REAL NOT NULL, FOREIGN KEY (stock_id) REFERENCES stocks(id) ); ` _, err := s.db.Exec(query) if err != nil { return fmt.Errorf("failed to create purchases table: %w", err) } query = ` CREATE INDEX IF NOT EXISTS purchases_stock_id ON purchases(stock_id); ` _, err = s.db.Exec(query) if err != nil { return fmt.Errorf("failed to create stock_id index for purchases table: %w", err) } return nil } func (s *sqlitePurchaseRepo) Create(purchase *models.Purchase) (*models.Purchase, error) { query := ` INSERT INTO purchases (purchase_date, stock_id, qty, price) VALUES (?, ?, ?, ?) ` // Convert protobuf timestamp to time.Time for SQLite var purchaseDate time.Time if purchase.PurchaseDate != nil { purchaseDate = purchase.PurchaseDate.AsTime() } else { purchaseDate = time.Now() purchase.PurchaseDate = timestamppb.New(purchaseDate) } result, err := s.db.Exec(query, purchaseDate, purchase.StockID, purchase.Qty, purchase.Price) if err != nil { return nil, fmt.Errorf("failed to create purchase: %w", err) } id, err := result.LastInsertId() if err != nil { return nil, fmt.Errorf("failed to get last insert ID: %w", err) } purchase.Id = id return purchase, nil } func (s *sqlitePurchaseRepo) Get(id int64) (*models.Purchase, error) { query := ` SELECT id, purchase_date, stock_id, qty, price FROM purchases WHERE id = ? ` row := s.db.QueryRow(query, id) purchase := &models.Purchase{} var purchaseDate time.Time err := row.Scan(&purchase.Id, &purchaseDate, &purchase.StockID, &purchase.Qty, &purchase.Price) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, ErrPurchaseNotFound } return nil, fmt.Errorf("failed to get purchase: %w", err) } // Convert time.Time to protobuf timestamp purchase.PurchaseDate = timestamppb.New(purchaseDate) return purchase, nil } func (s *sqlitePurchaseRepo) GetByStockID(stockID int64) ([]*models.Purchase, error) { query := ` SELECT id, purchase_date, stock_id, qty, price FROM purchases WHERE stock_id = ? ` rows, err := s.db.Query(query, stockID) if err != nil { return nil, fmt.Errorf("failed to get purchases by stock ID: %w", err) } defer rows.Close() var purchases []*models.Purchase for rows.Next() { purchase := &models.Purchase{} var purchaseDate time.Time err := rows.Scan(&purchase.Id, &purchaseDate, &purchase.StockID, &purchase.Qty, &purchase.Price) if err != nil { return nil, fmt.Errorf("failed to scan purchase row: %w", err) } // Convert time.Time to protobuf timestamp purchase.PurchaseDate = timestamppb.New(purchaseDate) purchases = append(purchases, purchase) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating purchase rows: %w", err) } return purchases, nil } func (s *sqlitePurchaseRepo) Update(purchase *models.Purchase) error { query := ` UPDATE purchases SET purchase_date = ?, stock_id = ?, qty = ?, price = ? WHERE id = ? ` // Convert protobuf timestamp to time.Time for SQLite var purchaseDate time.Time if purchase.PurchaseDate != nil { purchaseDate = purchase.PurchaseDate.AsTime() } else { purchaseDate = time.Now() purchase.PurchaseDate = timestamppb.New(purchaseDate) } result, err := s.db.Exec(query, purchaseDate, purchase.StockID, purchase.Qty, purchase.Price, purchase.Id) if err != nil { return fmt.Errorf("failed to update purchase: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("failed to get rows affected: %w", err) } if rowsAffected == 0 { return ErrPurchaseNotFound } return nil } func (s *sqlitePurchaseRepo) Delete(purchase *models.Purchase) error { query := ` DELETE FROM purchases WHERE id = ? ` result, err := s.db.Exec(query, purchase.Id) if err != nil { return fmt.Errorf("failed to delete purchase: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("failed to get rows affected: %w", err) } if rowsAffected == 0 { return ErrPurchaseNotFound } return nil } func (s *sqlitePurchaseRepo) List() ([]*models.Purchase, error) { query := ` SELECT id, purchase_date, stock_id, qty, price FROM purchases ORDER BY purchase_date DESC ` rows, err := s.db.Query(query) if err != nil { return nil, fmt.Errorf("failed to list purchases: %w", err) } defer rows.Close() var purchases []*models.Purchase for rows.Next() { purchase := &models.Purchase{} var purchaseDate time.Time err := rows.Scan(&purchase.Id, &purchaseDate, &purchase.StockID, &purchase.Qty, &purchase.Price) if err != nil { return nil, fmt.Errorf("failed to scan purchase row: %w", err) } // Convert time.Time to protobuf timestamp purchase.PurchaseDate = timestamppb.New(purchaseDate) purchases = append(purchases, purchase) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating purchase rows: %w", err) } return purchases, nil }