package opportunities import ( "encoding/json" "errors" "net/http" "strconv" "strings" "github.com/go-chi/chi/v5" "github.com/jackc/pgx/v5" ) // Handler serves the cached SAM opportunities read API. type Handler struct{ svc *Service } func NewHandler(svc *Service) *Handler { return &Handler{svc: svc} } // Routes mounts the opportunities endpoints under the /api group. func (h *Handler) Routes(r chi.Router) { r.Get("/opportunities", h.list) r.Get("/opportunities/{noticeID}", h.get) } func (h *Handler) list(w http.ResponseWriter, r *http.Request) { f := Filters{ NAICS: multiVals(r, "naics"), SetAsides: multiVals(r, "set_aside"), States: multiVals(r, "state"), Departments: multiVals(r, "department"), SubAgencies: multiVals(r, "sub_agency"), Q: strings.TrimSpace(r.URL.Query().Get("q")), ActiveOnly: activeOnly(r), Limit: intParam(r, "limit", defaultLimit), } items, err := h.svc.List(r.Context(), f) if err != nil { writeErr(w, err) return } writeJSON(w, http.StatusOK, items) } func (h *Handler) get(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "noticeID") item, err := h.svc.Get(r.Context(), id) if err != nil { if errors.Is(err, pgx.ErrNoRows) { writeJSON(w, http.StatusNotFound, map[string]string{"error": "opportunity not found"}) return } writeErr(w, err) return } writeJSON(w, http.StatusOK, item) } // activeOnly defaults to true; only an explicit false/0/no turns it off. func activeOnly(r *http.Request) bool { switch strings.ToLower(strings.TrimSpace(r.URL.Query().Get("active_only"))) { case "false", "0", "no": return false default: return true } } // multiVals collects a filter that may be repeated (?naics=a&naics=b) and/or comma-listed (?naics=a,b). func multiVals(r *http.Request, key string) []string { var out []string for _, v := range r.URL.Query()[key] { for _, p := range strings.Split(v, ",") { if p = strings.TrimSpace(p); p != "" { out = append(out, p) } } } return out } func intParam(r *http.Request, key string, def int) int { if v := strings.TrimSpace(r.URL.Query().Get(key)); v != "" { if n, err := strconv.Atoi(v); err == nil { return n } } return def } func writeErr(w http.ResponseWriter, err error) { if errors.Is(err, ErrNoDatabase) { writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "database not configured"}) return } writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal error"}) } func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) }