Files
gochat/backend/internal/handler/api/v1/enterprise_account_handler.go
T
rogee aeddedf2a3 Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories:
- backend/: Go module root (cmd, internal, pkg, configs, migrations,
  docs/swagger, scripts, tests, go.mod, Makefile, .air.toml)
- deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd
- docs/: project documentation + reports/ (moved from repo root)
- AGENTS.md: new AI coding-agent guide at repo root

Update all references to the new layout:
- Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root)
- docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile,
  env_file ../../.env, volume mounts ../../backend:/app
- deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile
- CI: working-directory: backend for go commands, file deploy/docker/Dockerfile,
  coverage path backend/coverage.out, health_check backend/scripts/
- backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../
- README: architecture tree, quickstart, config paths updated

Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh,
gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to
preserve history. Build, vet, SQLite tests, and docker compose config verified.
2026-07-07 14:44:12 +08:00

130 lines
4.8 KiB
Go

package v1
import (
"net/http"
"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
}
if err := h.svc.EnsureEnterpriseAccountCustomerCreationFlag(c.Request.Context(), accountID, getUserID(c)); err != nil {
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrAccountNotFound, "account not found")
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"})
}