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.
130 lines
4.8 KiB
Go
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"})
|
|
}
|