Spaces:
Runtime error
Runtime error
AzureAD\AdityaDevarshi
Phase 7: org-scoped in-memory TTL cache + read caching + invalidation + HTTP ETags
affd5ec | package api | |
| import ( | |
| "net/http" | |
| "strconv" | |
| "time" | |
| ) | |
| // Analytics handlers. All endpoints are org-scoped (orgID from the token via | |
| // requireOrg) and read-only, gated behind PermAnalyticsView in the router. | |
| // | |
| // Date parameters (from/to/as_of) are inclusive 'YYYY-MM-DD' strings validated | |
| // with validDate when present. Defaults when omitted: | |
| // - from/to: a trailing 30-day window ending today (server local date), i.e. | |
| // from = today-29 days, to = today. | |
| // - as_of: today (server local date). | |
| const analyticsDateLayout = "2006-01-02" | |
| // dateRange resolves the from/to query params for an analytics request, | |
| // applying defaults and validation. On a validation failure it writes a 400 | |
| // VALIDATION error (naming the offending field) and returns ok=false. | |
| func dateRange(w http.ResponseWriter, r *http.Request) (from, to string, ok bool) { | |
| from = r.URL.Query().Get("from") | |
| to = r.URL.Query().Get("to") | |
| if from != "" && !validDate(from) { | |
| writeFieldError(w, http.StatusBadRequest, CodeValidation, "from must be a YYYY-MM-DD date", "from") | |
| return "", "", false | |
| } | |
| if to != "" && !validDate(to) { | |
| writeFieldError(w, http.StatusBadRequest, CodeValidation, "to must be a YYYY-MM-DD date", "to") | |
| return "", "", false | |
| } | |
| now := time.Now() | |
| if to == "" { | |
| to = now.Format(analyticsDateLayout) | |
| } | |
| if from == "" { | |
| from = now.AddDate(0, 0, -29).Format(analyticsDateLayout) | |
| } | |
| return from, to, true | |
| } | |
| // summary -> GET /api/analytics/summary?from=&to= | |
| func (s *Server) analyticsSummary(w http.ResponseWriter, r *http.Request) { | |
| orgID, ok := s.requireOrg(w, r) | |
| if !ok { | |
| return | |
| } | |
| from, to, ok := dateRange(w, r) | |
| if !ok { | |
| return | |
| } | |
| key := keyAnalytics(orgID, "summary", "from="+from+":to="+to) | |
| if b, hit := s.cacheGetJSON(key); hit { | |
| writeCachedBytesETag(w, r, b, analyticsTTL) | |
| return | |
| } | |
| res, err := s.store.SalesSummary(r.Context(), orgID, from, to) | |
| if handleStoreErr(w, err) { | |
| return | |
| } | |
| s.writeJSONCached(w, r, http.StatusOK, res, key, analyticsTTL) | |
| } | |
| // sales-trend -> GET /api/analytics/sales-trend?from=&to=&bucket=day|month | |
| func (s *Server) analyticsSalesTrend(w http.ResponseWriter, r *http.Request) { | |
| orgID, ok := s.requireOrg(w, r) | |
| if !ok { | |
| return | |
| } | |
| from, to, ok := dateRange(w, r) | |
| if !ok { | |
| return | |
| } | |
| bucket := r.URL.Query().Get("bucket") | |
| if bucket == "" { | |
| bucket = "day" | |
| } | |
| if bucket != "day" && bucket != "month" { | |
| writeFieldError(w, http.StatusBadRequest, CodeValidation, "bucket must be 'day' or 'month'", "bucket") | |
| return | |
| } | |
| key := keyAnalytics(orgID, "sales-trend", "from="+from+":to="+to+":bucket="+bucket) | |
| if b, hit := s.cacheGetJSON(key); hit { | |
| writeCachedBytesETag(w, r, b, analyticsTTL) | |
| return | |
| } | |
| res, err := s.store.SalesTrend(r.Context(), orgID, from, to, bucket) | |
| if handleStoreErr(w, err) { | |
| return | |
| } | |
| s.writeJSONCached(w, r, http.StatusOK, res, key, analyticsTTL) | |
| } | |
| // by-product -> GET /api/analytics/by-product?from=&to= | |
| func (s *Server) analyticsByProduct(w http.ResponseWriter, r *http.Request) { | |
| orgID, ok := s.requireOrg(w, r) | |
| if !ok { | |
| return | |
| } | |
| from, to, ok := dateRange(w, r) | |
| if !ok { | |
| return | |
| } | |
| key := keyAnalytics(orgID, "by-product", "from="+from+":to="+to) | |
| if b, hit := s.cacheGetJSON(key); hit { | |
| writeCachedBytesETag(w, r, b, analyticsTTL) | |
| return | |
| } | |
| res, err := s.store.SalesByProduct(r.Context(), orgID, from, to) | |
| if handleStoreErr(w, err) { | |
| return | |
| } | |
| s.writeJSONCached(w, r, http.StatusOK, res, key, analyticsTTL) | |
| } | |
| // top-customers -> GET /api/analytics/top-customers?from=&to=&limit= | |
| func (s *Server) analyticsTopCustomers(w http.ResponseWriter, r *http.Request) { | |
| orgID, ok := s.requireOrg(w, r) | |
| if !ok { | |
| return | |
| } | |
| from, to, ok := dateRange(w, r) | |
| if !ok { | |
| return | |
| } | |
| limit := queryInt(r, "limit", 10) | |
| key := keyAnalytics(orgID, "top-customers", "from="+from+":to="+to+":limit="+strconv.Itoa(limit)) | |
| if b, hit := s.cacheGetJSON(key); hit { | |
| writeCachedBytesETag(w, r, b, analyticsTTL) | |
| return | |
| } | |
| res, err := s.store.TopCustomers(r.Context(), orgID, from, to, limit) | |
| if handleStoreErr(w, err) { | |
| return | |
| } | |
| s.writeJSONCached(w, r, http.StatusOK, res, key, analyticsTTL) | |
| } | |
| // outstanding -> GET /api/analytics/outstanding?as_of= | |
| func (s *Server) analyticsOutstanding(w http.ResponseWriter, r *http.Request) { | |
| orgID, ok := s.requireOrg(w, r) | |
| if !ok { | |
| return | |
| } | |
| asOf := r.URL.Query().Get("as_of") | |
| if asOf != "" && !validDate(asOf) { | |
| writeFieldError(w, http.StatusBadRequest, CodeValidation, "as_of must be a YYYY-MM-DD date", "as_of") | |
| return | |
| } | |
| if asOf == "" { | |
| asOf = time.Now().Format(analyticsDateLayout) | |
| } | |
| key := keyAnalytics(orgID, "outstanding", "as_of="+asOf) | |
| if b, hit := s.cacheGetJSON(key); hit { | |
| writeCachedBytesETag(w, r, b, analyticsTTL) | |
| return | |
| } | |
| res, err := s.store.OutstandingAging(r.Context(), orgID, asOf) | |
| if handleStoreErr(w, err) { | |
| return | |
| } | |
| s.writeJSONCached(w, r, http.StatusOK, res, key, analyticsTTL) | |
| } | |
| // customer detail -> GET /api/analytics/customer/{id}?from=&to= | |
| func (s *Server) analyticsCustomer(w http.ResponseWriter, r *http.Request) { | |
| orgID, ok := s.requireOrg(w, r) | |
| if !ok { | |
| return | |
| } | |
| id, ok := pathID(w, r) | |
| if !ok { | |
| return | |
| } | |
| from, to, ok := dateRange(w, r) | |
| if !ok { | |
| return | |
| } | |
| key := keyAnalytics(orgID, "customer", "id="+strconv.FormatInt(id, 10)+":from="+from+":to="+to) | |
| if b, hit := s.cacheGetJSON(key); hit { | |
| writeCachedBytesETag(w, r, b, analyticsTTL) | |
| return | |
| } | |
| res, err := s.store.CustomerAnalytics(r.Context(), orgID, id, from, to) | |
| if handleStoreErr(w, err) { | |
| return | |
| } | |
| s.writeJSONCached(w, r, http.StatusOK, res, key, analyticsTTL) | |
| } | |