diff --git a/.gitignore b/.gitignore index 0873d77..84c3545 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ go.work.sum tmp tmp/main +foo.db diff --git a/TODO.md b/TODO.md index 0869934..f3c4382 100644 --- a/TODO.md +++ b/TODO.md @@ -8,6 +8,7 @@ - [ ] Create proper schemas - [ ] Implement data persistence - [ ] Add database migrations + - [ ] Add database caching - [ ] Input Validation @@ -120,6 +121,8 @@ - [ ] API Documentation - [ ] Unit Tests - [ ] Integration Tests +- [ ] File structure +- [ ] Embed templates ## Future Considerations diff --git a/main.go b/main.go index eb8f37b..eb8ac0e 100644 --- a/main.go +++ b/main.go @@ -16,7 +16,8 @@ func main() { log.Fatal("can't parse templates: ", err) } h.tmpl = templates - h.s = NewStore() + // #todo: error handling + h.s, _ = NewStore("./foo.db") h.s.saveCake(cake{"Sernik ulubiony", -1, 65, 0}) h.s.saveCake(cake{"Malinowa chmurka", -1, 150, 0}) diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..c707e86 --- /dev/null +++ b/schema.sql @@ -0,0 +1,24 @@ +create table cake ( + id integer primary key autoincrement, + name text not null unique, + price integer not null +); + +create table customer_order ( + id integer primary key autoincrement, + name text, + surname text, + phone text, + location text, + order_date string not null, + delivery_date string not null, + status text, + paid integer +); + +create table ordered_cake ( + customer_order integer references customer_order (id) not null, + cake integer references cake (id) not null, + amount integer not null, + primary key (customer_order, cake) +) diff --git a/store.go b/store.go index 7499083..1d93138 100644 --- a/store.go +++ b/store.go @@ -1,65 +1,144 @@ package main import ( - "errors" + "database/sql" + _ "embed" + "fmt" + "os" + "strconv" + "time" ) +//go:embed schema.sql +var schema string + type store struct { - cakes []cake - orders []order + db *sql.DB } -func NewStore() *store { +// #todo: implement database persistance +func NewStore(filename string) (*store, error) { var out store - out.cakes = make([]cake, 0) - out.orders = make([]order, 0) - return &out + var err error + if filename != ":memory:" { + os.Remove(filename) + } + // #todo: handle the error + out.db, err = sql.Open("sqlite3", filename) + if err != nil { + return nil, err + } + + _, err = out.db.Exec(schema) + if err != nil { + out.db.Close() + return nil, err + } + return &out, nil } +// #todo: should return an error func (s *store) getCakes() []cake { - return s.cakes -} - -func (s *store) saveCake(newCake cake) int { - if newCake.ID == -1 { - newCake.ID = len(s.cakes) - s.cakes = append(s.cakes, newCake) - return newCake.ID + // #todo: error handling + rows, err := s.db.Query("select id, name, price from cake") + defer rows.Close() + if err != nil { + return nil } - for i := range s.cakes { - if s.cakes[i].ID == newCake.ID { - s.cakes[i] = newCake - break + cakes := make([]cake, 0) + for rows.Next() { + var c cake + err = rows.Scan(&c.ID, &c.Name, &c.Price) + c.Amount = 0 + if err != nil { + fmt.Println("error scanning rows into cake: ", err) } + cakes = append(cakes, c) } - return newCake.ID + return cakes } +func (s *store) saveCake(newCake cake) (int, error) { + query := "insert into cake(name, price) values (?, ?) returning id;" + if newCake.ID != -1 { + query = "update cake set name = ? , price = ? where id = " + query += strconv.Itoa(newCake.ID) + " returning id;" + } + + // #todo: error handling + row, err := s.db.Query(query, newCake.Name, newCake.Price) + if err != nil { + return -1, err + } + defer row.Close() + + // #todo: see if next is required or not to get the first element + row.Next() + row.Scan(&newCake.ID) + return newCake.ID, nil +} + +// #todo: retrieve and save order contents func (s *store) getOrder(id int) (order, error) { - for _, e := range s.orders { - if e.ID == id { - return e, nil - } + var out order + // #todo: error handling + row, err := s.db.Query("select id, name, surname, phone, location, order_date, delivery_date, status, paid from customer_order where id = ?;", id) + defer row.Close() + if err != nil { + return out, err } - return order{}, errors.New("order not found") + + var order_date, delivery_date string + row.Next() + row.Scan(&out.ID, &out.Name, &out.Surname, &out.Phone, &out.Location, &order_date, &delivery_date, &out.Status, &out.Paid) + // #todo: error handling + out.Accepted, _ = time.Parse("2006-01-02 15:04", order_date) + out.Date, _ = time.Parse("2006-01-02 15:04", delivery_date) + return out, nil } +// #todo: should return an error func (s *store) getOrders() []order { - return s.orders -} - -func (s *store) saveOrder(newOrder order) int { - if newOrder.ID == -1 { - newOrder.ID = len(s.orders) - s.orders = append(s.orders, newOrder) - return newOrder.ID + var out []order + // #todo: error handling + row, err := s.db.Query("select id, name, surname, phone, location, order_date, delivery_date, status, paid from customer_order;") + defer row.Close() + if err != nil { + return out } - for i := range s.orders { - if s.orders[i].ID == newOrder.ID { - s.orders[i] = newOrder - break - } + for row.Next() { + var o order + var order_date, delivery_date string + row.Scan(&o.ID, &o.Name, &o.Surname, &o.Phone, &o.Location, &order_date, &delivery_date, &o.Status, &o.Paid) + // #todo: error handling + o.Accepted, _ = time.Parse("2006-01-02 15:04", order_date) + o.Date, _ = time.Parse("2006-01-02 15:04", delivery_date) + out = append(out, o) } - return newOrder.ID + + return out +} + +func (s *store) saveOrder(newOrder order) (int, error) { + query := "insert into customer_order(name, surname, phone, location, order_date, delivery_date, status, paid) values (?, ?, ?, ?, ?, ?, ?, ?) returning id;" + if newOrder.ID != -1 { + query = "update customer_order set name = ?, surname = ?, phone = ?, location = ?, order_date = ?, delivery_date = ?, status = ?, paid = ? where id = " + query += strconv.Itoa(newOrder.ID) + " returning id;" + } + + // #todo: error handling + // #todo: date format + accepted := newOrder.Accepted.Format("2006-01-02 15:04") + date := newOrder.Date.Format("2006-01-02 15:04") + row, err := s.db.Query(query, newOrder.Name, newOrder.Surname, newOrder.Phone, newOrder.Location, accepted, date, newOrder.Status, newOrder.Paid) + if err != nil { + return -1, err + } + defer row.Close() + + // #todo: see if next is required or not to get the first element + row.Next() + row.Scan(&newOrder.ID) + return newOrder.ID, nil } diff --git a/store_test.go b/store_test.go new file mode 100644 index 0000000..d22f74c --- /dev/null +++ b/store_test.go @@ -0,0 +1,140 @@ +package main + +import ( + "testing" + "time" +) + +func TestCakeOperations(t *testing.T) { + s, err := NewStore(":memory:") + if err != nil { + t.Fatal("Failed to open database", err) + } + defer s.db.Close() + + c := cake{ + ID: -1, + Name: "Test Cake", + Price: 10, + } + + id, err := s.saveCake(c) + if id == -1 || err != nil { + t.Fatal("Failed to save cake", err) + } + + cakes := s.getCakes() + if len(cakes) != 1 { + t.Errorf("Expected 1 cake, got %d", len(cakes)) + } + + if cakes[0].Name != "Test Cake" || cakes[0].Price != 10 { + t.Errorf("Cake data mismatch. Got %+v", cakes[0]) + } + + updatedCake := cake{ + ID: id, + Name: "Updated Cake", + Price: 15, + } + + newID, err := s.saveCake(updatedCake) + if newID != id { + t.Errorf("Update returned different ID. Expected %d, got %d", id, newID) + } + + cakes = s.getCakes() + if cakes[0].Name != "Updated Cake" || cakes[0].Price != 15 { + t.Errorf("Cake update failed. Got %+v", cakes[0]) + } +} + +func TestOrderOperations(t *testing.T) { + s, err := NewStore(":memory:") + if err != nil { + t.Fatal("Failed to open database", err) + } + defer s.db.Close() + + now := time.Now() + delivery := now.Add(24 * time.Hour) + + o := order{ + ID: -1, + Name: "John", + Surname: "Doe", + Phone: "1234567890", + Location: "123 Test St", + Accepted: now, + Date: delivery, + Status: "pending", + Paid: 0, + } + + id, err := s.saveOrder(o) + if id == -1 || err != nil { + t.Fatal("Failed to save order", err) + } + + retrieved, err := s.getOrder(id) + if err != nil { + t.Fatalf("Failed to get order: %v", err) + } + + if retrieved.Name != "John" || retrieved.Surname != "Doe" { + t.Errorf("Order names mismatch. Got %+v", retrieved) + } + + if !timeEqual(retrieved.Date, o.Date) || !timeEqual(retrieved.Accepted, o.Accepted) { + t.Errorf("Order dates mismatch. \nGot %+v\nWant %+v", retrieved, o) + } + + orders := s.getOrders() + if len(orders) != 1 { + t.Errorf("Expected 1 order, got %d", len(orders)) + } + + // Test updating an order + o.ID = id + o.Status = "completed" + + newID, err := s.saveOrder(o) + if err != nil { + t.Fatal("Failed to save order", err) + } + if newID != id { + t.Errorf("Update returned different ID. Expected %d, got %d", id, newID) + } + + updated, err := s.getOrder(id) + if err != nil { + t.Fatalf("Failed to get updated order: %v", err) + } + + if updated.Status != "completed" { + t.Errorf("Order update failed. Got %+v", updated) + } +} + +func TestStoreInitialization(t *testing.T) { + s, err := NewStore(":memory:") + if err != nil { + t.Fatal("Failed to open database", err) + } + defer s.db.Close() + + cakes := s.getCakes() + if len(cakes) != 0 { + t.Errorf("New store should have no cakes, got %d", len(cakes)) + } + + orders := s.getOrders() + if len(orders) != 0 { + t.Errorf("New store should have no orders, got %d", len(orders)) + } +} + +func timeEqual(t1, t2 time.Time) bool { + format := "2006-01-02 15:04" + return t1.Format(format) == t2.Format(format) +}