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.
134 lines
3.0 KiB
Go
134 lines
3.0 KiB
Go
package model
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"gorm.io/datatypes"
|
|
)
|
|
|
|
const accountOutboundEmailCountKey = "_outbound_email_count"
|
|
|
|
// LimitsMap returns the Chatwoot-style account limits JSON object.
|
|
func (a *Account) LimitsMap() map[string]any {
|
|
return jsonMap(a.Limits)
|
|
}
|
|
|
|
// EmailTranscriptEnabled mirrors Account#email_transcript_enabled?. Community
|
|
// accounts allow transcripts by default; enterprise tests may disable it via
|
|
// limits or custom attributes.
|
|
func (a *Account) EmailTranscriptEnabled() bool {
|
|
if a == nil {
|
|
return true
|
|
}
|
|
for _, attrs := range []map[string]any{a.LimitsMap(), a.CustomAttributesMap()} {
|
|
if value, ok := firstPresent(attrs, "email_transcript_enabled", "email_transcript"); ok {
|
|
return truthy(value, true)
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// EmailRateLimit returns the configured daily transcript/outbound email limit.
|
|
// A zero value means unlimited, matching non-cloud Chatwoot behavior.
|
|
func (a *Account) EmailRateLimit() int {
|
|
if a == nil {
|
|
return 0
|
|
}
|
|
for _, attrs := range []map[string]any{a.LimitsMap(), a.CustomAttributesMap()} {
|
|
if value, ok := firstPresent(attrs, "emails", "email_rate_limit", "outbound_email_limit"); ok {
|
|
if n := intValue(value); n > 0 {
|
|
return n
|
|
}
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (a *Account) EmailsSentToday(now time.Time) int {
|
|
attrs := a.CustomAttributesMap()
|
|
value, ok := attrs[accountOutboundEmailCountKey]
|
|
if !ok {
|
|
return intValue(attrs["emails_sent_today"])
|
|
}
|
|
record, ok := value.(map[string]any)
|
|
if !ok || strings.TrimSpace(stringValue(record["date"])) != now.Format("2006-01-02") {
|
|
return 0
|
|
}
|
|
return intValue(record["count"])
|
|
}
|
|
|
|
func (a *Account) IncrementEmailSentCount(now time.Time) error {
|
|
attrs := a.CustomAttributesMap()
|
|
count := a.EmailsSentToday(now) + 1
|
|
attrs[accountOutboundEmailCountKey] = map[string]any{
|
|
"date": now.Format("2006-01-02"),
|
|
"count": count,
|
|
}
|
|
return a.SetCustomAttributesMap(attrs)
|
|
}
|
|
|
|
func jsonMap(raw datatypes.JSON) map[string]any {
|
|
attrs := map[string]any{}
|
|
if len(raw) > 0 {
|
|
_ = json.Unmarshal(raw, &attrs)
|
|
}
|
|
return attrs
|
|
}
|
|
|
|
func firstPresent(attrs map[string]any, keys ...string) (any, bool) {
|
|
for _, key := range keys {
|
|
if value, ok := attrs[key]; ok {
|
|
return value, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func truthy(value any, defaultValue bool) bool {
|
|
switch v := value.(type) {
|
|
case bool:
|
|
return v
|
|
case string:
|
|
s := strings.TrimSpace(strings.ToLower(v))
|
|
if s == "" {
|
|
return defaultValue
|
|
}
|
|
return s == "true" || s == "1" || s == "yes" || s == "enabled"
|
|
case float64:
|
|
return v != 0
|
|
case int:
|
|
return v != 0
|
|
default:
|
|
return defaultValue
|
|
}
|
|
}
|
|
|
|
func intValue(value any) int {
|
|
switch v := value.(type) {
|
|
case int:
|
|
return v
|
|
case int64:
|
|
return int(v)
|
|
case float64:
|
|
return int(v)
|
|
case json.Number:
|
|
n, _ := v.Int64()
|
|
return int(n)
|
|
case string:
|
|
n, _ := strconv.Atoi(strings.TrimSpace(v))
|
|
return n
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func stringValue(value any) string {
|
|
if s, ok := value.(string); ok {
|
|
return s
|
|
}
|
|
return ""
|
|
}
|