Files
gochat/backend/internal/handler/api/v1/enterprise_account_handler.go
T

167 lines
5.9 KiB
Go

package v1
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/gochat/gochat/internal/service"
"github.com/gochat/gochat/pkg/response"
)
// EnterpriseAccountHandler implements Chatwoot enterprise account billing and
// limit endpoints consumed by the reused dashboard EnterpriseAccountAPI client.
type EnterpriseAccountHandler struct {
svc *service.AccountService
}
func NewEnterpriseAccountHandler(svc *service.AccountService) *EnterpriseAccountHandler {
return &EnterpriseAccountHandler{svc: svc}
}
func enterpriseAccountID(c *gin.Context) uint {
if id := parseAccountIDParam(c); id != 0 {
return id
}
return getAccountID(c)
}
// Limits returns account usage limits in Chatwoot's enterprise payload shape.
// GET /enterprise/api/v1/accounts/:account_id/limits
func (h *EnterpriseAccountHandler) Limits(c *gin.Context) {
accountID := enterpriseAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
return
}
payload, err := h.svc.EnterpriseLimits(c.Request.Context(), accountID, getUserID(c))
if err != nil {
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrAccountNotFound, "account not found")
return
}
c.JSON(http.StatusOK, payload)
}
// ToggleDeletion marks or unmarks an account for scheduled deletion.
// POST /enterprise/api/v1/accounts/:account_id/toggle_deletion
func (h *EnterpriseAccountHandler) ToggleDeletion(c *gin.Context) {
accountID := enterpriseAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
return
}
var req struct {
ActionType string `json:"action_type" form:"action_type"`
}
_ = c.ShouldBind(&req)
switch req.ActionType {
case "delete":
if _, err := h.svc.MarkForDeletion(c.Request.Context(), accountID, getUserID(c), "manual_deletion"); err != nil {
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, err.Error())
return
}
c.JSON(http.StatusOK, gin.H{"message": "Account marked for deletion"})
case "undelete":
if _, err := h.svc.UnmarkForDeletion(c.Request.Context(), accountID, getUserID(c)); err != nil {
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, err.Error())
return
}
c.JSON(http.StatusOK, gin.H{"message": "Account unmarked for deletion"})
default:
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "Invalid action_type. Must be either \"delete\" or \"undelete\""})
}
}
// Subscription mirrors the Cloud customer-creation guard and returns no content.
// POST /enterprise/api/v1/accounts/:account_id/subscription
func (h *EnterpriseAccountHandler) Subscription(c *gin.Context) {
accountID := enterpriseAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
return
}
payload, selectionRequired, err := h.svc.EnterpriseSubscription(c.Request.Context(), getUserID(c), accountID)
if err != nil {
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrAccountNotFound, "account not found")
return
}
if selectionRequired {
c.JSON(http.StatusOK, payload)
return
}
c.Status(http.StatusNoContent)
}
// Checkout returns Chatwoot's billing-details error when no Stripe session can be created locally.
// POST /enterprise/api/v1/accounts/:account_id/checkout
func (h *EnterpriseAccountHandler) Checkout(c *gin.Context) {
accountID := enterpriseAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
return
}
if _, err := h.svc.GetByUserAndID(c.Request.Context(), getUserID(c), accountID); err != nil {
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrAccountNotFound, "account not found")
return
}
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "Please subscribe to a plan before viewing the billing details"})
}
// TopupCheckout validates credits and exposes a provider-unavailable boundary for local installs.
// POST /enterprise/api/v1/accounts/:account_id/topup_checkout
func (h *EnterpriseAccountHandler) TopupCheckout(c *gin.Context) {
accountID := enterpriseAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
return
}
if _, err := h.svc.GetByUserAndID(c.Request.Context(), getUserID(c), accountID); err != nil {
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrAccountNotFound, "account not found")
return
}
var req struct {
Credits int `json:"credits" form:"credits"`
}
_ = c.ShouldBind(&req)
if req.Credits <= 0 {
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "Credits are required"})
return
}
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "Top-up checkout provider is not configured"})
}
func (h *EnterpriseAccountHandler) SelectBillingCurrency(c *gin.Context) {
accountID := enterpriseAccountID(c)
var req struct {
Currency string `json:"currency" form:"currency"`
}
_ = c.ShouldBind(&req)
if accountID == 0 || strings.TrimSpace(req.Currency) == "" {
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "Invalid billing currency"})
return
}
if err := h.svc.SelectBillingCurrency(c.Request.Context(), getUserID(c), accountID, req.Currency); err != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
return
}
c.Status(http.StatusNoContent)
}
func (h *EnterpriseAccountHandler) TopupOptions(c *gin.Context) {
accountID := enterpriseAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
return
}
payload, err := h.svc.EnterpriseTopupOptions(c.Request.Context(), getUserID(c), accountID)
if err != nil {
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrAccountNotFound, "account not found")
return
}
c.JSON(http.StatusOK, payload)
}