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.
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
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"})
|
||||
}
|
||||
Reference in New Issue
Block a user