65a9aae4e5
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>
410 lines
11 KiB
Go
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,
|
|
})
|
|
}
|