mirror of
https://github.com/warmbly/warmbly.git
synced 2026-08-18 16:01:18 +00:00
147 lines
4.3 KiB
Go
147 lines
4.3 KiB
Go
package handler
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/warmbly/warmbly/internal/api/middleware"
|
|
"github.com/warmbly/warmbly/internal/app/behavior"
|
|
"github.com/warmbly/warmbly/internal/errx"
|
|
"github.com/warmbly/warmbly/internal/models"
|
|
)
|
|
|
|
// Sending-behaviour endpoints. The profile is a mailbox-scoped resource rather
|
|
// than more fields on PATCH /emails/:id: it has its own validation surface, its
|
|
// own derived read (today's rolled plan), and a customer can reasonably let a
|
|
// teammate tune sending hours without handing them the mailbox itself.
|
|
|
|
// resolveOwnedMailbox parses the :id param and confirms the caller's
|
|
// organization owns that mailbox. Every behaviour route goes through it, so a
|
|
// mailbox id from another workspace 404s rather than leaking its schedule.
|
|
func (h *Handler) resolveOwnedMailbox(c *gin.Context) (uuid.UUID, bool) {
|
|
accountID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
errx.Handle(c, errx.ErrUuid)
|
|
return uuid.Nil, false
|
|
}
|
|
|
|
orgID := middleware.GetOrganizationID(c)
|
|
if orgID == nil {
|
|
errx.Handle(c, errx.New(errx.BadRequest, "no organization selected"))
|
|
return uuid.Nil, false
|
|
}
|
|
|
|
if _, gerr := h.EmailService.Get(c.Request.Context(), orgID.String(), accountID.String()); gerr != nil {
|
|
errx.Handle(c, gerr)
|
|
return uuid.Nil, false
|
|
}
|
|
return accountID, true
|
|
}
|
|
|
|
// behaviorUnavailable reports the feature as unconfigured rather than 500ing,
|
|
// for deployments running without the behaviour engine wired.
|
|
func (h *Handler) behaviorUnavailable(c *gin.Context) bool {
|
|
if h.BehaviorService == nil {
|
|
errx.Handle(c, errx.New(errx.BadRequest, "sending behaviour is not enabled on this deployment"))
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// handleBehaviorError maps the service's errors onto the API's error shape,
|
|
// turning a validation failure into a 400 that names the offending field.
|
|
func handleBehaviorError(c *gin.Context, err error) {
|
|
var verr *models.BehaviorValidationError
|
|
if errors.As(err, &verr) {
|
|
errx.Handle(c, errx.New(errx.BadRequest, verr.Message))
|
|
return
|
|
}
|
|
if errors.Is(err, behavior.ErrNotFound) {
|
|
errx.Handle(c, errx.ErrNotFound)
|
|
return
|
|
}
|
|
var apiErr *errx.Error
|
|
if errors.As(err, &apiErr) {
|
|
errx.Handle(c, apiErr)
|
|
return
|
|
}
|
|
errx.Handle(c, errx.InternalError())
|
|
}
|
|
|
|
// GetEmailBehavior returns a mailbox's sending-behaviour profile, substituting
|
|
// the defaults for a mailbox that has never been configured so the client
|
|
// always has a complete object to render.
|
|
// GET /emails/:id/behavior
|
|
func (h *Handler) GetEmailBehavior(c *gin.Context) {
|
|
if h.behaviorUnavailable(c) {
|
|
return
|
|
}
|
|
accountID, ok := h.resolveOwnedMailbox(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
profile, err := h.BehaviorService.Get(c.Request.Context(), accountID)
|
|
if err != nil {
|
|
handleBehaviorError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, profile)
|
|
}
|
|
|
|
// UpdateEmailBehavior applies a partial update to the profile. Omitted fields
|
|
// keep their stored value, so toggling `enabled` does not require resending
|
|
// every range.
|
|
//
|
|
// Naturally idempotent: the body is the desired state, so a retry converges on
|
|
// the same profile and no Idempotency-Key is needed.
|
|
// PUT /emails/:id/behavior
|
|
func (h *Handler) UpdateEmailBehavior(c *gin.Context) {
|
|
if h.behaviorUnavailable(c) {
|
|
return
|
|
}
|
|
accountID, ok := h.resolveOwnedMailbox(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var patch models.UpdateSendingBehavior
|
|
if err := c.ShouldBindJSON(&patch); err != nil {
|
|
errx.Handle(c, errx.ErrInvalid)
|
|
return
|
|
}
|
|
|
|
profile, err := h.BehaviorService.Update(c.Request.Context(), accountID, patch)
|
|
if err != nil {
|
|
handleBehaviorError(c, err)
|
|
return
|
|
}
|
|
|
|
h.auditOrg(c, models.AuditActionUpdate, models.AuditEntityEmailAccount, &accountID, nil, nil)
|
|
c.JSON(http.StatusOK, profile)
|
|
}
|
|
|
|
// GetEmailBehaviorPlan returns the workday this mailbox actually rolled for the
|
|
// current local date, plus how much of it is already spent. This is the read
|
|
// that answers "why is nothing sending right now" without anyone guessing.
|
|
// GET /emails/:id/behavior/plan
|
|
func (h *Handler) GetEmailBehaviorPlan(c *gin.Context) {
|
|
if h.behaviorUnavailable(c) {
|
|
return
|
|
}
|
|
accountID, ok := h.resolveOwnedMailbox(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
plan, err := h.BehaviorService.Today(c.Request.Context(), accountID)
|
|
if err != nil {
|
|
handleBehaviorError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, plan)
|
|
}
|