package api import ( "encoding/json" "net/http" "time" "github.com/jacek/pamietnik/backend/internal/db" "github.com/jacek/pamietnik/backend/internal/domain" ) // Compile-time check: *db.TrackpointStore must satisfy db.TrackpointStorer. var _ db.TrackpointStorer = (*db.TrackpointStore)(nil) type trackpointInput struct { EventID string `json:"event_id"` DeviceID string `json:"device_id"` TripID string `json:"trip_id"` Timestamp string `json:"timestamp"` // RFC3339 Lat float64 `json:"lat"` Lon float64 `json:"lon"` Source string `json:"source"` Note string `json:"note,omitempty"` AccuracyM *float64 `json:"accuracy_m,omitempty"` SpeedMps *float64 `json:"speed_mps,omitempty"` BearingDeg *float64 `json:"bearing_deg,omitempty"` AltitudeM *float64 `json:"altitude_m,omitempty"` } func (t trackpointInput) toDomain() (domain.Trackpoint, error) { ts, err := time.Parse(time.RFC3339, t.Timestamp) if err != nil { return domain.Trackpoint{}, err } src := t.Source if src == "" { src = "gps" } return domain.Trackpoint{ EventID: t.EventID, DeviceID: t.DeviceID, TripID: t.TripID, Timestamp: ts, Lat: t.Lat, Lon: t.Lon, Source: src, Note: t.Note, AccuracyM: t.AccuracyM, SpeedMps: t.SpeedMps, BearingDeg: t.BearingDeg, AltitudeM: t.AltitudeM, }, nil } type batchResponse struct { ServerTime string `json:"server_time"` AcceptedIDs []string `json:"accepted_ids"` Rejected []db.RejectedItem `json:"rejected"` } // HandleSingleTrackpoint handles POST /v1/trackpoints func HandleSingleTrackpoint(store db.TrackpointStorer) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var input trackpointInput if err := json.NewDecoder(r.Body).Decode(&input); err != nil { writeError(w, http.StatusBadRequest, "BAD_REQUEST", "invalid JSON") return } point, err := input.toDomain() if err != nil { writeError(w, http.StatusBadRequest, "BAD_REQUEST", "invalid timestamp: "+err.Error()) return } userID := userIDFromContext(r.Context()) accepted, rejected, err := store.UpsertBatch(r.Context(), userID, []domain.Trackpoint{point}) if err != nil { writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "database error") return } writeJSON(w, http.StatusOK, batchResponse{ ServerTime: time.Now().UTC().Format(time.RFC3339), AcceptedIDs: accepted, Rejected: rejected, }) } } // HandleBatchTrackpoints handles POST /v1/trackpoints:batch func HandleBatchTrackpoints(store db.TrackpointStorer) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var inputs []trackpointInput if err := json.NewDecoder(r.Body).Decode(&inputs); err != nil { writeError(w, http.StatusBadRequest, "BAD_REQUEST", "invalid JSON") return } if len(inputs) == 0 { writeError(w, http.StatusBadRequest, "BAD_REQUEST", "empty batch") return } if len(inputs) > 500 { writeError(w, http.StatusBadRequest, "TOO_LARGE", "batch exceeds 500 items") return } points := make([]domain.Trackpoint, 0, len(inputs)) var parseRejected []db.RejectedItem for _, inp := range inputs { p, err := inp.toDomain() if err != nil { parseRejected = append(parseRejected, db.RejectedItem{ EventID: inp.EventID, Code: "INVALID_TIMESTAMP", Message: err.Error(), }) continue } points = append(points, p) } userID := userIDFromContext(r.Context()) accepted, rejected, err := store.UpsertBatch(r.Context(), userID, points) if err != nil { writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "database error") return } rejected = append(rejected, parseRejected...) writeJSON(w, http.StatusOK, batchResponse{ ServerTime: time.Now().UTC().Format(time.RFC3339), AcceptedIDs: accepted, Rejected: rejected, }) } }