package api import ( "errors" "net/http" "milkway/internal/models" "milkway/internal/store" ) // validateSubscriptionRefs verifies the referenced product and customer both // belong to the org, and that the morning/evening quantities are whole numbers // when the product is sold by count (piece/dozen/packet). It returns false (after // writing an error) on any problem. func (s *Server) validateSubscriptionRefs(w http.ResponseWriter, r *http.Request, orgID int64, sub models.Subscription) bool { if _, err := s.store.GetCustomer(r.Context(), orgID, sub.CustomerID); err != nil { if errors.Is(err, store.ErrNotFound) { writeError(w, http.StatusBadRequest, "customer not found") return false } internalError(w, err) return false } product, err := s.store.GetProduct(r.Context(), orgID, sub.ProductID) if err != nil { if errors.Is(err, store.ErrNotFound) { writeError(w, http.StatusBadRequest, "product not found") return false } internalError(w, err) return false } if integerUnits[product.Unit] { if !isIntegerQty(sub.MorningQty) { writeFieldError(w, http.StatusBadRequest, CodeValidation, "morning_qty must be a whole number for unit "+product.Unit, "morning_qty") return false } if !isIntegerQty(sub.EveningQty) { writeFieldError(w, http.StatusBadRequest, CodeValidation, "evening_qty must be a whole number for unit "+product.Unit, "evening_qty") return false } } return true } func (s *Server) listSubscriptions(w http.ResponseWriter, r *http.Request) { orgID, ok := s.requireOrg(w, r) if !ok { return } customerID := queryCustomerID(r) // By default only active subscriptions are returned; ?all=true includes inactive. activeOnly := r.URL.Query().Get("all") != "true" q, limit, offset := pageParams(r) subs, total, err := s.store.ListSubscriptionsPage(r.Context(), orgID, customerID, activeOnly, q, limit, offset) if handleStoreErr(w, err) { return } writeJSON(w, http.StatusOK, models.Page[models.Subscription]{Items: subs, Total: total, Limit: limit, Offset: offset}) } func (s *Server) getSubscription(w http.ResponseWriter, r *http.Request) { orgID, ok := s.requireOrg(w, r) if !ok { return } id, ok := pathID(w, r) if !ok { return } sub, err := s.store.GetSubscription(r.Context(), orgID, id) if handleStoreErr(w, err) { return } writeJSON(w, http.StatusOK, sub) } // validateSubscriptionBody validates the user-supplied fields common to create // and update via the struct validator (gt=0 ids, gte=0 quantities, days_of_week // in 0..6, optional YYYY-MM-DD start_date). It does not touch ID/OrgID (callers // set those from trusted input). func validateSubscriptionBody(w http.ResponseWriter, sub models.Subscription) bool { return validateStruct(w, sub) } func (s *Server) createSubscription(w http.ResponseWriter, r *http.Request) { orgID, ok := s.requireOrg(w, r) if !ok { return } var sub models.Subscription if !decodeJSON(w, r, &sub) { return } if !validateSubscriptionBody(w, sub) { return } if !s.validateSubscriptionRefs(w, r, orgID, sub) { return } // A standing order with no days never delivers; default to every day. if len(sub.DaysOfWeek) == 0 { sub.DaysOfWeek = []int{0, 1, 2, 3, 4, 5, 6} } sub.OrgID = orgID sub.Active = true // new subscriptions are active by default created, err := s.store.CreateSubscription(r.Context(), sub) if handleStoreErr(w, err) { return } s.audit(r, "create", "subscription", created.ID) writeJSON(w, http.StatusCreated, created) } func (s *Server) updateSubscription(w http.ResponseWriter, r *http.Request) { orgID, ok := s.requireOrg(w, r) if !ok { return } id, ok := pathID(w, r) if !ok { return } var sub models.Subscription if !decodeJSON(w, r, &sub) { return } if !validateSubscriptionBody(w, sub) { return } if !s.validateSubscriptionRefs(w, r, orgID, sub) { return } if sub.DaysOfWeek == nil { sub.DaysOfWeek = []int{} } sub.ID = id sub.OrgID = orgID updated, err := s.store.UpdateSubscription(r.Context(), sub) if handleStoreErr(w, err) { return } s.audit(r, "update", "subscription", updated.ID) writeJSON(w, http.StatusOK, updated) } func (s *Server) deleteSubscription(w http.ResponseWriter, r *http.Request) { orgID, ok := s.requireOrg(w, r) if !ok { return } id, ok := pathID(w, r) if !ok { return } if handleStoreErr(w, s.store.DeleteSubscription(r.Context(), orgID, id)) { return } s.audit(r, "delete", "subscription", id) w.WriteHeader(http.StatusNoContent) }