Files
honeyDueAPI/internal/handlers/residence_handler.go
T
Trey t 65a9aae4e5
Backend CI / Test (push) Has been cancelled
Backend CI / Contract Tests (push) Has been cancelled
Backend CI / Build (push) Has been cancelled
Backend CI / Lint (push) Has been cancelled
Backend CI / Secret Scanning (push) Has been cancelled
Migrate TaskService + ResidenceService to ctx-aware repos
Every public method on TaskService and ResidenceService now takes
ctx context.Context as the first arg and routes its repo calls through
.WithContext(ctx). With otelgorm registered, this means every API
endpoint backed by these two services produces a flame graph in Jaeger
where the SQL spans nest under the parent HTTP request span — instead
of appearing as orphaned queries.

Endpoints now fully traced (HTTP → service → SQL):
- GET    /api/tasks/                       (already shipped)
- GET    /api/tasks/by-residence/:id/      (already shipped)
- GET    /api/tasks/:id/
- POST   /api/tasks/
- POST   /api/tasks/bulk/
- PUT    /api/tasks/:id/
- DELETE /api/tasks/:id/
- POST   /api/tasks/:id/in-progress/
- POST   /api/tasks/:id/cancel/
- POST   /api/tasks/:id/uncancel/
- POST   /api/tasks/:id/archive/
- POST   /api/tasks/:id/unarchive/
- POST   /api/tasks/:id/complete/
- POST   /api/tasks/:id/quick-complete/
- GET    /api/tasks/completions/* (CRUD)
- GET    /api/static_data/ (categories, priorities, frequencies)
- GET    /api/residences/
- GET    /api/residences/my/
- GET    /api/residences/summary/
- GET    /api/residences/:id/
- POST   /api/residences/
- PUT    /api/residences/:id/
- DELETE /api/residences/:id/
- Share-code + member management endpoints
- GET    /api/residences/:id/report/

Mechanical work: ~50 method signatures, ~80 handler call sites,
~25 test call sites updated. Internal sendTaskCompletedNotification
helper also takes ctx so background notification SQL nests correctly.

The remaining services (ContractorService, DocumentService,
AuthService, NotificationService, SubscriptionService) follow the same
pattern; they continue to emit untraced SQL until migrated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:04:01 -05:00

410 lines
11 KiB
Go

package handlers
import (
"net/http"
"strconv"
"github.com/labstack/echo/v4"
"github.com/treytartt/honeydue-api/internal/apperrors"
"github.com/treytartt/honeydue-api/internal/dto/requests"
"github.com/treytartt/honeydue-api/internal/i18n"
"github.com/treytartt/honeydue-api/internal/middleware"
"github.com/treytartt/honeydue-api/internal/services"
"github.com/treytartt/honeydue-api/internal/validator"
)
// ResidenceHandler handles residence-related HTTP requests
type ResidenceHandler struct {
residenceService *services.ResidenceService
pdfService *services.PDFService
emailService *services.EmailService
pdfReportsEnabled bool
}
// NewResidenceHandler creates a new residence handler
func NewResidenceHandler(residenceService *services.ResidenceService, pdfService *services.PDFService, emailService *services.EmailService, pdfReportsEnabled bool) *ResidenceHandler {
return &ResidenceHandler{
residenceService: residenceService,
pdfService: pdfService,
emailService: emailService,
pdfReportsEnabled: pdfReportsEnabled,
}
}
// ListResidences handles GET /api/residences/
func (h *ResidenceHandler) ListResidences(c echo.Context) error {
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
response, err := h.residenceService.ListResidences(c.Request().Context(), user.ID)
if err != nil {
return err
}
return c.JSON(http.StatusOK, response)
}
// GetMyResidences handles GET /api/residences/my-residences/
func (h *ResidenceHandler) GetMyResidences(c echo.Context) error {
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
userNow := middleware.GetUserNow(c)
response, err := h.residenceService.GetMyResidences(c.Request().Context(), user.ID, userNow)
if err != nil {
return err
}
return c.JSON(http.StatusOK, response)
}
// GetSummary handles GET /api/residences/summary/
// Returns just the task statistics summary without full residence data
func (h *ResidenceHandler) GetSummary(c echo.Context) error {
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
userNow := middleware.GetUserNow(c)
summary, err := h.residenceService.GetSummary(c.Request().Context(), user.ID, userNow)
if err != nil {
return err
}
return c.JSON(http.StatusOK, summary)
}
// GetResidence handles GET /api/residences/:id/
func (h *ResidenceHandler) GetResidence(c echo.Context) error {
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
return apperrors.BadRequest("error.invalid_residence_id")
}
userNow := middleware.GetUserNow(c)
response, err := h.residenceService.GetResidence(c.Request().Context(), uint(residenceID), user.ID, userNow)
if err != nil {
return err
}
return c.JSON(http.StatusOK, response)
}
// CreateResidence handles POST /api/residences/
func (h *ResidenceHandler) CreateResidence(c echo.Context) error {
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
var req requests.CreateResidenceRequest
if err := c.Bind(&req); err != nil {
return apperrors.BadRequest("error.invalid_request")
}
if err := c.Validate(&req); err != nil {
return c.JSON(http.StatusBadRequest, validator.FormatValidationErrors(err))
}
response, err := h.residenceService.CreateResidence(c.Request().Context(), &req, user.ID)
if err != nil {
return err
}
return c.JSON(http.StatusCreated, response)
}
// UpdateResidence handles PUT/PATCH /api/residences/:id/
func (h *ResidenceHandler) UpdateResidence(c echo.Context) error {
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
return apperrors.BadRequest("error.invalid_residence_id")
}
var req requests.UpdateResidenceRequest
if err := c.Bind(&req); err != nil {
return apperrors.BadRequest("error.invalid_request")
}
if err := c.Validate(&req); err != nil {
return c.JSON(http.StatusBadRequest, validator.FormatValidationErrors(err))
}
response, err := h.residenceService.UpdateResidence(c.Request().Context(), uint(residenceID), user.ID, &req)
if err != nil {
return err
}
return c.JSON(http.StatusOK, response)
}
// DeleteResidence handles DELETE /api/residences/:id/
func (h *ResidenceHandler) DeleteResidence(c echo.Context) error {
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
return apperrors.BadRequest("error.invalid_residence_id")
}
response, err := h.residenceService.DeleteResidence(c.Request().Context(), uint(residenceID), user.ID)
if err != nil {
return err
}
return c.JSON(http.StatusOK, response)
}
// GetShareCode handles GET /api/residences/:id/share-code/
// Returns the active share code for a residence, or null if none exists
func (h *ResidenceHandler) GetShareCode(c echo.Context) error {
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
return apperrors.BadRequest("error.invalid_residence_id")
}
shareCode, err := h.residenceService.GetShareCode(c.Request().Context(), uint(residenceID), user.ID)
if err != nil {
return err
}
if shareCode == nil {
return c.JSON(http.StatusOK, map[string]interface{}{"share_code": nil})
}
return c.JSON(http.StatusOK, map[string]interface{}{"share_code": shareCode})
}
// GenerateShareCode handles POST /api/residences/:id/generate-share-code/
func (h *ResidenceHandler) GenerateShareCode(c echo.Context) error {
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
return apperrors.BadRequest("error.invalid_residence_id")
}
var req requests.GenerateShareCodeRequest
// Request body is optional
c.Bind(&req)
response, err := h.residenceService.GenerateShareCode(c.Request().Context(), uint(residenceID), user.ID, req.ExpiresInHours)
if err != nil {
return err
}
return c.JSON(http.StatusOK, response)
}
// GenerateSharePackage handles POST /api/residences/:id/generate-share-package/
// Returns a share code with metadata for creating a .honeydue package file
func (h *ResidenceHandler) GenerateSharePackage(c echo.Context) error {
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
return apperrors.BadRequest("error.invalid_residence_id")
}
var req requests.GenerateShareCodeRequest
// Request body is optional (for expires_in_hours)
c.Bind(&req)
response, err := h.residenceService.GenerateSharePackage(c.Request().Context(), uint(residenceID), user.ID, req.ExpiresInHours)
if err != nil {
return err
}
return c.JSON(http.StatusOK, response)
}
// JoinWithCode handles POST /api/residences/join-with-code/
func (h *ResidenceHandler) JoinWithCode(c echo.Context) error {
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
var req requests.JoinWithCodeRequest
if err := c.Bind(&req); err != nil {
return apperrors.BadRequest("error.invalid_request")
}
if err := c.Validate(&req); err != nil {
return err
}
response, err := h.residenceService.JoinWithCode(c.Request().Context(), req.Code, user.ID)
if err != nil {
return err
}
return c.JSON(http.StatusOK, response)
}
// GetResidenceUsers handles GET /api/residences/:id/users/
func (h *ResidenceHandler) GetResidenceUsers(c echo.Context) error {
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
return apperrors.BadRequest("error.invalid_residence_id")
}
users, err := h.residenceService.GetResidenceUsers(c.Request().Context(), uint(residenceID), user.ID)
if err != nil {
return err
}
return c.JSON(http.StatusOK, users)
}
// RemoveResidenceUser handles DELETE /api/residences/:id/users/:user_id/
func (h *ResidenceHandler) RemoveResidenceUser(c echo.Context) error {
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
return apperrors.BadRequest("error.invalid_residence_id")
}
userIDToRemove, err := strconv.ParseUint(c.Param("user_id"), 10, 32)
if err != nil {
return apperrors.BadRequest("error.invalid_user_id")
}
err = h.residenceService.RemoveUser(c.Request().Context(), uint(residenceID), uint(userIDToRemove), user.ID)
if err != nil {
return err
}
return c.JSON(http.StatusOK, map[string]interface{}{"message": i18n.LocalizedMessage(c, "message.user_removed")})
}
// GetResidenceTypes handles GET /api/residences/types/
func (h *ResidenceHandler) GetResidenceTypes(c echo.Context) error {
types, err := h.residenceService.GetResidenceTypes(c.Request().Context())
if err != nil {
return err
}
return c.JSON(http.StatusOK, types)
}
// GenerateTasksReport handles POST /api/residences/:id/generate-tasks-report/
// Generates a PDF report of tasks for the residence and emails it
func (h *ResidenceHandler) GenerateTasksReport(c echo.Context) error {
if !h.pdfReportsEnabled {
return apperrors.BadRequest("error.feature_disabled")
}
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
return apperrors.BadRequest("error.invalid_residence_id")
}
// Optional request body for email recipient
var req struct {
Email string `json:"email"`
}
c.Bind(&req)
// Generate the report data
report, err := h.residenceService.GenerateTasksReport(c.Request().Context(), uint(residenceID), user.ID)
if err != nil {
return err
}
// Determine recipient email
recipientEmail := req.Email
if recipientEmail == "" {
recipientEmail = user.Email
}
// Get recipient name
recipientName := user.FirstName
if recipientName == "" {
recipientName = user.Username
}
// Generate PDF if PDF service is available
var pdfGenerated bool
var emailSent bool
if h.pdfService != nil && h.emailService != nil {
pdfData, pdfErr := h.pdfService.GenerateTasksReportPDF(report)
if pdfErr == nil {
pdfGenerated = true
// Send email with PDF attachment
emailErr := h.emailService.SendTasksReportEmail(
recipientEmail,
recipientName,
report.ResidenceName,
report.TotalTasks,
report.Completed,
report.Pending,
report.Overdue,
pdfData,
)
if emailErr == nil {
emailSent = true
}
}
}
// Build response message
message := i18n.LocalizedMessage(c, "message.tasks_report_generated")
if pdfGenerated && emailSent {
message = i18n.LocalizedMessageWithData(c, "message.tasks_report_sent", map[string]interface{}{"Email": recipientEmail})
} else if pdfGenerated && !emailSent {
message = i18n.LocalizedMessage(c, "message.tasks_report_email_failed")
}
return c.JSON(http.StatusOK, map[string]interface{}{
"message": message,
"residence_name": report.ResidenceName,
"recipient_email": recipientEmail,
"pdf_generated": pdfGenerated,
"email_sent": emailSent,
"report": report,
})
}