delivery slot api creation
This commit is contained in:
257
services/deliverySlotService_test.go
Normal file
257
services/deliverySlotService_test.go
Normal file
@@ -0,0 +1,257 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"nearle/models"
|
||||
)
|
||||
|
||||
// A stand-in for the table, so these tests are about the RULE and not about
|
||||
// GORM. Every test below fixes "now" explicitly, because a rule about clock
|
||||
// time that is tested at whatever o'clock CI happens to run is not tested.
|
||||
type fakeSlotRepo struct {
|
||||
slots []models.DeliverySlots
|
||||
}
|
||||
|
||||
func (f *fakeSlotRepo) ListForBranch(tenantID, locationID int) ([]models.DeliverySlots, error) {
|
||||
return f.slots, nil
|
||||
}
|
||||
|
||||
func (f *fakeSlotRepo) FindForBranch(tenantID, locationID, slotID int) (*models.DeliverySlots, error) {
|
||||
for i := range f.slots {
|
||||
if f.slots[i].Slotid == slotID {
|
||||
return &f.slots[i], nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeSlotRepo) Save(slots []models.DeliverySlots) error {
|
||||
f.slots = slots
|
||||
return nil
|
||||
}
|
||||
|
||||
func threeSlots() []models.DeliverySlots {
|
||||
return []models.DeliverySlots{
|
||||
{Slotid: 1, Slotkey: "morning", Name: "Morning", Starttime: "08:00", Endtime: "10:00", Status: "active"},
|
||||
{Slotid: 2, Slotkey: "afternoon", Name: "Afternoon", Starttime: "12:00", Endtime: "15:00", Status: "active"},
|
||||
{Slotid: 3, Slotkey: "evening", Name: "Evening", Starttime: "17:00", Endtime: "20:00", Status: "active"},
|
||||
}
|
||||
}
|
||||
|
||||
func serviceAt(clock string, slots []models.DeliverySlots) *deliverySlotService {
|
||||
at, err := time.Parse("2006-01-02 15:04", clock)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &deliverySlotService{
|
||||
repo: &fakeSlotRepo{slots: slots},
|
||||
now: func() time.Time { return at },
|
||||
}
|
||||
}
|
||||
|
||||
// Today's windows that have not ended, soonest first.
|
||||
func todayKeys(t *testing.T, svc *deliverySlotService, date string) []string {
|
||||
t.Helper()
|
||||
available, err := svc.Available(1, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("Available: %v", err)
|
||||
}
|
||||
keys := []string{}
|
||||
for _, slot := range available {
|
||||
if slot.Slotdate == date {
|
||||
keys = append(keys, slot.Slotkey)
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func TestAvailableDropsWindowsThatHaveEnded(t *testing.T) {
|
||||
// The rule the shop was given: a window takes orders right up to the moment
|
||||
// it ends, and then stops being offered.
|
||||
cases := []struct {
|
||||
clock string
|
||||
want string
|
||||
}{
|
||||
{"2026-10-06 07:00", "morning,afternoon,evening"}, // before trading
|
||||
{"2026-10-06 09:59", "morning,afternoon,evening"}, // one minute left
|
||||
{"2026-10-06 10:01", "afternoon,evening"}, // morning just closed
|
||||
{"2026-10-06 15:30", "evening"}, // afternoon gone too
|
||||
{"2026-10-06 20:01", ""}, // day over
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
svc := serviceAt(tc.clock, threeSlots())
|
||||
got := strings.Join(todayKeys(t, svc, "2026-10-06"), ",")
|
||||
if got != tc.want {
|
||||
t.Errorf("at %s: today offers %q, want %q", tc.clock, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAvailableClosesOnTheEndMinuteNotTheStart(t *testing.T) {
|
||||
// The boundary, stated on its own because it is the one thing a shopkeeper
|
||||
// would notice being wrong. 10:00 exactly is PAST the end of an 08:00-10:00
|
||||
// window: the window is over, and an order placed at 10:00 cannot be in it.
|
||||
svc := serviceAt("2026-10-06 10:00", threeSlots())
|
||||
for _, key := range todayKeys(t, svc, "2026-10-06") {
|
||||
if key == "morning" {
|
||||
t.Error("morning is still on offer at 10:00, its own end time")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAvailableAlwaysOffersTomorrow(t *testing.T) {
|
||||
// A shopper at 9pm is ordering for tomorrow. Offering nothing would quietly
|
||||
// drop the feature for every evening customer.
|
||||
svc := serviceAt("2026-10-06 21:00", threeSlots())
|
||||
available, err := svc.Available(1, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("Available: %v", err)
|
||||
}
|
||||
|
||||
if len(available) != 3 {
|
||||
t.Fatalf("after the last window closed, got %d slots, want tomorrow's 3", len(available))
|
||||
}
|
||||
for _, slot := range available {
|
||||
if slot.Slotdate != "2026-10-07" {
|
||||
t.Errorf("%s is dated %s, want tomorrow", slot.Slotkey, slot.Slotdate)
|
||||
}
|
||||
if !slot.IsTomorrow {
|
||||
t.Errorf("%s is tomorrow's but istomorrow is false", slot.Slotkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAvailableSkipsInactiveWindows(t *testing.T) {
|
||||
slots := threeSlots()
|
||||
slots[1].Status = "inactive" // the shop stopped doing afternoons
|
||||
|
||||
svc := serviceAt("2026-10-06 07:00", slots)
|
||||
for _, key := range todayKeys(t, svc, "2026-10-06") {
|
||||
if key == "afternoon" {
|
||||
t.Error("an inactive window is being offered to shoppers")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAvailableIsEmptyAndNotAnErrorWhenNothingIsConfigured(t *testing.T) {
|
||||
// The state EVERY shop is in the day this ships. An error here would turn
|
||||
// every trading shop on the platform into a broken one.
|
||||
svc := serviceAt("2026-10-06 09:00", nil)
|
||||
|
||||
available, err := svc.Available(1, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("a branch with no windows must not be an error, got %v", err)
|
||||
}
|
||||
if available == nil {
|
||||
t.Error("got nil, want an empty slice — `null` and `[]` read differently in the app")
|
||||
}
|
||||
if len(available) != 0 {
|
||||
t.Errorf("got %d slots from a branch that configured none", len(available))
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateForOrderAcceptsNoWindow(t *testing.T) {
|
||||
// Every order placed before this shipped, and every order from a branch
|
||||
// that has set no windows.
|
||||
svc := serviceAt("2026-10-06 09:00", threeSlots())
|
||||
if err := svc.ValidateForOrder(1, 1, 0, ""); err != nil {
|
||||
t.Errorf("an order with no window was rejected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateForOrderRejectsAWindowThatClosedWhileCheckingOut(t *testing.T) {
|
||||
// The case this guard exists for: the shopper opened checkout at 09:58 and
|
||||
// paid at 10:02.
|
||||
svc := serviceAt("2026-10-06 10:02", threeSlots())
|
||||
|
||||
err := svc.ValidateForOrder(1, 1, 1, "2026-10-06")
|
||||
if err == nil {
|
||||
t.Fatal("an order was accepted into a window that had already closed")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "morning") {
|
||||
t.Errorf("the message does not name the window: %q", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateForOrderAcceptsTomorrowsWindowAfterTodaysClosed(t *testing.T) {
|
||||
// Same window, next day. Closing today must not close it forever.
|
||||
svc := serviceAt("2026-10-06 10:02", threeSlots())
|
||||
if err := svc.ValidateForOrder(1, 1, 1, "2026-10-07"); err != nil {
|
||||
t.Errorf("tomorrow's morning was rejected at 10:02 today: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateForOrderRejectsAWindowFromAnotherShop(t *testing.T) {
|
||||
// The repository scopes its lookup by tenant and branch, so a slot id that
|
||||
// belongs elsewhere comes back as nothing.
|
||||
svc := serviceAt("2026-10-06 09:00", threeSlots())
|
||||
|
||||
if err := svc.ValidateForOrder(1, 1, 99, "2026-10-06"); err == nil {
|
||||
t.Error("an order named a window this shop does not offer and was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateForOrderRequiresADateWithAWindow(t *testing.T) {
|
||||
// "evening" alone cannot say tonight or tomorrow night.
|
||||
svc := serviceAt("2026-10-06 09:00", threeSlots())
|
||||
|
||||
if err := svc.ValidateForOrder(1, 1, 3, ""); err == nil {
|
||||
t.Error("a window was accepted with no date")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveRejectsAWindowThatEndsBeforeItStarts(t *testing.T) {
|
||||
// Not pedantry: such a window never passes the "still running" test, so it
|
||||
// would simply never appear, with nothing saying why.
|
||||
svc := serviceAt("2026-10-06 09:00", nil)
|
||||
|
||||
err := svc.Save(1, 1, []models.DeliverySlots{
|
||||
{Slotkey: "morning", Starttime: "10:00", Endtime: "08:00", Status: "active"},
|
||||
})
|
||||
if err == nil {
|
||||
t.Error("a window ending before it starts was saved")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveRejectsADuplicateWindow(t *testing.T) {
|
||||
svc := serviceAt("2026-10-06 09:00", nil)
|
||||
|
||||
err := svc.Save(1, 1, []models.DeliverySlots{
|
||||
{Slotkey: "morning", Starttime: "08:00", Endtime: "10:00", Status: "active"},
|
||||
{Slotkey: "morning", Starttime: "09:00", Endtime: "11:00", Status: "active"},
|
||||
})
|
||||
if err == nil {
|
||||
t.Error("a branch was given two mornings")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveRejectsAKeyThatIsNotOneOfTheThree(t *testing.T) {
|
||||
svc := serviceAt("2026-10-06 09:00", nil)
|
||||
|
||||
err := svc.Save(1, 1, []models.DeliverySlots{
|
||||
{Slotkey: "midnight", Starttime: "00:00", Endtime: "02:00", Status: "active"},
|
||||
})
|
||||
if err == nil {
|
||||
t.Error("a fourth window was accepted; the app has nowhere to show it")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveAppliesNothingWhenOneRowIsWrong(t *testing.T) {
|
||||
// A shop editing three and getting two is worse than getting none: the two
|
||||
// that took are live, and nothing on screen says which.
|
||||
repo := &fakeSlotRepo{}
|
||||
svc := &deliverySlotService{repo: repo, now: time.Now}
|
||||
|
||||
_ = svc.Save(1, 1, []models.DeliverySlots{
|
||||
{Slotkey: "morning", Starttime: "08:00", Endtime: "10:00", Status: "active"},
|
||||
{Slotkey: "afternoon", Starttime: "15:00", Endtime: "12:00", Status: "active"}, // bad
|
||||
})
|
||||
|
||||
if len(repo.slots) != 0 {
|
||||
t.Errorf("%d windows were written despite one being invalid", len(repo.slots))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user