fix(erp): enable erp pages and menu items

This commit is contained in:
Erik Silva
2025-12-29 17:23:59 -03:00
parent e124a64a5d
commit adbff9bb1e
13990 changed files with 1110936 additions and 59 deletions

View File

@@ -60,6 +60,8 @@ func main() {
subscriptionRepo := repository.NewSubscriptionRepository(db)
crmRepo := repository.NewCRMRepository(db)
solutionRepo := repository.NewSolutionRepository(db)
erpRepo := repository.NewERPRepository(db)
docRepo := repository.NewDocumentRepository(db)
// Initialize services
authService := service.NewAuthService(userRepo, tenantRepo, crmRepo, cfg)
@@ -83,6 +85,8 @@ func main() {
agencyTemplateHandler := handlers.NewAgencyTemplateHandler(agencyTemplateRepo, agencyService, userRepo, tenantRepo)
filesHandler := handlers.NewFilesHandler(cfg)
customerPortalHandler := handlers.NewCustomerPortalHandler(crmRepo, authService, cfg)
erpHandler := handlers.NewERPHandler(erpRepo)
docHandler := handlers.NewDocumentHandler(docRepo)
// Initialize upload handler
uploadHandler, err := handlers.NewUploadHandler(cfg)
@@ -424,6 +428,133 @@ func main() {
}
}))).Methods("POST", "DELETE")
// ==================== ERP ROUTES (TENANT) ====================
// Finance
router.Handle("/api/erp/finance/categories", middleware.RequireAgencyUser(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
erpHandler.GetFinancialCategories(w, r)
case http.MethodPost:
erpHandler.CreateFinancialCategory(w, r)
}
}))).Methods("GET", "POST")
router.Handle("/api/erp/finance/accounts", middleware.RequireAgencyUser(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
erpHandler.GetBankAccounts(w, r)
case http.MethodPost:
erpHandler.CreateBankAccount(w, r)
}
}))).Methods("GET", "POST")
router.Handle("/api/erp/finance/accounts/{id}", middleware.RequireAgencyUser(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPut:
erpHandler.UpdateBankAccount(w, r)
case http.MethodDelete:
erpHandler.DeleteBankAccount(w, r)
}
}))).Methods("PUT", "DELETE")
router.Handle("/api/erp/finance/transactions", middleware.RequireAgencyUser(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
erpHandler.GetTransactions(w, r)
case http.MethodPost:
erpHandler.CreateTransaction(w, r)
}
}))).Methods("GET", "POST")
router.Handle("/api/erp/finance/transactions/{id}", middleware.RequireAgencyUser(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPut:
erpHandler.UpdateTransaction(w, r)
case http.MethodDelete:
erpHandler.DeleteTransaction(w, r)
}
}))).Methods("PUT", "DELETE")
// Products
router.Handle("/api/erp/products", middleware.RequireAgencyUser(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
erpHandler.GetProducts(w, r)
case http.MethodPost:
erpHandler.CreateProduct(w, r)
}
}))).Methods("GET", "POST")
router.Handle("/api/erp/products/{id}", middleware.RequireAgencyUser(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPut:
erpHandler.UpdateProduct(w, r)
case http.MethodDelete:
erpHandler.DeleteProduct(w, r)
}
}))).Methods("PUT", "DELETE")
// Orders
router.Handle("/api/erp/orders", middleware.RequireAgencyUser(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
erpHandler.GetOrders(w, r)
case http.MethodPost:
erpHandler.CreateOrder(w, r)
}
}))).Methods("GET", "POST")
router.Handle("/api/erp/orders/{id}", middleware.RequireAgencyUser(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodDelete:
erpHandler.DeleteOrder(w, r)
}
}))).Methods("DELETE")
// Entities
router.Handle("/api/erp/entities", middleware.RequireAgencyUser(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
erpHandler.GetEntities(w, r)
case http.MethodPost:
erpHandler.CreateEntity(w, r)
}
}))).Methods("GET", "POST")
router.Handle("/api/erp/entities/{id}", middleware.RequireAgencyUser(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPut, http.MethodPatch:
erpHandler.UpdateEntity(w, r)
case http.MethodDelete:
erpHandler.DeleteEntity(w, r)
}
}))).Methods("PUT", "PATCH", "DELETE")
// Documents
router.Handle("/api/documents", middleware.RequireAgencyUser(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
docHandler.List(w, r)
case http.MethodPost:
docHandler.Create(w, r)
}
}))).Methods("GET", "POST")
router.Handle("/api/documents/{id}", middleware.RequireAgencyUser(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
docHandler.Get(w, r)
case http.MethodPut:
docHandler.Update(w, r)
case http.MethodDelete:
docHandler.Delete(w, r)
}
}))).Methods("GET", "PUT", "DELETE")
router.Handle("/api/documents/{id}/subpages", middleware.RequireAgencyUser(cfg)(http.HandlerFunc(docHandler.GetSubpages))).Methods("GET")
router.Handle("/api/documents/{id}/activities", middleware.RequireAgencyUser(cfg)(http.HandlerFunc(docHandler.GetActivities))).Methods("GET")
// Apply global middlewares: tenant -> cors -> security -> rateLimit -> router
handler := tenantDetector(corsMiddleware(securityMiddleware(rateLimitMiddleware(router))))

View File

@@ -5,8 +5,32 @@ go 1.23
require (
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1
github.com/lib/pq v1.10.9
github.com/minio/minio-go/v7 v7.0.63
github.com/shopspring/decimal v1.3.1
github.com/xuri/excelize/v2 v2.8.1
golang.org/x/crypto v0.27.0
)
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.3 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 // indirect
github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/text v0.18.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
)

View File

