Files
honeyDueAPI/internal/handlers/residence_handler.go
treyt e26116e2cf Add webhook logging, pagination, middleware, migrations, and prod hardening
- Webhook event logging repo and subscription webhook idempotency
- Pagination helper (echohelpers) with cursor/offset support
- Request ID and structured logging middleware
- Push client improvements (FCM HTTP v1, better error handling)
- Task model version column, business constraint migrations, targeted indexes
- Expanded categorization chain tests
- Email service and config hardening
- CI workflow updates, .gitignore additions, .env.example updates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 21:32:09 -06:00

365 lines
10 KiB
Go

package handlers
import (
"net/http"
"strconv"
"github.com/labstack/echo/v4"
"github.com/treytartt/casera-api/internal/apperrors"
"github.com/treytartt/casera-api/internal/dto/requests"
"github.com/treytartt/casera-api/internal/i18n"
"github.com/treytartt/casera-api/internal/middleware"
"github.com/treytartt/casera-api/internal/models"
"github.com/treytartt/casera-api/internal/services"
"github.com/treytartt/casera-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 := c.Get(middleware.AuthUserKey).(*models.User)
response, err := h.residenceService.ListResidences(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 := c.Get(middleware.AuthUserKey).(*models.User)
userNow := middleware.GetUserNow(c)
response, err := h.residenceService.GetMyResidences(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 := c.Get(middleware.AuthUserKey).(*models.User)
userNow := middleware.GetUserNow(c)
summary, err := h.residenceService.GetSummary(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 := c.Get(middleware.AuthUserKey).(*models.User)
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
return apperrors.BadRequest("error.invalid_residence_id")
}
response, err := h.residenceService.GetResidence(uint(residenceID), user.ID)
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 := c.Get(middleware.AuthUserKey).(*models.User)
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(&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 := c.Get(middleware.AuthUserKey).(*models.User)
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(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 := c.Get(middleware.AuthUserKey).(*models.User)
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
return apperrors.BadRequest("error.invalid_residence_id")
}
response, err := h.residenceService.DeleteResidence(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 := c.Get(middleware.AuthUserKey).(*models.User)
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
return apperrors.BadRequest("error.invalid_residence_id")
}
shareCode, err := h.residenceService.GetShareCode(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 := c.Get(middleware.AuthUserKey).(*models.User)
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(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 .casera package file
func (h *ResidenceHandler) GenerateSharePackage(c echo.Context) error {
user := c.Get(middleware.AuthUserKey).(*models.User)
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(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 := c.Get(middleware.AuthUserKey).(*models.User)
var req requests.JoinWithCodeRequest
if err := c.Bind(&req); err != nil {
return apperrors.BadRequest("error.invalid_request")
}
response, err := h.residenceService.JoinWithCode(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 := c.Get(middleware.AuthUserKey).(*models.User)
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
return apperrors.BadRequest("error.invalid_residence_id")
}
users, err := h.residenceService.GetResidenceUsers(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 := c.Get(middleware.AuthUserKey).(*models.User)
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(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()
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 := c.Get(middleware.AuthUserKey).(*models.User)
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(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,
})
}