package handler import ( "context" "encoding/json" "errors" "io" "net/http" "chis_osi/pipeline" "chis_osi/source" ) const maxHealthRecordUpsertBody = 1 << 20 type HealthRecordUpserter interface { Upsert(context.Context, source.HealthRecordTask) (pipeline.Outcome, error) } type HealthRecordUpsertHandler struct { upserter HealthRecordUpserter } func NewHealthRecordUpsertHandler(upserter HealthRecordUpserter) *HealthRecordUpsertHandler { return &HealthRecordUpsertHandler{upserter: upserter} } func (h *HealthRecordUpsertHandler) Upsert(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.Header().Set("Allow", http.MethodPost) writeJSONError(w, http.StatusMethodNotAllowed, "only POST is supported") return } if h.upserter == nil { writeJSONError(w, http.StatusServiceUnavailable, "health record upsert is unavailable") return } r.Body = http.MaxBytesReader(w, r.Body, maxHealthRecordUpsertBody) raw, err := io.ReadAll(r.Body) if err != nil { var maxBytesErr *http.MaxBytesError if errors.As(err, &maxBytesErr) { writeJSONError(w, http.StatusRequestEntityTooLarge, "request body exceeds 1 MiB") return } writeJSONError(w, http.StatusBadRequest, "read request body failed") return } task, err := source.DecodeHealthRecordTask(raw) if err != nil { writeJSONError(w, http.StatusBadRequest, "invalid PHIS health record envelope") return } outcome, err := h.upserter.Upsert(r.Context(), task) if err != nil { response := healthRecordUpsertErrorResponse{Error: "health record upsert failed", RetrySafe: outcome.Status == ""} if outcome.Status != "" { response.Outcome = &outcome } writeJSON(w, http.StatusBadGateway, response) return } writeJSON(w, outcomeHTTPStatus(outcome), outcome) } type healthRecordUpsertErrorResponse struct { Error string `json:"error"` RetrySafe bool `json:"retrySafe"` Outcome *pipeline.Outcome `json:"outcome,omitempty"` } func outcomeHTTPStatus(outcome pipeline.Outcome) int { switch outcome.Status { case pipeline.StatusDone: return http.StatusOK case pipeline.StatusManualReview: return http.StatusConflict case pipeline.StatusRetry: return http.StatusServiceUnavailable case pipeline.StatusFailed: if outcome.ServiceID != "" { return http.StatusBadGateway } return http.StatusUnprocessableEntity default: return http.StatusInternalServerError } } func writeJSON(w http.ResponseWriter, status int, value any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(value) }