package repositories import ( "errors" "gorm.io/gorm" "gorm.io/gorm/clause" "nearle/models" ) type DeliverySlotRepository interface { ListForBranch(tenantID, locationID int) ([]models.DeliverySlots, error) FindForBranch(tenantID, locationID, slotID int) (*models.DeliverySlots, error) Save(slots []models.DeliverySlots) error } type deliverySlotRepository struct { db *gorm.DB } func NewDeliverySlotRepository(db *gorm.DB) DeliverySlotRepository { return &deliverySlotRepository{db: db} } // Ordered by start time so every caller — the console's editor and the app's // list alike — sees morning before evening without sorting it again. func (r *deliverySlotRepository) ListForBranch(tenantID, locationID int) ([]models.DeliverySlots, error) { slots := make([]models.DeliverySlots, 0, len(models.SlotKeys)) err := r.db.Table("deliveryslots"). Where("tenantid = ? AND locationid = ?", tenantID, locationID). Order("starttime ASC"). Find(&slots).Error if err != nil { return nil, err } return slots, nil } /* One window, scoped to the branch that claims it. The tenant and location are in the WHERE and not checked afterwards: a slot id arrives from the app, which carries no session, so it is a claim about which shop it belongs to. Looking it up by id alone and trusting the row would let one shop's checkout name another shop's window. */ func (r *deliverySlotRepository) FindForBranch(tenantID, locationID, slotID int) (*models.DeliverySlots, error) { var slot models.DeliverySlots err := r.db.Table("deliveryslots"). Where("slotid = ? AND tenantid = ? AND locationid = ?", slotID, tenantID, locationID). First(&slot).Error if errors.Is(err, gorm.ErrRecordNotFound) { // Not an error: "this shop does not offer that" is an answer, and the // service turns it into something a shopper can read. return nil, nil } if err != nil { return nil, err } return &slot, nil } /* Write a branch's windows, all of them or none. ── Upsert, not delete-then-insert ────────────────────────────────────────── Slot ids are referenced by `orders.deliveryslotid`. Replacing the rows would renumber them, and every order already placed would point at a window that no longer means what it did — or at nothing. The unique index on (tenantid, locationid, slotkey) is what makes the conflict target work, so a branch keeps one morning however many times it is edited. ── One transaction ───────────────────────────────────────────────────────── A shop editing all three and getting two is worse than getting none: the two that took are live and serving shoppers, and nothing on screen says which. */ func (r *deliverySlotRepository) Save(slots []models.DeliverySlots) error { if len(slots) == 0 { return nil } return r.db.Transaction(func(tx *gorm.DB) error { return tx.Table("deliveryslots"). Clauses(clause.OnConflict{ Columns: []clause.Column{ {Name: "tenantid"}, {Name: "locationid"}, {Name: "slotkey"}, }, DoUpdates: clause.AssignmentColumns([]string{"name", "starttime", "endtime", "status", "updated"}), }). Create(&slots).Error }) }