summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/store/shopping.go82
-rw-r--r--internal/store/shopping_test.go151
-rw-r--r--internal/store/sqlite.go79
-rw-r--r--internal/store/sqlite_test.go142
4 files changed, 233 insertions, 221 deletions
diff --git a/internal/store/shopping.go b/internal/store/shopping.go
new file mode 100644
index 0000000..40571ac
--- /dev/null
+++ b/internal/store/shopping.go
@@ -0,0 +1,82 @@
+package store
+
+import "time"
+
+type UserShoppingItem struct {
+ ID int64
+ Name string
+ Store string
+ Checked bool
+ CreatedAt time.Time
+}
+
+// SaveUserShoppingItem saves a new user shopping item
+func (s *Store) SaveUserShoppingItem(name, store string) error {
+ _, err := s.db.Exec(`INSERT INTO user_shopping_items (name, store) VALUES (?, ?)`, name, store)
+ return err
+}
+
+// GetUserShoppingItems retrieves all user shopping items
+func (s *Store) GetUserShoppingItems() ([]UserShoppingItem, error) {
+ rows, err := s.db.Query(`SELECT id, name, store, checked, created_at FROM user_shopping_items ORDER BY store, created_at DESC`)
+ if err != nil {
+ return nil, err
+ }
+ defer func() { _ = rows.Close() }()
+
+ var items []UserShoppingItem
+ for rows.Next() {
+ var item UserShoppingItem
+ if err := rows.Scan(&item.ID, &item.Name, &item.Store, &item.Checked, &item.CreatedAt); err != nil {
+ return nil, err
+ }
+ items = append(items, item)
+ }
+ return items, rows.Err()
+}
+
+// ToggleUserShoppingItem toggles the checked state of a user shopping item
+func (s *Store) ToggleUserShoppingItem(id int64, checked bool) error {
+ _, err := s.db.Exec(`UPDATE user_shopping_items SET checked = ? WHERE id = ?`, checked, id)
+ return err
+}
+
+// DeleteUserShoppingItem removes a user shopping item
+func (s *Store) DeleteUserShoppingItem(id int64) error {
+ _, err := s.db.Exec(`DELETE FROM user_shopping_items WHERE id = ?`, id)
+ return err
+}
+
+// SetShoppingItemChecked sets the checked state for an external shopping item
+func (s *Store) SetShoppingItemChecked(source, itemID string, checked bool) error {
+ checkedInt := 0
+ if checked {
+ checkedInt = 1
+ }
+ _, err := s.db.Exec(`
+ INSERT INTO shopping_item_checks (source, item_id, checked, updated_at)
+ VALUES (?, ?, ?, CURRENT_TIMESTAMP)
+ ON CONFLICT(source, item_id) DO UPDATE SET checked = ?, updated_at = CURRENT_TIMESTAMP
+ `, source, itemID, checkedInt, checkedInt)
+ return err
+}
+
+// GetShoppingItemChecks returns a map of item_id -> checked for a given source
+func (s *Store) GetShoppingItemChecks(source string) (map[string]bool, error) {
+ rows, err := s.db.Query(`SELECT item_id, checked FROM shopping_item_checks WHERE source = ?`, source)
+ if err != nil {
+ return nil, err
+ }
+ defer func() { _ = rows.Close() }()
+
+ checks := make(map[string]bool)
+ for rows.Next() {
+ var itemID string
+ var checked int
+ if err := rows.Scan(&itemID, &checked); err != nil {
+ return nil, err
+ }
+ checks[itemID] = checked == 1
+ }
+ return checks, rows.Err()
+}
diff --git a/internal/store/shopping_test.go b/internal/store/shopping_test.go
new file mode 100644
index 0000000..01f106c
--- /dev/null
+++ b/internal/store/shopping_test.go
@@ -0,0 +1,151 @@
+package store
+
+import (
+ "database/sql"
+ "path/filepath"
+ "testing"
+
+ _ "github.com/mattn/go-sqlite3"
+)
+
+func setupTestStoreWithShopping(t *testing.T) *Store {
+ t.Helper()
+
+ tempDir := t.TempDir()
+ dbPath := filepath.Join(tempDir, "test.db")
+
+ db, err := sql.Open("sqlite3", dbPath)
+ if err != nil {
+ t.Fatalf("Failed to open test database: %v", err)
+ }
+
+ db.SetMaxOpenConns(1)
+ store := &Store{db: db}
+
+ schema := `
+ CREATE TABLE IF NOT EXISTS user_shopping_items (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ store TEXT NOT NULL,
+ checked INTEGER DEFAULT 0,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ );
+ CREATE TABLE IF NOT EXISTS shopping_item_checks (
+ source TEXT NOT NULL,
+ item_id TEXT NOT NULL,
+ checked INTEGER DEFAULT 0,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (source, item_id)
+ );
+ `
+ if _, err := db.Exec(schema); err != nil {
+ t.Fatalf("Failed to create schema: %v", err)
+ }
+
+ return store
+}
+
+func TestUserShoppingItems_CRUD(t *testing.T) {
+ store := setupTestStoreWithShopping(t)
+ defer func() { _ = store.Close() }()
+
+ // Save items
+ if err := store.SaveUserShoppingItem("Milk", "Costco"); err != nil {
+ t.Fatalf("Failed to save item: %v", err)
+ }
+ if err := store.SaveUserShoppingItem("Bread", "Safeway"); err != nil {
+ t.Fatalf("Failed to save second item: %v", err)
+ }
+
+ // Get items
+ items, err := store.GetUserShoppingItems()
+ if err != nil {
+ t.Fatalf("Failed to get items: %v", err)
+ }
+ if len(items) != 2 {
+ t.Errorf("Expected 2 items, got %d", len(items))
+ }
+
+ // Verify item data
+ var milkItem UserShoppingItem
+ for _, item := range items {
+ if item.Name == "Milk" {
+ milkItem = item
+ break
+ }
+ }
+ if milkItem.Name != "Milk" {
+ t.Error("Could not find Milk item")
+ }
+ if milkItem.Store != "Costco" {
+ t.Errorf("Expected store 'Costco', got '%s'", milkItem.Store)
+ }
+ if milkItem.Checked {
+ t.Error("New item should not be checked")
+ }
+
+ // Toggle item
+ if err := store.ToggleUserShoppingItem(milkItem.ID, true); err != nil {
+ t.Fatalf("Failed to toggle item: %v", err)
+ }
+
+ items, _ = store.GetUserShoppingItems()
+ for _, item := range items {
+ if item.ID == milkItem.ID && !item.Checked {
+ t.Error("Item should be checked after toggle")
+ }
+ }
+
+ // Delete item
+ if err := store.DeleteUserShoppingItem(milkItem.ID); err != nil {
+ t.Fatalf("Failed to delete item: %v", err)
+ }
+
+ items, _ = store.GetUserShoppingItems()
+ if len(items) != 1 {
+ t.Errorf("Expected 1 item after delete, got %d", len(items))
+ }
+}
+
+func TestShoppingItemChecks_ExternalSources(t *testing.T) {
+ store := setupTestStoreWithShopping(t)
+ defer func() { _ = store.Close() }()
+
+ // Set checked for trello item
+ if err := store.SetShoppingItemChecked("trello", "card-123", true); err != nil {
+ t.Fatalf("Failed to set trello checked: %v", err)
+ }
+
+ // Set checked for plantoeat item
+ if err := store.SetShoppingItemChecked("plantoeat", "pte-456", true); err != nil {
+ t.Fatalf("Failed to set plantoeat checked: %v", err)
+ }
+
+ // Get trello checks
+ trelloChecks, err := store.GetShoppingItemChecks("trello")
+ if err != nil {
+ t.Fatalf("Failed to get trello checks: %v", err)
+ }
+ if !trelloChecks["card-123"] {
+ t.Error("Expected trello card to be checked")
+ }
+
+ // Get plantoeat checks
+ pteChecks, err := store.GetShoppingItemChecks("plantoeat")
+ if err != nil {
+ t.Fatalf("Failed to get plantoeat checks: %v", err)
+ }
+ if !pteChecks["pte-456"] {
+ t.Error("Expected plantoeat item to be checked")
+ }
+
+ // Uncheck trello item
+ if err := store.SetShoppingItemChecked("trello", "card-123", false); err != nil {
+ t.Fatalf("Failed to uncheck trello item: %v", err)
+ }
+
+ trelloChecks, _ = store.GetShoppingItemChecks("trello")
+ if trelloChecks["card-123"] {
+ t.Error("Trello item should be unchecked after update")
+ }
+}
diff --git a/internal/store/sqlite.go b/internal/store/sqlite.go
index e8c9eac..1662c6a 100644
--- a/internal/store/sqlite.go
+++ b/internal/store/sqlite.go
@@ -386,85 +386,6 @@ func (s *Store) ClearSyncToken(service string) error {
}
// UserShoppingItem represents a user-added shopping item
-type UserShoppingItem struct {
- ID int64
- Name string
- Store string
- Checked bool
- CreatedAt time.Time
-}
-
-// SaveUserShoppingItem saves a new user shopping item
-func (s *Store) SaveUserShoppingItem(name, store string) error {
- _, err := s.db.Exec(`INSERT INTO user_shopping_items (name, store) VALUES (?, ?)`, name, store)
- return err
-}
-
-// GetUserShoppingItems retrieves all user shopping items
-func (s *Store) GetUserShoppingItems() ([]UserShoppingItem, error) {
- rows, err := s.db.Query(`SELECT id, name, store, checked, created_at FROM user_shopping_items ORDER BY store, created_at DESC`)
- if err != nil {
- return nil, err
- }
- defer func() { _ = rows.Close() }()
-
- var items []UserShoppingItem
- for rows.Next() {
- var item UserShoppingItem
- if err := rows.Scan(&item.ID, &item.Name, &item.Store, &item.Checked, &item.CreatedAt); err != nil {
- return nil, err
- }
- items = append(items, item)
- }
- return items, rows.Err()
-}
-
-// ToggleUserShoppingItem toggles the checked state of a user shopping item
-func (s *Store) ToggleUserShoppingItem(id int64, checked bool) error {
- _, err := s.db.Exec(`UPDATE user_shopping_items SET checked = ? WHERE id = ?`, checked, id)
- return err
-}
-
-// DeleteUserShoppingItem removes a user shopping item
-func (s *Store) DeleteUserShoppingItem(id int64) error {
- _, err := s.db.Exec(`DELETE FROM user_shopping_items WHERE id = ?`, id)
- return err
-}
-
-// SetShoppingItemChecked sets the checked state for an external shopping item
-func (s *Store) SetShoppingItemChecked(source, itemID string, checked bool) error {
- checkedInt := 0
- if checked {
- checkedInt = 1
- }
- _, err := s.db.Exec(`
- INSERT INTO shopping_item_checks (source, item_id, checked, updated_at)
- VALUES (?, ?, ?, CURRENT_TIMESTAMP)
- ON CONFLICT(source, item_id) DO UPDATE SET checked = ?, updated_at = CURRENT_TIMESTAMP
- `, source, itemID, checkedInt, checkedInt)
- return err
-}
-
-// GetShoppingItemChecks returns a map of item_id -> checked for a given source
-func (s *Store) GetShoppingItemChecks(source string) (map[string]bool, error) {
- rows, err := s.db.Query(`SELECT item_id, checked FROM shopping_item_checks WHERE source = ?`, source)
- if err != nil {
- return nil, err
- }
- defer func() { _ = rows.Close() }()
-
- checks := make(map[string]bool)
- for rows.Next() {
- var itemID string
- var checked int
- if err := rows.Scan(&itemID, &checked); err != nil {
- return nil, err
- }
- checks[itemID] = checked == 1
- }
- return checks, rows.Err()
-}
-
// GetCardsByDateRange retrieves cards due within a specific date range,
// including overdue cards (due before start) so they appear in the timeline.
func (s *Store) GetCardsByDateRange(start, end time.Time) ([]models.Card, error) {
diff --git a/internal/store/sqlite_test.go b/internal/store/sqlite_test.go
index 55e1510..dc9bfad 100644
--- a/internal/store/sqlite_test.go
+++ b/internal/store/sqlite_test.go
@@ -628,148 +628,6 @@ func TestGetCardsByDateRange(t *testing.T) {
// User Shopping Items Tests
// =============================================================================
-func setupTestStoreWithShopping(t *testing.T) *Store {
- t.Helper()
-
- tempDir := t.TempDir()
- dbPath := filepath.Join(tempDir, "test.db")
-
- db, err := sql.Open("sqlite3", dbPath)
- if err != nil {
- t.Fatalf("Failed to open test database: %v", err)
- }
-
- db.SetMaxOpenConns(1)
- store := &Store{db: db}
-
- schema := `
- CREATE TABLE IF NOT EXISTS user_shopping_items (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- name TEXT NOT NULL,
- store TEXT NOT NULL,
- checked INTEGER DEFAULT 0,
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
- );
- CREATE TABLE IF NOT EXISTS shopping_item_checks (
- source TEXT NOT NULL,
- item_id TEXT NOT NULL,
- checked INTEGER DEFAULT 0,
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
- PRIMARY KEY (source, item_id)
- );
- `
- if _, err := db.Exec(schema); err != nil {
- t.Fatalf("Failed to create schema: %v", err)
- }
-
- return store
-}
-
-func TestUserShoppingItems_CRUD(t *testing.T) {
- store := setupTestStoreWithShopping(t)
- defer func() { _ = store.Close() }()
-
- // Save items
- if err := store.SaveUserShoppingItem("Milk", "Costco"); err != nil {
- t.Fatalf("Failed to save item: %v", err)
- }
- if err := store.SaveUserShoppingItem("Bread", "Safeway"); err != nil {
- t.Fatalf("Failed to save second item: %v", err)
- }
-
- // Get items
- items, err := store.GetUserShoppingItems()
- if err != nil {
- t.Fatalf("Failed to get items: %v", err)
- }
- if len(items) != 2 {
- t.Errorf("Expected 2 items, got %d", len(items))
- }
-
- // Verify item data
- var milkItem UserShoppingItem
- for _, item := range items {
- if item.Name == "Milk" {
- milkItem = item
- break
- }
- }
- if milkItem.Name != "Milk" {
- t.Error("Could not find Milk item")
- }
- if milkItem.Store != "Costco" {
- t.Errorf("Expected store 'Costco', got '%s'", milkItem.Store)
- }
- if milkItem.Checked {
- t.Error("New item should not be checked")
- }
-
- // Toggle item
- if err := store.ToggleUserShoppingItem(milkItem.ID, true); err != nil {
- t.Fatalf("Failed to toggle item: %v", err)
- }
-
- items, _ = store.GetUserShoppingItems()
- for _, item := range items {
- if item.ID == milkItem.ID && !item.Checked {
- t.Error("Item should be checked after toggle")
- }
- }
-
- // Delete item
- if err := store.DeleteUserShoppingItem(milkItem.ID); err != nil {
- t.Fatalf("Failed to delete item: %v", err)
- }
-
- items, _ = store.GetUserShoppingItems()
- if len(items) != 1 {
- t.Errorf("Expected 1 item after delete, got %d", len(items))
- }
-}
-
-func TestShoppingItemChecks_ExternalSources(t *testing.T) {
- store := setupTestStoreWithShopping(t)
- defer func() { _ = store.Close() }()
-
- // Set checked for trello item
- if err := store.SetShoppingItemChecked("trello", "card-123", true); err != nil {
- t.Fatalf("Failed to set trello checked: %v", err)
- }
-
- // Set checked for plantoeat item
- if err := store.SetShoppingItemChecked("plantoeat", "pte-456", true); err != nil {
- t.Fatalf("Failed to set plantoeat checked: %v", err)
- }
-
- // Get trello checks
- trelloChecks, err := store.GetShoppingItemChecks("trello")
- if err != nil {
- t.Fatalf("Failed to get trello checks: %v", err)
- }
- if !trelloChecks["card-123"] {
- t.Error("Expected trello card to be checked")
- }
-
- // Get plantoeat checks
- pteChecks, err := store.GetShoppingItemChecks("plantoeat")
- if err != nil {
- t.Fatalf("Failed to get plantoeat checks: %v", err)
- }
- if !pteChecks["pte-456"] {
- t.Error("Expected plantoeat item to be checked")
- }
-
- // Uncheck trello item
- if err := store.SetShoppingItemChecked("trello", "card-123", false); err != nil {
- t.Fatalf("Failed to uncheck trello item: %v", err)
- }
-
- trelloChecks, _ = store.GetShoppingItemChecks("trello")
- if trelloChecks["card-123"] {
- t.Error("Trello item should be unchecked after update")
- }
-}
-
// =============================================================================
// Feature Toggles Tests
// =============================================================================