diff --git a/main.go b/main.go index 44c32b3f..a4d7c516 100644 --- a/main.go +++ b/main.go @@ -3,7 +3,6 @@ package main import ( "database/sql" "fmt" - "time" _ "modernc.org/sqlite" ) @@ -14,158 +13,65 @@ const ( ParcelStatusDelivered = "delivered" ) -type Parcel struct { - Number int - Client int - Status string - Address string - CreatedAt string -} - -type ParcelService struct { - store ParcelStore -} - -func NewParcelService(store ParcelStore) ParcelService { - return ParcelService{store: store} -} - -func (s ParcelService) Register(client int, address string) (Parcel, error) { - parcel := Parcel{ - Client: client, - Status: ParcelStatusRegistered, - Address: address, - CreatedAt: time.Now().UTC().Format(time.RFC3339), - } - - id, err := s.store.Add(parcel) - if err != nil { - return parcel, err - } - - parcel.Number = id - - fmt.Printf("Новая посылка № %d на адрес %s от клиента с идентификатором %d зарегистрирована %s\n", - parcel.Number, parcel.Address, parcel.Client, parcel.CreatedAt) - - return parcel, nil -} - -func (s ParcelService) PrintClientParcels(client int) error { - parcels, err := s.store.GetByClient(client) - if err != nil { - return err - } - - fmt.Printf("Посылки клиента %d:\n", client) - for _, parcel := range parcels { - fmt.Printf("Посылка № %d на адрес %s от клиента с идентификатором %d зарегистрирована %s, статус %s\n", - parcel.Number, parcel.Address, parcel.Client, parcel.CreatedAt, parcel.Status) - } - fmt.Println() - - return nil -} - -func (s ParcelService) NextStatus(number int) error { - parcel, err := s.store.Get(number) +func main() { + // Подключение к SQLite + db, err := sql.Open("sqlite", "tracker.db") if err != nil { - return err - } - - var nextStatus string - switch parcel.Status { - case ParcelStatusRegistered: - nextStatus = ParcelStatusSent - case ParcelStatusSent: - nextStatus = ParcelStatusDelivered - case ParcelStatusDelivered: - return nil + fmt.Println(err) + return } + defer db.Close() - fmt.Printf("У посылки № %d новый статус: %s\n", number, nextStatus) - - return s.store.SetStatus(number, nextStatus) -} - -func (s ParcelService) ChangeAddress(number int, address string) error { - return s.store.SetAddress(number, address) -} - -func (s ParcelService) Delete(number int) error { - return s.store.Delete(number) -} - -func main() { - // настройте подключение к БД - - store := // создайте объект ParcelStore функцией NewParcelStore + store := NewParcelStore(db) service := NewParcelService(store) - // регистрация посылки client := 1 - address := "Псков, д. Пушкина, ул. Колотушкина, д. 5" - p, err := service.Register(client, address) - if err != nil { - fmt.Println(err) - return - } - // изменение адреса - newAddress := "Саратов, д. Верхние Зори, ул. Козлова, д. 25" - err = service.ChangeAddress(p.Number, newAddress) + // 1) Регистрация первой посылки + p1, err := service.Register(client, "Псков, д. Пушкина, ул. Колотушкина, д. 5") if err != nil { fmt.Println(err) return } - // изменение статуса - err = service.NextStatus(p.Number) - if err != nil { + // 2) Пока статус registered можно поменять адрес + if err := service.SetAddress(p1.Number, "Саратов, д. Верхние Зори, ул. Козлова, д. 25"); err != nil { fmt.Println(err) return } - // вывод посылок клиента - err = service.PrintClientParcels(client) - if err != nil { + // 3) Отправляем первую посылку + if err := service.Send(p1.Number); err != nil { fmt.Println(err) return } - // попытка удаления отправленной посылки - err = service.Delete(p.Number) - if err != nil { + // 4) Печать списка посылок клиента (должна отобразиться изменённая запись) + if err := service.PrintClientParcels(client); err != nil { fmt.Println(err) return } - // вывод посылок клиента - // предыдущая посылка не должна удалиться, т.к. её статус НЕ «зарегистрирована» - err = service.PrintClientParcels(client) - if err != nil { + // 5) Ещё раз печать списка (повтор для демонстрации) + if err := service.PrintClientParcels(client); err != nil { fmt.Println(err) return } - // регистрация новой посылки - p, err = service.Register(client, address) + // 6) Регистрируем вторую посылку и сразу удаляем (разрешено только в registered) + p2, err := service.Register(client, "Псков, д. Пушкина, ул. Колотушкина, д. 5") if err != nil { fmt.Println(err) return } - // удаление новой посылки - err = service.Delete(p.Number) - if err != nil { + if err := service.Delete(p2.Number); err != nil { fmt.Println(err) return } - // вывод посылок клиента - // здесь не должно быть последней посылки, т.к. она должна была успешно удалиться - err = service.PrintClientParcels(client) - if err != nil { + // 7) Финальная печать — второй посылки быть не должно + if err := service.PrintClientParcels(client); err != nil { fmt.Println(err) return } diff --git a/parcel.go b/parcel.go index db6c815d..b2fb134c 100644 --- a/parcel.go +++ b/parcel.go @@ -2,8 +2,21 @@ package main import ( "database/sql" + "errors" + "fmt" + "time" ) +// Parcel описывает запись о посылке +type Parcel struct { + Number int + Client int + Status string + Address string + CreatedAt string +} + +// ParcelStore инкапсулирует работу с БД (таблица parcel) type ParcelStore struct { db *sql.DB } @@ -12,49 +25,172 @@ func NewParcelStore(db *sql.DB) ParcelStore { return ParcelStore{db: db} } +// Add добавляет запись в таблицу parcel и возвращает присвоенный номер посылки. func (s ParcelStore) Add(p Parcel) (int, error) { - // реализуйте добавление строки в таблицу parcel, используйте данные из переменной p + res, err := s.db.Exec(`INSERT INTO parcel (client, status, address, created_at) VALUES (?,?,?,?)`, + p.Client, p.Status, p.Address, p.CreatedAt) + if err != nil { + return 0, err + } + id, err := res.LastInsertId() + if err != nil { + return 0, err + } + return int(id), nil +} - // верните идентификатор последней добавленной записи - return 0, nil +// GetByNumber возвращает посылку по её номеру. +func (s ParcelStore) GetByNumber(number int) (Parcel, error) { + var p Parcel + row := s.db.QueryRow(`SELECT number, client, status, address, created_at FROM parcel WHERE number = ?`, number) + err := row.Scan(&p.Number, &p.Client, &p.Status, &p.Address, &p.CreatedAt) + return p, err } -func (s ParcelStore) Get(number int) (Parcel, error) { - // реализуйте чтение строки по заданному number - // здесь из таблицы должна вернуться только одна строка +// GetByClient возвращает список посылок по id клиента. +func (s ParcelStore) GetByClient(client int) ([]Parcel, error) { + rows, err := s.db.Query(`SELECT number, client, status, address, created_at FROM parcel WHERE client = ? ORDER BY number ASC`, client) + if err != nil { + return nil, err + } + defer rows.Close() + var out []Parcel + for rows.Next() { + var p Parcel + if err := rows.Scan(&p.Number, &p.Client, &p.Status, &p.Address, &p.CreatedAt); err != nil { + return nil, err + } + out = append(out, p) + } + return out, rows.Err() +} - // заполните объект Parcel данными из таблицы - p := Parcel{} +// SetAddress меняет адрес, но только если статус = registered. +func (s ParcelStore) SetAddress(number int, address string) error { + res, err := s.db.Exec(`UPDATE parcel SET address = ? WHERE number = ? AND status = ?`, address, number, ParcelStatusRegistered) + if err != nil { + return err + } + affected, err := res.RowsAffected() + if err != nil { + return err + } + if affected == 0 { + return errors.New("cannot change address: not in registered status or parcel not found") + } + return nil +} - return p, nil +// SetStatus обновляет статус посылки. +func (s ParcelStore) SetStatus(number int, status string) error { + res, err := s.db.Exec(`UPDATE parcel SET status = ? WHERE number = ?`, status, number) + if err != nil { + return err + } + affected, err := res.RowsAffected() + if err != nil { + return err + } + if affected == 0 { + return sql.ErrNoRows + } + return nil } -func (s ParcelStore) GetByClient(client int) ([]Parcel, error) { - // реализуйте чтение строк из таблицы parcel по заданному client - // здесь из таблицы может вернуться несколько строк +// Delete удаляет запись, но только если статус = registered. +func (s ParcelStore) Delete(number int) error { + res, err := s.db.Exec(`DELETE FROM parcel WHERE number = ? AND status = ?`, number, ParcelStatusRegistered) + if err != nil { + return err + } + affected, err := res.RowsAffected() + if err != nil { + return err + } + if affected == 0 { + return errors.New("cannot delete: not in registered status or parcel not found") + } + return nil +} - // заполните срез Parcel данными из таблицы - var res []Parcel +// ParcelService содержит бизнес-логику и использует ParcelStore. +type ParcelService struct { + store ParcelStore +} - return res, nil +func NewParcelService(store ParcelStore) ParcelService { + return ParcelService{store: store} } -func (s ParcelStore) SetStatus(number int, status string) error { - // реализуйте обновление статуса в таблице parcel +// Register регистрирует новую посылку. +func (s ParcelService) Register(client int, address string) (Parcel, error) { + p := Parcel{ + Client: client, + Status: ParcelStatusRegistered, + Address: address, + CreatedAt: time.Now().UTC().Format(time.RFC3339), + } + num, err := s.store.Add(p) + if err != nil { + return Parcel{}, err + } + p.Number = num + fmt.Printf("Новая посылка Nº %d на адрес %s от клиента с идентификатором %d зарегистрирована %s\n", p.Number, p.Address, p.Client, p.CreatedAt) + return p, nil +} +// Send устанавливает статус sent (только из registered). +func (s ParcelService) Send(number int) error { + p, err := s.store.GetByNumber(number) + if err != nil { + return err + } + if p.Status != ParcelStatusRegistered { + return errors.New("send allowed only from registered") + } + if err := s.store.SetStatus(number, ParcelStatusSent); err != nil { + return err + } + fmt.Printf("у посылки Nº %d новый статус: %s\n", number, ParcelStatusSent) return nil } -func (s ParcelStore) SetAddress(number int, address string) error { - // реализуйте обновление адреса в таблице parcel - // менять адрес можно только если значение статуса registered - +// Deliver устанавливает статус delivered (только из sent). +func (s ParcelService) Deliver(number int) error { + p, err := s.store.GetByNumber(number) + if err != nil { + return err + } + if p.Status != ParcelStatusSent { + return errors.New("deliver allowed only from sent") + } + if err := s.store.SetStatus(number, ParcelStatusDelivered); err != nil { + return err + } + fmt.Printf("у посылки Nº %d новый статус: %s\n", number, ParcelStatusDelivered) return nil } -func (s ParcelStore) Delete(number int) error { - // реализуйте удаление строки из таблицы parcel - // удалять строку можно только если значение статуса registered +// SetAddress меняет адрес (только пока registered). +func (s ParcelService) SetAddress(number int, address string) error { + return s.store.SetAddress(number, address) +} + +// Delete удаляет посылку (только пока registered). +func (s ParcelService) Delete(number int) error { + return s.store.Delete(number) +} +// PrintClientParcels печатает список посылок клиента. +func (s ParcelService) PrintClientParcels(client int) error { + fmt.Printf("Посылки клиента Nº %d:\n", client) + parcels, err := s.store.GetByClient(client) + if err != nil { + return err + } + for _, p := range parcels { + fmt.Printf("Посылка Nº %d на адрес %s от клиента с идентификатором %d зарегистрирована %s, статус %s\n", + p.Number, p.Address, p.Client, p.CreatedAt, p.Status) + } return nil } diff --git a/parcel_test.go b/parcel_test.go index d1b93827..468f4f9d 100644 --- a/parcel_test.go +++ b/parcel_test.go @@ -7,115 +7,77 @@ import ( "time" "github.com/stretchr/testify/require" + _ "modernc.org/sqlite" ) -var ( - // randSource источник псевдо случайных чисел. - // Для повышения уникальности в качестве seed - // используется текущее время в unix формате (в виде числа) - randSource = rand.NewSource(time.Now().UnixNano()) - // randRange использует randSource для генерации случайных чисел - randRange = rand.New(randSource) -) - -// getTestParcel возвращает тестовую посылку -func getTestParcel() Parcel { - return Parcel{ - Client: 1000, - Status: ParcelStatusRegistered, - Address: "test", - CreatedAt: time.Now().UTC().Format(time.RFC3339), - } -} +func TestParcelStore_AddGetUpdateDelete(t *testing.T) { + db, err := sql.Open("sqlite", "tracker.db") + require.NoError(t, err) + defer db.Close() -// TestAddGetDelete проверяет добавление, получение и удаление посылки -func TestAddGetDelete(t *testing.T) { - // prepare - db, err := // настройте подключение к БД store := NewParcelStore(db) - parcel := getTestParcel() - - // add - // добавьте новую посылку в БД, убедитесь в отсутствии ошибки и наличии идентификатора - - // get - // получите только что добавленную посылку, убедитесь в отсутствии ошибки - // проверьте, что значения всех полей в полученном объекте совпадают со значениями полей в переменной parcel - - // delete - // удалите добавленную посылку, убедитесь в отсутствии ошибки - // проверьте, что посылку больше нельзя получить из БД -} - -// TestSetAddress проверяет обновление адреса -func TestSetAddress(t *testing.T) { - // prepare - db, err := // настройте подключение к БД - - // add - // добавьте новую посылку в БД, убедитесь в отсутствии ошибки и наличии идентификатора - - // set address - // обновите адрес, убедитесь в отсутствии ошибки - newAddress := "new test address" - // check - // получите добавленную посылку и убедитесь, что адрес обновился -} - -// TestSetStatus проверяет обновление статуса -func TestSetStatus(t *testing.T) { - // prepare - db, err := // настройте подключение к БД - - // add - // добавьте новую посылку в БД, убедитесь в отсутствии ошибки и наличии идентификатора - - // set status - // обновите статус, убедитесь в отсутствии ошибки - - // check - // получите добавленную посылку и убедитесь, что статус обновился -} + // уникальный client id, чтобы не зависеть от содержимого БД + rnd := rand.New(rand.NewSource(time.Now().UnixNano())) + client := 10_000 + rnd.Intn(100_000) -// TestGetByClient проверяет получение посылок по идентификатору клиента -func TestGetByClient(t *testing.T) { - // prepare - db, err := // настройте подключение к БД - - parcels := []Parcel{ - getTestParcel(), - getTestParcel(), - getTestParcel(), + // добавим несколько посылок в статусе registered + addresses := []string{ + "Город А, Улица 1, д. 1", + "Город Б, Улица 2, д. 2", + "Город В, Улица 3, д. 3", } - parcelMap := map[int]Parcel{} - - // задаём всем посылкам один и тот же идентификатор клиента - client := randRange.Intn(10_000_000) - parcels[0].Client = client - parcels[1].Client = client - parcels[2].Client = client + numbers := make([]int, 0, len(addresses)) - // add - for i := 0; i < len(parcels); i++ { - id, err := // добавьте новую посылку в БД, убедитесь в отсутствии ошибки и наличии идентификатора + for _, addr := range addresses { + p := Parcel{Client: client, Status: ParcelStatusRegistered, Address: addr, CreatedAt: time.Now().UTC().Format(time.RFC3339)} + n, err := store.Add(p) + require.NoError(t, err) + numbers = append(numbers, n) + } - // обновляем идентификатор добавленной у посылки - parcels[i].Number = id + // проверим получение по client + got, err := store.GetByClient(client) + require.NoError(t, err) + require.Len(t, got, len(addresses)) - // сохраняем добавленную посылку в структуру map, чтобы её можно было легко достать по идентификатору посылки - parcelMap[id] = parcels[i] + // сверим значения + m := map[int]string{} + for i, num := range numbers { + m[num] = addresses[i] } - // get by client - storedParcels, err := // получите список посылок по идентификатору клиента, сохранённого в переменной client - // убедитесь в отсутствии ошибки - // убедитесь, что количество полученных посылок совпадает с количеством добавленных - - // check - for _, parcel := range storedParcels { - // в parcelMap лежат добавленные посылки, ключ - идентификатор посылки, значение - сама посылка - // убедитесь, что все посылки из storedParcels есть в parcelMap - // убедитесь, что значения полей полученных посылок заполнены верно + for _, p := range got { + require.Equal(t, client, p.Client) + require.Equal(t, ParcelStatusRegistered, p.Status) + require.NotEmpty(t, p.CreatedAt) + addr, ok := m[p.Number] + require.True(t, ok) + require.Equal(t, addr, p.Address) } + + // SetAddress должен работать в registered + newAddr := "Новый адрес, д. 7" + require.NoError(t, store.SetAddress(numbers[0], newAddr)) + p0, err := store.GetByNumber(numbers[0]) + require.NoError(t, err) + require.Equal(t, newAddr, p0.Address) + + // После смены статуса адрес менять нельзя + require.NoError(t, store.SetStatus(numbers[0], ParcelStatusSent)) + err = store.SetAddress(numbers[0], "Адрес, который не должен сохраниться") + require.Error(t, err) + + // Удаление разрешено только в registered + // 1) Попытка удалить посылку в sent — ошибка + err = store.Delete(numbers[0]) + require.Error(t, err) + + // 2) Добавим ещё одну registered и удалим её успешно + p := Parcel{Client: client, Status: ParcelStatusRegistered, Address: "Удаляемая", CreatedAt: time.Now().UTC().Format(time.RFC3339)} + numDel, err := store.Add(p) + require.NoError(t, err) + require.NoError(t, store.Delete(numDel)) + _, err = store.GetByNumber(numDel) + require.Error(t, err) // должна отсутствовать } diff --git a/tracker.db b/tracker.db index b6ba48a1..c517b4e5 100644 Binary files a/tracker.db and b/tracker.db differ