Files
apocapoc-api/internal/infrastructure/http/sync_handlers.go
T
2026-03-07 04:36:52 +01:00

232 lines
6.5 KiB
Go

package http
import (
"encoding/json"
"net/http"
"time"
"apocapoc-api/internal/application/commands"
"apocapoc-api/internal/application/queries"
"apocapoc-api/internal/domain/entities"
"apocapoc-api/internal/i18n"
"apocapoc-api/internal/shared/errors"
)
type SyncHandlers struct {
getSyncChangesHandler *queries.GetSyncChangesHandler
applySyncBatchHandler *commands.ApplySyncBatchHandler
translator *i18n.Translator
}
func NewSyncHandlers(
getSyncChangesHandler *queries.GetSyncChangesHandler,
applySyncBatchHandler *commands.ApplySyncBatchHandler,
translator *i18n.Translator,
) *SyncHandlers {
return &SyncHandlers{
getSyncChangesHandler: getSyncChangesHandler,
applySyncBatchHandler: applySyncBatchHandler,
translator: translator,
}
}
// GetSyncChanges godoc
// @Summary Get sync changes
// @Description Get all changes (habits and entries) since a given timestamp for offline sync
// @Tags sync
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param since query string true "ISO 8601 timestamp (e.g., 2025-01-01T00:00:00Z)"
// @Success 200 {object} SyncChangesResponse
// @Failure 400 {object} ErrorResponse
// @Failure 401 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /sync/changes [get]
func (h *SyncHandlers) GetSyncChanges(w http.ResponseWriter, r *http.Request) {
userID, ok := GetUserIDFromContext(r.Context())
if !ok {
respondErrorI18n(w, r, h.translator, http.StatusUnauthorized, "user_not_authenticated")
return
}
sinceStr := r.URL.Query().Get("since")
if sinceStr == "" {
respondErrorI18n(w, r, h.translator, http.StatusBadRequest, "missing_since_parameter")
return
}
since, err := time.Parse(time.RFC3339, sinceStr)
if err != nil {
respondErrorI18n(w, r, h.translator, http.StatusBadRequest, "invalid_since_format")
return
}
query := queries.GetSyncChangesQuery{
UserID: userID,
Since: since,
}
result, err := h.getSyncChangesHandler.Handle(r.Context(), query)
if err != nil {
if err == errors.ErrInvalidInput {
respondErrorI18n(w, r, h.translator, http.StatusBadRequest, "invalid_input")
return
}
respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "internal_server_error")
return
}
response := SyncChangesResponse{
Habits: HabitChangesDTO{
Created: toHabitDTOs(result.Habits.Created),
Updated: toHabitDTOs(result.Habits.Updated),
Deleted: result.Habits.Deleted,
},
Entries: EntryChangesDTO{
Created: toHabitEntryDTOs(result.Entries.Created),
Updated: toHabitEntryDTOs(result.Entries.Updated),
Deleted: result.Entries.Deleted,
},
}
respondJSON(w, http.StatusOK, response)
}
// ApplySyncBatch godoc
// @Summary Apply sync batch
// @Description Apply a batch of changes from the client for offline sync (Last-Write-Wins)
// @Tags sync
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body SyncBatchRequest true "Sync batch data"
// @Success 200 {object} map[string]string
// @Failure 400 {object} ErrorResponse
// @Failure 401 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /sync/batch [post]
func (h *SyncHandlers) ApplySyncBatch(w http.ResponseWriter, r *http.Request) {
userID, ok := GetUserIDFromContext(r.Context())
if !ok {
respondErrorI18n(w, r, h.translator, http.StatusUnauthorized, "user_not_authenticated")
return
}
var req SyncBatchRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondErrorI18n(w, r, h.translator, http.StatusBadRequest, "invalid_request_body")
return
}
habitChanges := commands.HabitBatchChanges{
Created: fromHabitDTOs(req.Habits.Created),
Updated: fromHabitDTOs(req.Habits.Updated),
Deleted: req.Habits.Deleted,
}
entryChanges := commands.EntryBatchChanges{
Created: fromHabitEntryDTOs(req.Entries.Created),
Updated: fromHabitEntryDTOs(req.Entries.Updated),
Deleted: req.Entries.Deleted,
}
cmd := commands.ApplySyncBatchCommand{
UserID: userID,
Habits: habitChanges,
Entries: entryChanges,
}
err := h.applySyncBatchHandler.Handle(r.Context(), cmd)
if err != nil {
if err == errors.ErrInvalidInput {
respondErrorI18n(w, r, h.translator, http.StatusBadRequest, "invalid_input")
return
}
if err == errors.ErrUnauthorized {
respondErrorI18n(w, r, h.translator, http.StatusForbidden, "forbidden")
return
}
respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "internal_server_error")
return
}
respondJSON(w, http.StatusOK, map[string]string{"message": "sync_batch_applied"})
}
func toHabitDTOs(habits []*entities.Habit) []SyncHabitDTO {
dtos := make([]SyncHabitDTO, len(habits))
for i, h := range habits {
dtos[i] = SyncHabitDTO{
ID: h.ID,
UserID: h.UserID,
Name: h.Name,
Description: h.Description,
Type: h.Type,
Frequency: h.Frequency,
SpecificDays: h.SpecificDays,
SpecificDates: h.SpecificDates,
CarryOver: h.CarryOver,
IsNegative: h.IsNegative,
TargetValue: h.TargetValue,
CreatedAt: h.CreatedAt,
UpdatedAt: h.UpdatedAt,
ArchivedAt: h.ArchivedAt,
}
}
return dtos
}
func fromHabitDTOs(dtos []SyncHabitDTO) []*entities.Habit {
habits := make([]*entities.Habit, len(dtos))
for i, dto := range dtos {
habits[i] = &entities.Habit{
ID: dto.ID,
UserID: dto.UserID,
Name: dto.Name,
Description: dto.Description,
Type: dto.Type,
Frequency: dto.Frequency,
SpecificDays: dto.SpecificDays,
SpecificDates: dto.SpecificDates,
CarryOver: dto.CarryOver,
IsNegative: dto.IsNegative,
TargetValue: dto.TargetValue,
CreatedAt: dto.CreatedAt,
UpdatedAt: dto.UpdatedAt,
ArchivedAt: dto.ArchivedAt,
}
}
return habits
}
func toHabitEntryDTOs(entries []*entities.HabitEntry) []SyncHabitEntryDTO {
dtos := make([]SyncHabitEntryDTO, len(entries))
for i, e := range entries {
dtos[i] = SyncHabitEntryDTO{
ID: e.ID,
HabitID: e.HabitID,
ScheduledDate: e.ScheduledDate,
CompletedAt: e.CompletedAt,
Value: e.Value,
UpdatedAt: e.UpdatedAt,
}
}
return dtos
}
func fromHabitEntryDTOs(dtos []SyncHabitEntryDTO) []*entities.HabitEntry {
entries := make([]*entities.HabitEntry, len(dtos))
for i, dto := range dtos {
entries[i] = &entities.HabitEntry{
ID: dto.ID,
HabitID: dto.HabitID,
ScheduledDate: dto.ScheduledDate,
CompletedAt: dto.CompletedAt,
Value: dto.Value,
UpdatedAt: dto.UpdatedAt,
}
}
return entries
}