@@ -1,8 +1,76 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.63 h1:GbZ2oCvaUdgT5640WJOpyDhhDxvknAJU2/T3yurwcbQ=
github.com/minio/minio-go/v7 v7.0.63/go.mod h1:Q6X7Qjb7WMhvG65qKf4gUgA5XaiSox74kR1uAEjxRS4=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM=
github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 h1:Chd9DkqERQQuHpXjR/HSV1jLZA6uaoiwwH3vSuF3IW0=
github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/excelize/v2 v2.8.1 h1:pZLMEwK8ep+CLIUWpWmvW8IWE/yxqG0I1xcN6cVMGuQ=
github.com/xuri/excelize/v2 v2.8.1/go.mod h1:oli1E4C3Pa5RXg1TBXn4ENCXDV5JUMlBluUhG7c+CEE=
github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 h1:qhbILQo1K3mphbwKh1vNm4oGezE1eF9fQWmNiIpSfI4=
github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,144 @@
package handlers
import (
"aggios-app/backend/internal/api/middleware"
"aggios-app/backend/internal/domain"
"aggios-app/backend/internal/repository"
"encoding/json"
"net/http"
"github.com/google/uuid"
"github.com/gorilla/mux"
)
type DocumentHandler struct {
repo *repository.DocumentRepository
}
func NewDocumentHandler(repo *repository.DocumentRepository) *DocumentHandler {
return &DocumentHandler{repo: repo}
}
func (h *DocumentHandler) Create(w http.ResponseWriter, r *http.Request) {
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
userID, _ := r.Context().Value(middleware.UserIDKey).(string)
var doc domain.Document
if err := json.NewDecoder(r.Body).Decode(&doc); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
doc.ID = uuid.New()
doc.TenantID, _ = uuid.Parse(tenantID)
doc.CreatedBy, _ = uuid.Parse(userID)
doc.LastUpdatedBy, _ = uuid.Parse(userID)
if doc.Status == "" {
doc.Status = "draft"
}
if err := h.repo.Create(&doc); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(doc)
}
func (h *DocumentHandler) List(w http.ResponseWriter, r *http.Request) {
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
docs, err := h.repo.GetByTenant(tenantID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(docs)
}
func (h *DocumentHandler) Get(w http.ResponseWriter, r *http.Request) {
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
id := mux.Vars(r)["id"]
doc, err := h.repo.GetByID(id, tenantID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if doc == nil {
http.Error(w, "document not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(doc)
}
func (h *DocumentHandler) Update(w http.ResponseWriter, r *http.Request) {
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
userID, _ := r.Context().Value(middleware.UserIDKey).(string)
id := mux.Vars(r)["id"]
var doc domain.Document
if err := json.NewDecoder(r.Body).Decode(&doc); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
doc.ID, _ = uuid.Parse(id)
doc.TenantID, _ = uuid.Parse(tenantID)
doc.LastUpdatedBy, _ = uuid.Parse(userID)
if err := h.repo.Update(&doc); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(doc)
}
func (h *DocumentHandler) Delete(w http.ResponseWriter, r *http.Request) {
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
id := mux.Vars(r)["id"]
if err := h.repo.Delete(id, tenantID); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func (h *DocumentHandler) GetSubpages(w http.ResponseWriter, r *http.Request) {
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
parentID := mux.Vars(r)["id"]
docs, err := h.repo.GetSubpages(parentID, tenantID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(docs)
}
func (h *DocumentHandler) GetActivities(w http.ResponseWriter, r *http.Request) {
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
id := mux.Vars(r)["id"]
activities, err := h.repo.GetActivities(id, tenantID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(activities)
}

View File

@@ -0,0 +1,399 @@
package handlers
import (
"aggios-app/backend/internal/api/middleware"
"aggios-app/backend/internal/domain"
"aggios-app/backend/internal/repository"
"encoding/json"
"log"
"net/http"
"time"
"github.com/google/uuid"
"github.com/gorilla/mux"
)
type ERPHandler struct {
repo *repository.ERPRepository
}
func NewERPHandler(repo *repository.ERPRepository) *ERPHandler {
return &ERPHandler{repo: repo}
}
// ==================== FINANCE ====================
func (h *ERPHandler) CreateFinancialCategory(w http.ResponseWriter, r *http.Request) {
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
var cat domain.FinancialCategory
if err := json.NewDecoder(r.Body).Decode(&cat); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
cat.ID = uuid.New()
cat.TenantID, _ = uuid.Parse(tenantID)
cat.IsActive = true
if err := h.repo.CreateFinancialCategory(&cat); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(cat)
}
func (h *ERPHandler) GetFinancialCategories(w http.ResponseWriter, r *http.Request) {
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
cats, err := h.repo.GetFinancialCategoriesByTenant(tenantID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(cats)
}
func (h *ERPHandler) CreateBankAccount(w http.ResponseWriter, r *http.Request) {
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
var acc domain.BankAccount
if err := json.NewDecoder(r.Body).Decode(&acc); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
acc.ID = uuid.New()
acc.TenantID, _ = uuid.Parse(tenantID)
acc.IsActive = true
if err := h.repo.CreateBankAccount(&acc); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(acc)
}
func (h *ERPHandler) GetBankAccounts(w http.ResponseWriter, r *http.Request) {
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
accs, err := h.repo.GetBankAccountsByTenant(tenantID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(accs)
}
func (h *ERPHandler) CreateTransaction(w http.ResponseWriter, r *http.Request) {
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
userID, _ := r.Context().Value(middleware.UserIDKey).(string)
var t domain.FinancialTransaction
if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
t.ID = uuid.New()
t.TenantID, _ = uuid.Parse(tenantID)
t.CreatedBy, _ = uuid.Parse(userID)
if err := h.repo.CreateTransaction(&t); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(t)
}
func (h *ERPHandler) GetTransactions(w http.ResponseWriter, r *http.Request) {
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
txs, err := h.repo.GetTransactionsByTenant(tenantID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(txs)
}
// ==================== PRODUCTS ====================
func (h *ERPHandler) CreateProduct(w http.ResponseWriter, r *http.Request) {
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
var p domain.Product
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
p.ID = uuid.New()
p.TenantID, _ = uuid.Parse(tenantID)
p.IsActive = true
if err := h.repo.CreateProduct(&p); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(p)
}
func (h *ERPHandler) GetProducts(w http.ResponseWriter, r *http.Request) {
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
products, err := h.repo.GetProductsByTenant(tenantID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(products)
}
// ==================== ORDERS ====================
type createOrderRequest struct {
Order domain.Order `json:"order"`
Items []domain.OrderItem `json:"items"`
}
func (h *ERPHandler) CreateOrder(w http.ResponseWriter, r *http.Request) {
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
userID, _ := r.Context().Value(middleware.UserIDKey).(string)
var req createOrderRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
req.Order.ID = uuid.New()
req.Order.TenantID, _ = uuid.Parse(tenantID)
req.Order.CreatedBy, _ = uuid.Parse(userID)
if req.Order.Status == "" {
req.Order.Status = "draft"
}
for i := range req.Items {
req.Items[i].ID = uuid.New()
req.Items[i].OrderID = req.Order.ID
req.Items[i].CreatedAt = time.Now()
}
if err := h.repo.CreateOrder(&req.Order, req.Items); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(req.Order)
}
func (h *ERPHandler) GetOrders(w http.ResponseWriter, r *http.Request) {
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
orders, err := h.repo.GetOrdersByTenant(tenantID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(orders)
}
// ==================== ENTITIES ====================
func (h *ERPHandler) CreateEntity(w http.ResponseWriter, r *http.Request) {
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
var e domain.Entity
if err := json.NewDecoder(r.Body).Decode(&e); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
e.ID = uuid.New()
e.TenantID, _ = uuid.Parse(tenantID)
if e.Status == "" {
e.Status = "active"
}
if err := h.repo.CreateEntity(&e); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(e)
}
func (h *ERPHandler) GetEntities(w http.ResponseWriter, r *http.Request) {
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
entityType := r.URL.Query().Get("type") // customer or supplier
entities, err := h.repo.GetEntitiesByTenant(tenantID, entityType)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(entities)
}
func (h *ERPHandler) UpdateTransaction(w http.ResponseWriter, r *http.Request) {
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
idStr := mux.Vars(r)["id"]
id, err := uuid.Parse(idStr)
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
var t domain.FinancialTransaction
if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
t.ID = id
t.TenantID, _ = uuid.Parse(tenantID)
if err := h.repo.UpdateTransaction(&t); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func (h *ERPHandler) DeleteTransaction(w http.ResponseWriter, r *http.Request) {
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
idStr := mux.Vars(r)["id"]
if err := h.repo.DeleteTransaction(idStr, tenantID); err != nil {
log.Printf("❌ Error deleting transaction: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func (h *ERPHandler) UpdateEntity(w http.ResponseWriter, r *http.Request) {
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
idStr := mux.Vars(r)["id"]
id, err := uuid.Parse(idStr)
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
var e domain.Entity
if err := json.NewDecoder(r.Body).Decode(&e); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
e.ID = id
e.TenantID, _ = uuid.Parse(tenantID)
if err := h.repo.UpdateEntity(&e); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func (h *ERPHandler) DeleteEntity(w http.ResponseWriter, r *http.Request) {
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
idStr := mux.Vars(r)["id"]
if err := h.repo.DeleteEntity(idStr, tenantID); err != nil {
log.Printf("❌ Error deleting entity: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func (h *ERPHandler) UpdateProduct(w http.ResponseWriter, r *http.Request) {
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
idStr := mux.Vars(r)["id"]
id, err := uuid.Parse(idStr)
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
var p domain.Product
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
p.ID = id
p.TenantID, _ = uuid.Parse(tenantID)
if err := h.repo.UpdateProduct(&p); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func (h *ERPHandler) DeleteProduct(w http.ResponseWriter, r *http.Request) {
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
idStr := mux.Vars(r)["id"]
if err := h.repo.DeleteProduct(idStr, tenantID); err != nil {
log.Printf("❌ Error deleting product: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func (h *ERPHandler) UpdateBankAccount(w http.ResponseWriter, r *http.Request) {
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
idStr := mux.Vars(r)["id"]
id, err := uuid.Parse(idStr)
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
var a domain.BankAccount
if err := json.NewDecoder(r.Body).Decode(&a); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
a.ID = id
a.TenantID, _ = uuid.Parse(tenantID)
if err := h.repo.UpdateBankAccount(&a); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func (h *ERPHandler) DeleteBankAccount(w http.ResponseWriter, r *http.Request) {
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
idStr := mux.Vars(r)["id"]
if err := h.repo.DeleteBankAccount(idStr, tenantID); err != nil {
log.Printf("❌ Error deleting bank account: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func (h *ERPHandler) DeleteOrder(w http.ResponseWriter, r *http.Request) {
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
idStr := mux.Vars(r)["id"]
if err := h.repo.DeleteOrder(idStr, tenantID); err != nil {
log.Printf("❌ Error deleting order: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}

View File

@@ -0,0 +1,93 @@
-- Migration: 025_create_erp_tables.sql
-- Description: Create tables for Finance, Inventory, and Order management
-- Financial Categories
CREATE TABLE IF NOT EXISTS erp_financial_categories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
type VARCHAR(20) NOT NULL CHECK (type IN ('income', 'expense')),
color VARCHAR(20),
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Bank Accounts
CREATE TABLE IF NOT EXISTS erp_bank_accounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
bank_name VARCHAR(255),
initial_balance DECIMAL(15,2) DEFAULT 0.00,
current_balance DECIMAL(15,2) DEFAULT 0.00,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Financial Transactions
CREATE TABLE IF NOT EXISTS erp_financial_transactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
account_id UUID REFERENCES erp_bank_accounts(id),
category_id UUID REFERENCES erp_financial_categories(id),
description TEXT,
amount DECIMAL(15,2) NOT NULL,
type VARCHAR(20) NOT NULL CHECK (type IN ('income', 'expense')),
status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'paid', 'cancelled')),
due_date DATE,
payment_date TIMESTAMP WITH TIME ZONE,
attachments TEXT[], -- URLs for proofs
created_by UUID REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Products & Services
CREATE TABLE IF NOT EXISTS erp_products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
sku VARCHAR(100),
description TEXT,
price DECIMAL(15,2) NOT NULL,
cost_price DECIMAL(15,2),
type VARCHAR(20) DEFAULT 'product' CHECK (type IN ('product', 'service')),
stock_quantity INT DEFAULT 0,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Orders
CREATE TABLE IF NOT EXISTS erp_orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
customer_id UUID REFERENCES companies(id), -- Linked to CRM (companies)
status VARCHAR(20) NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'confirmed', 'completed', 'cancelled')),
total_amount DECIMAL(15,2) DEFAULT 0.00,
notes TEXT,
created_by UUID REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Order Items
CREATE TABLE IF NOT EXISTS erp_order_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
order_id UUID NOT NULL REFERENCES erp_orders(id) ON DELETE CASCADE,
product_id UUID NOT NULL REFERENCES erp_products(id),
quantity INT NOT NULL DEFAULT 1,
unit_price DECIMAL(15,2) NOT NULL,
total_price DECIMAL(15,2) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Indexes for performance and multi-tenancy
CREATE INDEX idx_erp_fin_cat_tenant ON erp_financial_categories(tenant_id);
CREATE INDEX idx_erp_bank_acc_tenant ON erp_bank_accounts(tenant_id);
CREATE INDEX idx_erp_fin_trans_tenant ON erp_financial_transactions(tenant_id);
CREATE INDEX idx_erp_products_tenant ON erp_products(tenant_id);
CREATE INDEX idx_erp_orders_tenant ON erp_orders(tenant_id);
CREATE INDEX idx_erp_order_items_order ON erp_order_items(order_id);

View File

@@ -0,0 +1,32 @@
-- Migration: 026_create_erp_entities.sql
-- Description: Create tables for Customers and Suppliers in ERP
-- ERP Entities (Customers and Suppliers)
CREATE TABLE IF NOT EXISTS erp_entities (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
document VARCHAR(20), -- CPF/CNPJ
email VARCHAR(255),
phone VARCHAR(20),
type VARCHAR(20) NOT NULL CHECK (type IN ('customer', 'supplier', 'both')),
status VARCHAR(20) DEFAULT 'active',
address TEXT,
city VARCHAR(100),
state VARCHAR(2),
zip VARCHAR(10),
notes TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Update Financial Transactions to link with Entities
ALTER TABLE erp_financial_transactions ADD COLUMN IF NOT EXISTS entity_id UUID REFERENCES erp_entities(id);
-- Update Orders to link with Entities instead of companies (optional but more consistent for ERP)
-- Keep customer_id for now to avoid breaking existing logic, but allow entity_id
ALTER TABLE erp_orders ADD COLUMN IF NOT EXISTS entity_id UUID REFERENCES erp_entities(id);
-- Indexes
CREATE INDEX IF NOT EXISTS idx_erp_entities_tenant ON erp_entities(tenant_id);
CREATE INDEX IF NOT EXISTS idx_erp_entities_type ON erp_entities(type);

View File

@@ -0,0 +1,4 @@
-- Migration: 027_add_payment_method_to_transactions.sql
-- Description: Add payment_method field to financial transactions
ALTER TABLE erp_financial_transactions ADD COLUMN IF NOT EXISTS payment_method VARCHAR(50);

View File

@@ -0,0 +1,5 @@
-- Migration: 028_add_crm_links_to_transactions.sql
-- Description: Add fields to link financial transactions to CRM Customers and Companies
ALTER TABLE erp_financial_transactions ADD COLUMN IF NOT EXISTS crm_customer_id UUID REFERENCES crm_customers(id) ON DELETE SET NULL;
ALTER TABLE erp_financial_transactions ADD COLUMN IF NOT EXISTS company_id UUID REFERENCES companies(id) ON DELETE SET NULL;

View File

@@ -0,0 +1,15 @@
-- Migration: 029_create_documents_table.sql
-- Description: Create table for text documents (Google Docs style)
CREATE TABLE IF NOT EXISTS documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL,
content TEXT,
status VARCHAR(50) DEFAULT 'draft',
created_by UUID REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_documents_tenant_id ON documents(tenant_id);

View File

@@ -0,0 +1,22 @@
-- Migration: 030_add_subpages_and_activities_to_documents.sql
-- Description: Add parent_id for subpages and tracking columns (Fixed)
ALTER TABLE documents
ADD COLUMN IF NOT EXISTS parent_id UUID REFERENCES documents(id) ON DELETE CASCADE,
ADD COLUMN IF NOT EXISTS last_updated_by UUID REFERENCES users(id),
ADD COLUMN IF NOT EXISTS version INTEGER DEFAULT 1;
CREATE INDEX IF NOT EXISTS idx_documents_parent_id ON documents(parent_id);
-- Simple activity log table
CREATE TABLE IF NOT EXISTS document_activities (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id),
action VARCHAR(50) NOT NULL, -- 'created', 'updated', 'deleted', 'status_change'
description TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_doc_activities_doc_id ON document_activities(document_id);

View File

@@ -0,0 +1,32 @@
package domain
import (
"time"
"github.com/google/uuid"
)
type Document struct {
ID uuid.UUID `json:"id" db:"id"`
TenantID uuid.UUID `json:"tenant_id" db:"tenant_id"`
ParentID *uuid.UUID `json:"parent_id" db:"parent_id"`
Title string `json:"title" db:"title"`
Content string `json:"content" db:"content"` // JSON for blocks
Status string `json:"status" db:"status"` // draft, published
CreatedBy uuid.UUID `json:"created_by" db:"created_by"`
LastUpdatedBy uuid.UUID `json:"last_updated_by" db:"last_updated_by"`
Version int `json:"version" db:"version"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
type DocumentActivity struct {
ID uuid.UUID `json:"id" db:"id"`
DocumentID uuid.UUID `json:"document_id" db:"document_id"`
TenantID uuid.UUID `json:"tenant_id" db:"tenant_id"`
UserID uuid.UUID `json:"user_id" db:"user_id"`
UserName string `json:"user_name" db:"user_name"` // For join
Action string `json:"action" db:"action"`
Description string `json:"description" db:"description"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
}

View File

@@ -0,0 +1,115 @@
package domain
import (
"time"
"github.com/google/uuid"
"github.com/shopspring/decimal"
)
// FinancialCategory represents a category for income or expenses
type FinancialCategory struct {
ID uuid.UUID `json:"id" db:"id"`
TenantID uuid.UUID `json:"tenant_id" db:"tenant_id"`
Name string `json:"name" db:"name"`
Type string `json:"type" db:"type"` // income, expense
Color string `json:"color" db:"color"`
IsActive bool `json:"is_active" db:"is_active"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// BankAccount represents a financial account in the agency
type BankAccount struct {
ID uuid.UUID `json:"id" db:"id"`
TenantID uuid.UUID `json:"tenant_id" db:"tenant_id"`
Name string `json:"name" db:"name"`
BankName string `json:"bank_name" db:"bank_name"`
InitialBalance decimal.Decimal `json:"initial_balance" db:"initial_balance"`
CurrentBalance decimal.Decimal `json:"current_balance" db:"current_balance"`
IsActive bool `json:"is_active" db:"is_active"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// Entity represents a customer or supplier in the ERP
type Entity struct {
ID uuid.UUID `json:"id" db:"id"`
TenantID uuid.UUID `json:"tenant_id" db:"tenant_id"`
Name string `json:"name" db:"name"`
Document string `json:"document" db:"document"`
Email string `json:"email" db:"email"`
Phone string `json:"phone" db:"phone"`
Type string `json:"type" db:"type"` // customer, supplier, both
Status string `json:"status" db:"status"`
Address string `json:"address" db:"address"`
City string `json:"city" db:"city"`
State string `json:"state" db:"state"`
Zip string `json:"zip" db:"zip"`
Notes string `json:"notes" db:"notes"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// FinancialTransaction represents a single financial movement
type FinancialTransaction struct {
ID uuid.UUID `json:"id" db:"id"`
TenantID uuid.UUID `json:"tenant_id" db:"tenant_id"`
AccountID *uuid.UUID `json:"account_id" db:"account_id"`
CategoryID *uuid.UUID `json:"category_id" db:"category_id"`
EntityID *uuid.UUID `json:"entity_id" db:"entity_id"`
CRMCustomerID *uuid.UUID `json:"crm_customer_id" db:"crm_customer_id"`
CompanyID *uuid.UUID `json:"company_id" db:"company_id"`
Description string `json:"description" db:"description"`
Amount decimal.Decimal `json:"amount" db:"amount"`
Type string `json:"type" db:"type"` // income, expense
Status string `json:"status" db:"status"` // pending, paid, cancelled
DueDate *time.Time `json:"due_date" db:"due_date"`
PaymentDate *time.Time `json:"payment_date" db:"payment_date"`
PaymentMethod string `json:"payment_method" db:"payment_method"`
Attachments []string `json:"attachments" db:"attachments"`
CreatedBy uuid.UUID `json:"created_by" db:"created_by"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// Product represents a product or service in the catalog
type Product struct {
ID uuid.UUID `json:"id" db:"id"`
TenantID uuid.UUID `json:"tenant_id" db:"tenant_id"`
Name string `json:"name" db:"name"`
SKU string `json:"sku" db:"sku"`
Description string `json:"description" db:"description"`
Price decimal.Decimal `json:"price" db:"price"`
CostPrice decimal.Decimal `json:"cost_price" db:"cost_price"`
Type string `json:"type" db:"type"` // product, service
StockQuantity int `json:"stock_quantity" db:"stock_quantity"`
IsActive bool `json:"is_active" db:"is_active"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// Order represents a sales or service order
type Order struct {
ID uuid.UUID `json:"id" db:"id"`
TenantID uuid.UUID `json:"tenant_id" db:"tenant_id"`
CustomerID *uuid.UUID `json:"customer_id" db:"customer_id"`
EntityID *uuid.UUID `json:"entity_id" db:"entity_id"`
Status string `json:"status" db:"status"` // draft, confirmed, completed, cancelled
TotalAmount decimal.Decimal `json:"total_amount" db:"total_amount"`
Notes string `json:"notes" db:"notes"`
CreatedBy uuid.UUID `json:"created_by" db:"created_by"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// OrderItem represents an item within an order
type OrderItem struct {
ID uuid.UUID `json:"id" db:"id"`
OrderID uuid.UUID `json:"order_id" db:"order_id"`
ProductID uuid.UUID `json:"product_id" db:"product_id"`
Quantity int `json:"quantity" db:"quantity"`
UnitPrice decimal.Decimal `json:"unit_price" db:"unit_price"`
TotalPrice decimal.Decimal `json:"total_price" db:"total_price"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
}

View File

@@ -0,0 +1,156 @@
package repository
import (
"aggios-app/backend/internal/domain"
"database/sql"
"fmt"
)
type DocumentRepository struct {
db *sql.DB
}
func NewDocumentRepository(db *sql.DB) *DocumentRepository {
return &DocumentRepository{db: db}
}
func (r *DocumentRepository) Create(doc *domain.Document) error {
query := `
INSERT INTO documents (id, tenant_id, parent_id, title, content, status, created_by, last_updated_by, version, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $7, 1, NOW(), NOW())
`
_, err := r.db.Exec(query, doc.ID, doc.TenantID, doc.ParentID, doc.Title, doc.Content, doc.Status, doc.CreatedBy)
if err != nil {
return err
}
return r.logActivity(doc.ID.String(), doc.TenantID.String(), doc.CreatedBy.String(), "created", "Criou o documento")
}
func (r *DocumentRepository) GetByTenant(tenantID string) ([]domain.Document, error) {
query := `
SELECT id, tenant_id, parent_id, title, content, status, created_by, last_updated_by, version, created_at, updated_at
FROM documents
WHERE tenant_id = $1 AND parent_id IS NULL
ORDER BY updated_at DESC
`
rows, err := r.db.Query(query, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
var docs []domain.Document
for rows.Next() {
var doc domain.Document
if err := rows.Scan(&doc.ID, &doc.TenantID, &doc.ParentID, &doc.Title, &doc.Content, &doc.Status, &doc.CreatedBy, &doc.LastUpdatedBy, &doc.Version, &doc.CreatedAt, &doc.UpdatedAt); err != nil {
return nil, err
}
docs = append(docs, doc)
}
return docs, nil
}
func (r *DocumentRepository) GetSubpages(parentID, tenantID string) ([]domain.Document, error) {
query := `
SELECT id, tenant_id, parent_id, title, content, status, created_by, last_updated_by, version, created_at, updated_at
FROM documents
WHERE parent_id = $1 AND tenant_id = $2
ORDER BY created_at ASC
`
rows, err := r.db.Query(query, parentID, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
var docs []domain.Document
for rows.Next() {
var doc domain.Document
if err := rows.Scan(&doc.ID, &doc.TenantID, &doc.ParentID, &doc.Title, &doc.Content, &doc.Status, &doc.CreatedBy, &doc.LastUpdatedBy, &doc.Version, &doc.CreatedAt, &doc.UpdatedAt); err != nil {
return nil, err
}
docs = append(docs, doc)
}
return docs, nil
}
func (r *DocumentRepository) GetByID(id, tenantID string) (*domain.Document, error) {
query := `
SELECT id, tenant_id, parent_id, title, content, status, created_by, last_updated_by, version, created_at, updated_at
FROM documents
WHERE id = $1 AND tenant_id = $2
`
var doc domain.Document
err := r.db.QueryRow(query, id, tenantID).Scan(
&doc.ID, &doc.TenantID, &doc.ParentID, &doc.Title, &doc.Content, &doc.Status, &doc.CreatedBy, &doc.LastUpdatedBy, &doc.Version, &doc.CreatedAt, &doc.UpdatedAt,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
return &doc, nil
}
func (r *DocumentRepository) Update(doc *domain.Document) error {
query := `
UPDATE documents
SET title = $1, content = $2, status = $3, last_updated_by = $4, version = version + 1, updated_at = NOW()
WHERE id = $5 AND tenant_id = $6
`
_, err := r.db.Exec(query, doc.Title, doc.Content, doc.Status, doc.LastUpdatedBy, doc.ID, doc.TenantID)
if err != nil {
return err
}
return r.logActivity(doc.ID.String(), doc.TenantID.String(), doc.LastUpdatedBy.String(), "updated", "Atualizou o conteúdo")
}
func (r *DocumentRepository) Delete(id, tenantID string) error {
query := "DELETE FROM documents WHERE id = $1 AND tenant_id = $2"
res, err := r.db.Exec(query, id, tenantID)
if err != nil {
return err
}
rows, _ := res.RowsAffected()
if rows == 0 {
return fmt.Errorf("document not found")
}
return nil
}
func (r *DocumentRepository) logActivity(docID, tenantID, userID, action, description string) error {
query := `
INSERT INTO document_activities (document_id, tenant_id, user_id, action, description)
VALUES ($1, $2, $3, $4, $5)
`
_, err := r.db.Exec(query, docID, tenantID, userID, action, description)
return err
}
func (r *DocumentRepository) GetActivities(docID, tenantID string) ([]domain.DocumentActivity, error) {
query := `
SELECT a.id, a.document_id, a.tenant_id, a.user_id, COALESCE(u.first_name, 'Usuário Removido') as user_name, a.action, a.description, a.created_at
FROM document_activities a
LEFT JOIN users u ON a.user_id = u.id
WHERE a.document_id = $1 AND a.tenant_id = $2
ORDER BY a.created_at DESC
LIMIT 20
`
rows, err := r.db.Query(query, docID, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
var activities []domain.DocumentActivity
for rows.Next() {
var a domain.DocumentActivity
err := rows.Scan(&a.ID, &a.DocumentID, &a.TenantID, &a.UserID, &a.UserName, &a.Action, &a.Description, &a.CreatedAt)
if err != nil {
return nil, err
}
activities = append(activities, a)
}
return activities, nil
}

View File

@@ -0,0 +1,493 @@
package repository
import (
"aggios-app/backend/internal/domain"
"database/sql"
"github.com/lib/pq"
)
type ERPRepository struct {
db *sql.DB
}
func NewERPRepository(db *sql.DB) *ERPRepository {
return &ERPRepository{db: db}
}
// ==================== FINANCE: CATEGORIES ====================
func (r *ERPRepository) CreateFinancialCategory(cat *domain.FinancialCategory) error {
query := `
INSERT INTO erp_financial_categories (id, tenant_id, name, type, color, is_active)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING created_at, updated_at
`
return r.db.QueryRow(
query,
cat.ID, cat.TenantID, cat.Name, cat.Type, cat.Color, cat.IsActive,
).Scan(&cat.CreatedAt, &cat.UpdatedAt)
}
func (r *ERPRepository) GetFinancialCategoriesByTenant(tenantID string) ([]domain.FinancialCategory, error) {
query := `
SELECT id, tenant_id, name, type, color, is_active, created_at, updated_at
FROM erp_financial_categories
WHERE tenant_id = $1
ORDER BY name ASC
`
rows, err := r.db.Query(query, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
var categories []domain.FinancialCategory
for rows.Next() {
var c domain.FinancialCategory
err := rows.Scan(&c.ID, &c.TenantID, &c.Name, &c.Type, &c.Color, &c.IsActive, &c.CreatedAt, &c.UpdatedAt)
if err != nil {
return nil, err
}
categories = append(categories, c)
}
return categories, nil
}
// ==================== FINANCE: BANK ACCOUNTS ====================
func (r *ERPRepository) CreateBankAccount(acc *domain.BankAccount) error {
query := `
INSERT INTO erp_bank_accounts (id, tenant_id, name, bank_name, initial_balance, current_balance, is_active)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING created_at, updated_at
`
return r.db.QueryRow(
query,
acc.ID, acc.TenantID, acc.Name, acc.BankName, acc.InitialBalance, acc.InitialBalance, acc.IsActive,
).Scan(&acc.CreatedAt, &acc.UpdatedAt)
}
func (r *ERPRepository) GetBankAccountsByTenant(tenantID string) ([]domain.BankAccount, error) {
query := `
SELECT id, tenant_id, name, bank_name, initial_balance, current_balance, is_active, created_at, updated_at
FROM erp_bank_accounts
WHERE tenant_id = $1
ORDER BY name ASC
`
rows, err := r.db.Query(query, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
var accounts []domain.BankAccount
for rows.Next() {
var a domain.BankAccount
err := rows.Scan(&a.ID, &a.TenantID, &a.Name, &a.BankName, &a.InitialBalance, &a.CurrentBalance, &a.IsActive, &a.CreatedAt, &a.UpdatedAt)
if err != nil {
return nil, err
}
accounts = append(accounts, a)
}
return accounts, nil
}
// ==================== ENTITIES: CUSTOMERS & SUPPLIERS ====================
func (r *ERPRepository) CreateEntity(e *domain.Entity) error {
query := `
INSERT INTO erp_entities (id, tenant_id, name, document, email, phone, type, status, address, city, state, zip, notes)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
RETURNING created_at, updated_at
`
return r.db.QueryRow(
query,
e.ID, e.TenantID, e.Name, e.Document, e.Email, e.Phone, e.Type, e.Status, e.Address, e.City, e.State, e.Zip, e.Notes,
).Scan(&e.CreatedAt, &e.UpdatedAt)
}
func (r *ERPRepository) GetEntitiesByTenant(tenantID string, entityType string) ([]domain.Entity, error) {
query := `
SELECT id, tenant_id, name, document, email, phone, type, status, address, city, state, zip, notes, created_at, updated_at
FROM erp_entities
WHERE tenant_id = $1
`
var args []interface{}
args = append(args, tenantID)
if entityType != "" {
query += " AND (type = $2 OR type = 'both')"
args = append(args, entityType)
}
query += " ORDER BY name ASC"
rows, err := r.db.Query(query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var entities []domain.Entity
for rows.Next() {
var e domain.Entity
err := rows.Scan(
&e.ID, &e.TenantID, &e.Name, &e.Document, &e.Email, &e.Phone, &e.Type, &e.Status, &e.Address, &e.City, &e.State, &e.Zip, &e.Notes, &e.CreatedAt, &e.UpdatedAt,
)
if err != nil {
return nil, err
}
entities = append(entities, e)
}
return entities, nil
}
// ==================== FINANCE: TRANSACTIONS ====================
func (r *ERPRepository) CreateTransaction(t *domain.FinancialTransaction) error {
tx, err := r.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
query := `
INSERT INTO erp_financial_transactions (
id, tenant_id, account_id, category_id, entity_id, crm_customer_id, company_id, description, amount, type, status, due_date, payment_date, payment_method, attachments, created_by
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
RETURNING created_at, updated_at
`
err = tx.QueryRow(
query,
t.ID, t.TenantID, t.AccountID, t.CategoryID, t.EntityID, t.CRMCustomerID, t.CompanyID, t.Description, t.Amount, t.Type, t.Status, t.DueDate, t.PaymentDate, t.PaymentMethod, pq.Array(t.Attachments), t.CreatedBy,
).Scan(&t.CreatedAt, &t.UpdatedAt)
if err != nil {
return err
}
// Update balance if paid
if t.Status == "paid" && t.AccountID != nil {
balanceQuery := ""
if t.Type == "income" {
balanceQuery = "UPDATE erp_bank_accounts SET current_balance = current_balance + $1 WHERE id = $2"
} else {
balanceQuery = "UPDATE erp_bank_accounts SET current_balance = current_balance - $1 WHERE id = $2"
}
_, err = tx.Exec(balanceQuery, t.Amount, t.AccountID)
if err != nil {
return err
}
}
return tx.Commit()
}
func (r *ERPRepository) GetTransactionsByTenant(tenantID string) ([]domain.FinancialTransaction, error) {
query := `
SELECT id, tenant_id, account_id, category_id, entity_id, crm_customer_id, company_id, description, amount, type, status, due_date, payment_date, payment_method, attachments, created_by, created_at, updated_at
FROM erp_financial_transactions
WHERE tenant_id = $1
ORDER BY created_at DESC
`
rows, err := r.db.Query(query, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
var transactions []domain.FinancialTransaction
for rows.Next() {
var t domain.FinancialTransaction
err := rows.Scan(
&t.ID, &t.TenantID, &t.AccountID, &t.CategoryID, &t.EntityID, &t.CRMCustomerID, &t.CompanyID, &t.Description, &t.Amount, &t.Type, &t.Status, &t.DueDate, &t.PaymentDate, &t.PaymentMethod, pq.Array(&t.Attachments), &t.CreatedBy, &t.CreatedAt, &t.UpdatedAt,
)
if err != nil {
return nil, err
}
transactions = append(transactions, t)
}
return transactions, nil
}
// ==================== PRODUCTS ====================
func (r *ERPRepository) CreateProduct(p *domain.Product) error {
query := `
INSERT INTO erp_products (id, tenant_id, name, sku, description, price, cost_price, type, stock_quantity, is_active)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING created_at, updated_at
`
return r.db.QueryRow(
query,
p.ID, p.TenantID, p.Name, p.SKU, p.Description, p.Price, p.CostPrice, p.Type, p.StockQuantity, p.IsActive,
).Scan(&p.CreatedAt, &p.UpdatedAt)
}
func (r *ERPRepository) GetProductsByTenant(tenantID string) ([]domain.Product, error) {
query := `
SELECT id, tenant_id, name, sku, description, price, cost_price, type, stock_quantity, is_active, created_at, updated_at
FROM erp_products
WHERE tenant_id = $1
ORDER BY name ASC
`
rows, err := r.db.Query(query, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
var products []domain.Product
for rows.Next() {
var p domain.Product
err := rows.Scan(&p.ID, &p.TenantID, &p.Name, &p.SKU, &p.Description, &p.Price, &p.CostPrice, &p.Type, &p.StockQuantity, &p.IsActive, &p.CreatedAt, &p.UpdatedAt)
if err != nil {
return nil, err
}
products = append(products, p)
}
return products, nil
}
// ==================== ORDERS ====================
func (r *ERPRepository) CreateOrder(o *domain.Order, items []domain.OrderItem) error {
tx, err := r.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
orderQuery := `
INSERT INTO erp_orders (id, tenant_id, customer_id, entity_id, status, total_amount, notes, created_by)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING created_at, updated_at
`
err = tx.QueryRow(
orderQuery,
o.ID, o.TenantID, o.CustomerID, o.EntityID, o.Status, o.TotalAmount, o.Notes, o.CreatedBy,
).Scan(&o.CreatedAt, &o.UpdatedAt)
if err != nil {
return err
}
itemQuery := `
INSERT INTO erp_order_items (id, order_id, product_id, quantity, unit_price, total_price)
VALUES ($1, $2, $3, $4, $5, $6)
`
for _, item := range items {
_, err = tx.Exec(itemQuery, item.ID, o.ID, item.ProductID, item.Quantity, item.UnitPrice, item.TotalPrice)
if err != nil {
return err
}
// Update stock if product
stockQuery := "UPDATE erp_products SET stock_quantity = stock_quantity - $1 WHERE id = $2 AND type = 'product'"
_, err = tx.Exec(stockQuery, item.Quantity, item.ProductID)
if err != nil {
return err
}
}
return tx.Commit()
}
func (r *ERPRepository) GetOrdersByTenant(tenantID string) ([]domain.Order, error) {
query := `
SELECT id, tenant_id, customer_id, status, total_amount, notes, created_by, created_at, updated_at
FROM erp_orders
WHERE tenant_id = $1
ORDER BY created_at DESC
`
rows, err := r.db.Query(query, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
var orders []domain.Order
for rows.Next() {
var o domain.Order
err := rows.Scan(&o.ID, &o.TenantID, &o.CustomerID, &o.Status, &o.TotalAmount, &o.Notes, &o.CreatedBy, &o.CreatedAt, &o.UpdatedAt)
if err != nil {
return nil, err
}
orders = append(orders, o)
}
return orders, nil
}
func (r *ERPRepository) UpdateTransaction(t *domain.FinancialTransaction) error {
tx, err := r.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
// Get old transaction to adjust balance
var oldT domain.FinancialTransaction
err = tx.QueryRow(`
SELECT amount, type, status, account_id
FROM erp_financial_transactions
WHERE id = $1 AND tenant_id = $2`, t.ID, t.TenantID).
Scan(&oldT.Amount, &oldT.Type, &oldT.Status, &oldT.AccountID)
if err != nil {
return err
}
// Falls back to old type if not provided in request
if t.Type == "" {
t.Type = oldT.Type
}
// Reverse old balance impact
if oldT.Status == "paid" && oldT.AccountID != nil {
balanceQuery := ""
if oldT.Type == "income" {
balanceQuery = "UPDATE erp_bank_accounts SET current_balance = current_balance - $1 WHERE id = $2"
} else {
balanceQuery = "UPDATE erp_bank_accounts SET current_balance = current_balance + $1 WHERE id = $2"
}
_, err = tx.Exec(balanceQuery, oldT.Amount, oldT.AccountID)
if err != nil {
return err
}
}
query := `
UPDATE erp_financial_transactions
SET description = $1, amount = $2, type = $3, status = $4, due_date = $5, payment_date = $6,
category_id = $7, entity_id = $8, crm_customer_id = $9, company_id = $10, account_id = $11, payment_method = $12, updated_at = NOW()
WHERE id = $13 AND tenant_id = $14
`
_, err = tx.Exec(query,
t.Description, t.Amount, t.Type, t.Status, t.DueDate, t.PaymentDate,
t.CategoryID, t.EntityID, t.CRMCustomerID, t.CompanyID, t.AccountID, t.PaymentMethod,
t.ID, t.TenantID)
if err != nil {
return err
}
// Apply new balance impact
if t.Status == "paid" && t.AccountID != nil {
balanceQuery := ""
if t.Type == "income" {
balanceQuery = "UPDATE erp_bank_accounts SET current_balance = current_balance + $1 WHERE id = $2"
} else {
balanceQuery = "UPDATE erp_bank_accounts SET current_balance = current_balance - $1 WHERE id = $2"
}
_, err = tx.Exec(balanceQuery, t.Amount, t.AccountID)
if err != nil {
return err
}
}
return tx.Commit()
}
func (r *ERPRepository) DeleteTransaction(id, tenantID string) error {
tx, err := r.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
// Adjust balance before delete
var t domain.FinancialTransaction
err = tx.QueryRow(`
SELECT amount, type, status, account_id
FROM erp_financial_transactions
WHERE id = $1 AND tenant_id = $2`, id, tenantID).
Scan(&t.Amount, &t.Type, &t.Status, &t.AccountID)
if err != nil {
return err
}
if t.Status == "paid" && t.AccountID != nil {
balanceQuery := ""
if t.Type == "income" {
balanceQuery = "UPDATE erp_bank_accounts SET current_balance = current_balance - $1 WHERE id = $2"
} else {
balanceQuery = "UPDATE erp_bank_accounts SET current_balance = current_balance + $1 WHERE id = $2"
}
_, err = tx.Exec(balanceQuery, t.Amount, t.AccountID)
if err != nil {
return err
}
}
_, err = tx.Exec("DELETE FROM erp_financial_transactions WHERE id = $1 AND tenant_id = $2", id, tenantID)
if err != nil {
return err
}
return tx.Commit()
}
func (r *ERPRepository) UpdateEntity(e *domain.Entity) error {
query := `
UPDATE erp_entities
SET name = $1, document = $2, email = $3, phone = $4, type = $5, status = $6,
address = $7, city = $8, state = $9, zip = $10, notes = $11, updated_at = NOW()
WHERE id = $12 AND tenant_id = $13
`
_, err := r.db.Exec(query, e.Name, e.Document, e.Email, e.Phone, e.Type, e.Status, e.Address, e.City, e.State, e.Zip, e.Notes, e.ID, e.TenantID)
return err
}
func (r *ERPRepository) DeleteEntity(id, tenantID string) error {
_, err := r.db.Exec("DELETE FROM erp_entities WHERE id = $1 AND tenant_id = $2", id, tenantID)
return err
}
func (r *ERPRepository) UpdateProduct(p *domain.Product) error {
query := `
UPDATE erp_products
SET name = $1, sku = $2, description = $3, price = $4, cost_price = $5,
type = $6, stock_quantity = $7, is_active = $8, updated_at = NOW()
WHERE id = $9 AND tenant_id = $10
`
_, err := r.db.Exec(query, p.Name, p.SKU, p.Description, p.Price, p.CostPrice, p.Type, p.StockQuantity, p.IsActive, p.ID, p.TenantID)
return err
}
func (r *ERPRepository) DeleteProduct(id, tenantID string) error {
_, err := r.db.Exec("DELETE FROM erp_products WHERE id = $1 AND tenant_id = $2", id, tenantID)
return err
}
func (r *ERPRepository) UpdateBankAccount(a *domain.BankAccount) error {
query := `
UPDATE erp_bank_accounts
SET name = $1, bank_name = $2, initial_balance = $3, is_active = $4, updated_at = NOW()
WHERE id = $5 AND tenant_id = $6
`
_, err := r.db.Exec(query, a.Name, a.BankName, a.InitialBalance, a.IsActive, a.ID, a.TenantID)
return err
}
func (r *ERPRepository) DeleteBankAccount(id, tenantID string) error {
_, err := r.db.Exec("DELETE FROM erp_bank_accounts WHERE id = $1 AND tenant_id = $2", id, tenantID)
return err
}
func (r *ERPRepository) DeleteOrder(id, tenantID string) error {
tx, err := r.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
// Deleta os itens do pedido primeiro
_, err = tx.Exec("DELETE FROM erp_order_items WHERE order_id = $1", id)
if err != nil {
return err
}
// Deleta o pedido
_, err = tx.Exec("DELETE FROM erp_orders WHERE id = $1 AND tenant_id = $2", id, tenantID)
if err != nil {
return err
}
return tx.Commit()
}