Files
backend_fiesta/services/deliverySlotService_test.go
2026-10-06 17:35:51 +05:30

258 lines
8.3 KiB
Go

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))
}
}