package httpapi import ( "time" "botpontos/internal/adapters/postgres" "botpontos/internal/app" "botpontos/internal/domain" ) // Formatos JSON da API. Os tipos TypeScript equivalentes ficam em web/lib/types.ts. type userJSON struct { ID int64 `json:"id"` TelegramChatID int64 `json:"telegram_chat_id"` DisplayName string `json:"display_name"` Status string `json:"status"` IsBotAdmin bool `json:"is_bot_admin"` NotificationsEnabled bool `json:"notifications_enabled"` PausedReason *string `json:"paused_reason"` Timezone string `json:"timezone"` CreatedAt time.Time `json:"created_at"` } func toUser(u domain.User) userJSON { var reason *string if u.PausedReason != nil { v := string(*u.PausedReason) reason = &v } return userJSON{ ID: u.ID, TelegramChatID: u.TelegramChatID, DisplayName: u.DisplayName, Status: string(u.Status), IsBotAdmin: u.IsBotAdmin, NotificationsEnabled: u.NotificationsEnabled, PausedReason: reason, Timezone: u.Timezone, CreatedAt: u.CreatedAt, } } type userSummaryJSON struct { userJSON HasCredential bool `json:"has_credential"` CredentialValidated bool `json:"credential_validated"` ConsecutiveFailures int `json:"consecutive_failures"` CustomWindow bool `json:"custom_window"` } func toUserSummary(s postgres.UserSummary) userSummaryJSON { return userSummaryJSON{ userJSON: toUser(s.User), HasCredential: s.HasCredential, CredentialValidated: s.CredentialValidated, ConsecutiveFailures: s.ConsecutiveFailures, CustomWindow: s.CustomWindow, } } type windowJSON struct { QueryWindowStart string `json:"query_window_start"` QueryWindowEnd string `json:"query_window_end"` DayCheckDeadline string `json:"day_check_deadline"` NoPointsRecheckUntil string `json:"no_points_recheck_until"` } func toWindow(w domain.QueryWindow) windowJSON { return windowJSON{ QueryWindowStart: w.Start.String(), QueryWindowEnd: w.End.String(), DayCheckDeadline: w.DayCheckDeadline.String(), NoPointsRecheckUntil: w.NoPointsRecheckUntil.String(), } } // parse converte para o domínio, com erros por campo. func (w windowJSON) parse(prefix string) (domain.QueryWindow, error) { var out domain.QueryWindow var errs domain.ValidationErrors fields := []struct { name string value string target *domain.ClockTime }{ {"query_window_start", w.QueryWindowStart, &out.Start}, {"query_window_end", w.QueryWindowEnd, &out.End}, {"day_check_deadline", w.DayCheckDeadline, &out.DayCheckDeadline}, {"no_points_recheck_until", w.NoPointsRecheckUntil, &out.NoPointsRecheckUntil}, } for _, f := range fields { c, err := domain.ParseClock(f.value) if err != nil { errs.Add(prefix+f.name, "horário inválido (use HH:MM)") continue } *f.target = c } return out, errs.OrNil() } type scheduleJSON struct { LunchMinutes int `json:"lunch_minutes"` DailyWorkMinutes int `json:"daily_work_minutes"` Entrada2LeadMinutes int `json:"entrada2_lead_minutes"` Saida2LeadMinutes int `json:"saida2_lead_minutes"` Window *windowJSON `json:"window"` } func toSchedule(s domain.WorkSchedule) scheduleJSON { out := scheduleJSON{ LunchMinutes: s.LunchMinutes, DailyWorkMinutes: s.DailyWorkMinutes, Entrada2LeadMinutes: s.Entrada2LeadMinutes, Saida2LeadMinutes: s.Saida2LeadMinutes, } if s.Window != nil { w := toWindow(*s.Window) out.Window = &w } return out } type credentialJSON struct { APIEmail string `json:"api_email"` Validated bool `json:"validated"` LastValidatedAt *time.Time `json:"last_validated_at"` LastError *string `json:"last_error"` ConsecutiveFailures int `json:"consecutive_failures"` LastFailureAt *time.Time `json:"last_failure_at"` UpdatedAt time.Time `json:"updated_at"` } type dayJSON struct { Day string `json:"day"` Status string `json:"status"` ZeroPointChecks int `json:"zero_point_checks"` LastPointCount int `json:"last_point_count"` NextCheckAt *time.Time `json:"next_check_at"` ResumedBy *string `json:"resumed_by"` } func toDay(d domain.UserDay) dayJSON { var resumed *string if d.ResumedBy != nil { v := string(*d.ResumedBy) resumed = &v } return dayJSON{ Day: d.Day, Status: string(d.Status), ZeroPointChecks: d.ZeroPointChecks, LastPointCount: d.LastPointCount, NextCheckAt: d.NextCheckAt, ResumedBy: resumed, } } type userDetailJSON struct { User userJSON `json:"user"` Schedule scheduleJSON `json:"schedule"` Credential *credentialJSON `json:"credential"` Today dayJSON `json:"today"` EffectiveWindow windowJSON `json:"effective_window"` CustomWindow bool `json:"custom_window"` } func toUserDetail(d app.UserDetail) userDetailJSON { out := userDetailJSON{ User: toUser(d.User), Schedule: toSchedule(d.Schedule), Today: toDay(d.Today), EffectiveWindow: toWindow(d.Window), CustomWindow: d.CustomWindow, } if c := d.Credential; c != nil { out.Credential = &credentialJSON{ APIEmail: c.APIEmail, Validated: c.IsValidated(), LastValidatedAt: c.LastValidatedAt, LastError: c.LastError, ConsecutiveFailures: c.ConsecutiveFailures, LastFailureAt: c.LastFailureAt, UpdatedAt: c.UpdatedAt, } } return out } type todayJSON struct { Day string `json:"day"` Timezone string `json:"timezone"` Points []time.Time `json:"points"` Invalid bool `json:"invalid"` Overtime bool `json:"overtime"` Entrada2At *time.Time `json:"entrada2_at"` Saida2At *time.Time `json:"saida2_at"` Saida2Error string `json:"saida2_error,omitempty"` DayState dayJSON `json:"day_state"` Window windowJSON `json:"window"` CustomWindow bool `json:"custom_window"` } func toToday(v app.TodayView) todayJSON { points := v.Points if points == nil { points = []time.Time{} } return todayJSON{ Day: v.Day, Timezone: v.User.Timezone, Points: points, Invalid: v.Invalid, Overtime: v.Overtime, Entrada2At: v.Entrada2At, Saida2At: v.Saida2At, Saida2Error: v.Saida2Error, DayState: toDay(v.DayState), Window: toWindow(v.Window), CustomWindow: v.CustomWindow, } } type notificationJSON struct { ID int64 `json:"id"` UserID int64 `json:"user_id"` UserName string `json:"user_name,omitempty"` ReferenceDate string `json:"reference_date"` Type string `json:"type"` ScheduledAt time.Time `json:"scheduled_at"` PredictedAt time.Time `json:"predicted_at"` Status string `json:"status"` Attempts int `json:"attempts"` SentAt *time.Time `json:"sent_at"` ErrorMessage *string `json:"error_message"` } func toNotification(n domain.Notification, userName string) notificationJSON { return notificationJSON{ ID: n.ID, UserID: n.UserID, UserName: userName, ReferenceDate: n.ReferenceDate, Type: string(n.Type), ScheduledAt: n.ScheduledAt, PredictedAt: n.PredictedAt, Status: string(n.Status), Attempts: n.Attempts, SentAt: n.SentAt, ErrorMessage: n.ErrorMessage, } } type auditJSON struct { ID int64 `json:"id"` OccurredAt time.Time `json:"occurred_at"` Source string `json:"source"` ActorUserID *int64 `json:"actor_user_id"` ActorUserName *string `json:"actor_user_name"` ActorAccount *int64 `json:"actor_account"` ActorUsername *string `json:"actor_username"` TargetUserID *int64 `json:"target_user_id"` TargetUserName *string `json:"target_user_name"` EventType string `json:"event_type"` Success bool `json:"success"` Details map[string]any `json:"details"` } func toAudit(l domain.AuditLog) auditJSON { return auditJSON{ ID: l.ID, OccurredAt: l.OccurredAt, Source: string(l.Source), ActorUserID: l.ActorUserID, ActorUserName: l.ActorUserName, ActorAccount: l.ActorAccount, ActorUsername: l.ActorUsername, TargetUserID: l.TargetUserID, TargetUserName: l.TargetUserName, EventType: l.EventType, Success: l.Success, Details: l.Details, } } type holidayJSON struct { Day string `json:"day"` Name string `json:"name"` Source string `json:"source"` } type accountJSON struct { ID int64 `json:"id"` Username string `json:"username"` Role string `json:"role"` UserID *int64 `json:"user_id"` Disabled bool `json:"disabled"` LockedUntil *time.Time `json:"locked_until"` LastLoginAt *time.Time `json:"last_login_at"` CreatedAt time.Time `json:"created_at"` } func toAccount(a domain.WebAccount) accountJSON { return accountJSON{ ID: a.ID, Username: a.Username, Role: string(a.Role), UserID: a.UserID, Disabled: a.IsDisabled(), LockedUntil: a.LockedUntil, LastLoginAt: a.LastLoginAt, CreatedAt: a.CreatedAt, } } type dashboardJSON struct { UsersByStatus map[string]int `json:"users_by_status"` NotificationsEnabled int `json:"notifications_enabled"` PausedByAPIFailures int `json:"paused_by_api_failures"` TodayByStatus map[string]int `json:"today_by_status"` FailedLast7Days int `json:"failed_last_7_days"` ExpiredLast7Days int `json:"expired_last_7_days"` CustomWindows int `json:"custom_windows"` OutageSince *string `json:"outage_since"` HolidayFallbackDay *string `json:"holiday_fallback_day"` Today string `json:"today"` }