Spaces:
Runtime error
Runtime error
AzureAD\AdityaDevarshi
Units fix (backend): unit travels + integer guard + per-unit analytics
7434f35 | 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) | |
| } | |