milkclient / backend /internal /api /subscriptions.go
AzureAD\AdityaDevarshi
Units fix (backend): unit travels + integer guard + per-unit analytics
7434f35
Raw
History Blame Contribute Delete
4.53 kB
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)
}