Files
aggios.app/backend/internal/repository/crm_repository.go

1160 lines
33 KiB
Go

package repository
import (
"aggios-app/backend/internal/domain"
"database/sql"
"fmt"
"log"
"github.com/lib/pq"
)
type CRMRepository struct {
db *sql.DB
}
func NewCRMRepository(db *sql.DB) *CRMRepository {
return &CRMRepository{db: db}
}
// ==================== CUSTOMERS ====================
func (r *CRMRepository) CreateCustomer(customer *domain.CRMCustomer) error {
query := `
INSERT INTO crm_customers (
id, tenant_id, name, email, phone, company, position,
address, city, state, zip_code, country, notes, tags,
is_active, created_by, logo_url
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
RETURNING created_at, updated_at
`
// Handle optional created_by field (NULL for public registrations)
var createdBy interface{}
if customer.CreatedBy != "" {
createdBy = customer.CreatedBy
} else {
createdBy = nil
}
return r.db.QueryRow(
query,
customer.ID, customer.TenantID, customer.Name, customer.Email, customer.Phone,
customer.Company, customer.Position, customer.Address, customer.City, customer.State,
customer.ZipCode, customer.Country, customer.Notes, pq.Array(customer.Tags),
customer.IsActive, createdBy, customer.LogoURL,
).Scan(&customer.CreatedAt, &customer.UpdatedAt)
}
func (r *CRMRepository) GetCustomersByTenant(tenantID string) ([]domain.CRMCustomer, error) {
query := `
SELECT id, tenant_id, name, email, phone, company, position,
address, city, state, zip_code, country, notes, tags,
is_active, COALESCE(created_by::text, '') AS created_by, created_at, updated_at,
COALESCE(logo_url, '') as logo_url
FROM crm_customers
WHERE tenant_id = $1 AND is_active = true
ORDER BY created_at DESC
`
rows, err := r.db.Query(query, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
var customers []domain.CRMCustomer
for rows.Next() {
var c domain.CRMCustomer
err := rows.Scan(
&c.ID, &c.TenantID, &c.Name, &c.Email, &c.Phone, &c.Company, &c.Position,
&c.Address, &c.City, &c.State, &c.ZipCode, &c.Country, &c.Notes, pq.Array(&c.Tags),
&c.IsActive, &c.CreatedBy, &c.CreatedAt, &c.UpdatedAt, &c.LogoURL,
)
if err != nil {
return nil, err
}
customers = append(customers, c)
}
return customers, nil
}
func (r *CRMRepository) GetCustomerByID(id string, tenantID string) (*domain.CRMCustomer, error) {
query := `
SELECT id, tenant_id, name, email, phone, company, position,
address, city, state, zip_code, country, notes, tags,
is_active, COALESCE(created_by::text, '') AS created_by, created_at, updated_at,
COALESCE(logo_url, '') as logo_url
FROM crm_customers
WHERE id = $1 AND tenant_id = $2
`
var c domain.CRMCustomer
err := r.db.QueryRow(query, id, tenantID).Scan(
&c.ID, &c.TenantID, &c.Name, &c.Email, &c.Phone, &c.Company, &c.Position,
&c.Address, &c.City, &c.State, &c.ZipCode, &c.Country, &c.Notes, pq.Array(&c.Tags),
&c.IsActive, &c.CreatedBy, &c.CreatedAt, &c.UpdatedAt, &c.LogoURL,
)
if err != nil {
return nil, err
}
return &c, nil
}
func (r *CRMRepository) UpdateCustomer(customer *domain.CRMCustomer) error {
query := `
UPDATE crm_customers SET
name = $1, email = $2, phone = $3, company = $4, position = $5,
address = $6, city = $7, state = $8, zip_code = $9, country = $10,
notes = $11, tags = $12, is_active = $13, logo_url = $14
WHERE id = $15 AND tenant_id = $16
`
result, err := r.db.Exec(
query,
customer.Name, customer.Email, customer.Phone, customer.Company, customer.Position,
customer.Address, customer.City, customer.State, customer.ZipCode, customer.Country,
customer.Notes, pq.Array(customer.Tags), customer.IsActive, customer.LogoURL,
customer.ID, customer.TenantID,
)
if err != nil {
return err
}
rows, err := result.RowsAffected()
if err != nil {
return err
}
if rows == 0 {
return fmt.Errorf("customer not found")
}
return nil
}
func (r *CRMRepository) DeleteCustomer(id string, tenantID string) error {
query := `DELETE FROM crm_customers WHERE id = $1 AND tenant_id = $2`
result, err := r.db.Exec(query, id, tenantID)
if err != nil {
return err
}
rows, err := result.RowsAffected()
if err != nil {
return err
}
if rows == 0 {
return fmt.Errorf("customer not found")
}
return nil
}
// ==================== LISTS ====================
func (r *CRMRepository) CreateList(list *domain.CRMList) error {
query := `
INSERT INTO crm_lists (id, tenant_id, customer_id, funnel_id, name, description, color, created_by)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING created_at, updated_at
`
return r.db.QueryRow(
query,
list.ID, list.TenantID, list.CustomerID, list.FunnelID, list.Name, list.Description, list.Color, list.CreatedBy,
).Scan(&list.CreatedAt, &list.UpdatedAt)
}
func (r *CRMRepository) GetListsByTenant(tenantID string) ([]domain.CRMListWithCustomers, error) {
query := `
SELECT l.id, l.tenant_id, l.customer_id, l.funnel_id, l.name, l.description, l.color, l.created_by,
l.created_at, l.updated_at,
COALESCE(c.name, '') as customer_name,
(SELECT COUNT(*) FROM crm_customer_lists cl WHERE cl.list_id = l.id) as customer_count,
(SELECT COUNT(*) FROM crm_lead_lists ll WHERE ll.list_id = l.id) as lead_count
FROM crm_lists l
LEFT JOIN crm_customers c ON l.customer_id = c.id
WHERE l.tenant_id = $1
ORDER BY l.created_at DESC
`
rows, err := r.db.Query(query, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
var lists []domain.CRMListWithCustomers
for rows.Next() {
var l domain.CRMListWithCustomers
err := rows.Scan(
&l.ID, &l.TenantID, &l.CustomerID, &l.FunnelID, &l.Name, &l.Description, &l.Color, &l.CreatedBy,
&l.CreatedAt, &l.UpdatedAt, &l.CustomerName, &l.CustomerCount, &l.LeadCount,
)
if err != nil {
return nil, err
}
lists = append(lists, l)
}
return lists, nil
}
func (r *CRMRepository) GetListByID(id string, tenantID string) (*domain.CRMList, error) {
query := `
SELECT id, tenant_id, customer_id, funnel_id, name, description, color, created_by, created_at, updated_at
FROM crm_lists
WHERE id = $1 AND tenant_id = $2
`
var l domain.CRMList
err := r.db.QueryRow(query, id, tenantID).Scan(
&l.ID, &l.TenantID, &l.CustomerID, &l.FunnelID, &l.Name, &l.Description, &l.Color, &l.CreatedBy,
&l.CreatedAt, &l.UpdatedAt,
)
if err != nil {
return nil, err
}
return &l, nil
}
func (r *CRMRepository) UpdateList(list *domain.CRMList) error {
query := `
UPDATE crm_lists SET
name = $1, description = $2, color = $3, customer_id = $4, funnel_id = $5
WHERE id = $6 AND tenant_id = $7
`
result, err := r.db.Exec(query, list.Name, list.Description, list.Color, list.CustomerID, list.FunnelID, list.ID, list.TenantID)
if err != nil {
return err
}
rows, err := result.RowsAffected()
if err != nil {
return err
}
if rows == 0 {
return fmt.Errorf("list not found")
}
return nil
}
func (r *CRMRepository) DeleteList(id string, tenantID string) error {
query := `DELETE FROM crm_lists WHERE id = $1 AND tenant_id = $2`
result, err := r.db.Exec(query, id, tenantID)
if err != nil {
return err
}
rows, err := result.RowsAffected()
if err != nil {
return err
}
if rows == 0 {
return fmt.Errorf("list not found")
}
return nil
}
// ==================== CUSTOMER <-> LIST ====================
func (r *CRMRepository) AddCustomerToList(customerID, listID, addedBy string) error {
query := `
INSERT INTO crm_customer_lists (customer_id, list_id, added_by)
VALUES ($1, $2, $3)
ON CONFLICT (customer_id, list_id) DO NOTHING
`
_, err := r.db.Exec(query, customerID, listID, addedBy)
return err
}
func (r *CRMRepository) RemoveCustomerFromList(customerID, listID string) error {
query := `DELETE FROM crm_customer_lists WHERE customer_id = $1 AND list_id = $2`
_, err := r.db.Exec(query, customerID, listID)
return err
}
func (r *CRMRepository) GetCustomerLists(customerID string) ([]domain.CRMList, error) {
query := `
SELECT l.id, l.tenant_id, l.name, l.description, l.color, l.created_by,
l.created_at, l.updated_at
FROM crm_lists l
INNER JOIN crm_customer_lists cl ON l.id = cl.list_id
WHERE cl.customer_id = $1
ORDER BY l.name
`
rows, err := r.db.Query(query, customerID)
if err != nil {
return nil, err
}
defer rows.Close()
var lists []domain.CRMList
for rows.Next() {
var l domain.CRMList
err := rows.Scan(
&l.ID, &l.TenantID, &l.Name, &l.Description, &l.Color, &l.CreatedBy,
&l.CreatedAt, &l.UpdatedAt,
)
if err != nil {
return nil, err
}
lists = append(lists, l)
}
return lists, nil
}
func (r *CRMRepository) GetListCustomers(listID string, tenantID string) ([]domain.CRMCustomer, error) {
query := `
SELECT c.id, c.tenant_id, c.name, c.email, c.phone, c.company, c.position,
c.address, c.city, c.state, c.zip_code, c.country, c.notes, c.tags,
c.is_active, c.created_by, c.created_at, c.updated_at,
COALESCE(c.logo_url, '') as logo_url
FROM crm_customers c
INNER JOIN crm_customer_lists cl ON c.id = cl.customer_id
WHERE cl.list_id = $1 AND c.tenant_id = $2 AND c.is_active = true
ORDER BY c.name
`
rows, err := r.db.Query(query, listID, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
var customers []domain.CRMCustomer
for rows.Next() {
var c domain.CRMCustomer
err := rows.Scan(
&c.ID, &c.TenantID, &c.Name, &c.Email, &c.Phone, &c.Company, &c.Position,
&c.Address, &c.City, &c.State, &c.ZipCode, &c.Country, &c.Notes, pq.Array(&c.Tags),
&c.IsActive, &c.CreatedBy, &c.CreatedAt, &c.UpdatedAt, &c.LogoURL,
)
if err != nil {
return nil, err
}
customers = append(customers, c)
}
return customers, nil
}
// ==================== LEADS ====================
func (r *CRMRepository) CreateLead(lead *domain.CRMLead) error {
query := `
INSERT INTO crm_leads (
id, tenant_id, customer_id, funnel_id, stage_id, name, email, phone, source, source_meta,
status, notes, tags, is_active, created_by
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15)
RETURNING created_at, updated_at
`
return r.db.QueryRow(
query,
lead.ID, lead.TenantID, lead.CustomerID, lead.FunnelID, lead.StageID, lead.Name, lead.Email, lead.Phone,
lead.Source, lead.SourceMeta, lead.Status, lead.Notes, pq.Array(lead.Tags),
lead.IsActive, lead.CreatedBy,
).Scan(&lead.CreatedAt, &lead.UpdatedAt)
}
func (r *CRMRepository) AddLeadToList(leadID, listID, addedBy string) error {
query := `
INSERT INTO crm_lead_lists (lead_id, list_id, added_by)
VALUES ($1, $2, $3)
ON CONFLICT (lead_id, list_id) DO NOTHING
`
_, err := r.db.Exec(query, leadID, listID, addedBy)
return err
}
func (r *CRMRepository) BulkAddLeadsToList(leadIDs []string, listID string, addedBy string) error {
tx, err := r.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
stmt, err := tx.Prepare(pq.CopyIn("crm_lead_lists", "lead_id", "list_id", "added_by"))
if err != nil {
return err
}
defer stmt.Close()
for _, leadID := range leadIDs {
_, err = stmt.Exec(leadID, listID, addedBy)
if err != nil {
return err
}
}
_, err = stmt.Exec()
if err != nil {
return err
}
return tx.Commit()
}
func (r *CRMRepository) BulkCreateLeads(leads []domain.CRMLead) error {
tx, err := r.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
query := `
INSERT INTO crm_leads (
id, tenant_id, customer_id, funnel_id, stage_id, name, email, phone, source,
source_meta, status, notes, tags, is_active, created_by
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15
) ON CONFLICT (tenant_id, email) DO UPDATE SET
customer_id = COALESCE(EXCLUDED.customer_id, crm_leads.customer_id),
funnel_id = COALESCE(EXCLUDED.funnel_id, crm_leads.funnel_id),
stage_id = COALESCE(EXCLUDED.stage_id, crm_leads.stage_id),
name = COALESCE(EXCLUDED.name, crm_leads.name),
phone = COALESCE(EXCLUDED.phone, crm_leads.phone),
source = EXCLUDED.source,
source_meta = EXCLUDED.source_meta,
status = EXCLUDED.status,
notes = COALESCE(EXCLUDED.notes, crm_leads.notes),
tags = EXCLUDED.tags,
updated_at = CURRENT_TIMESTAMP
RETURNING id
`
stmt, err := tx.Prepare(query)
if err != nil {
return err
}
defer stmt.Close()
for i := range leads {
var returnedID string
err = stmt.QueryRow(
leads[i].ID, leads[i].TenantID, leads[i].CustomerID, leads[i].FunnelID, leads[i].StageID, leads[i].Name, leads[i].Email, leads[i].Phone,
leads[i].Source, string(leads[i].SourceMeta), leads[i].Status, leads[i].Notes, pq.Array(leads[i].Tags),
leads[i].IsActive, leads[i].CreatedBy,
).Scan(&returnedID)
if err != nil {
return err
}
// Atualiza o ID do lead com o ID retornado (pode ser diferente em caso de conflito)
leads[i].ID = returnedID
}
return tx.Commit()
}
func (r *CRMRepository) GetLeadsByTenant(tenantID string) ([]domain.CRMLead, error) {
query := `
SELECT id, tenant_id, customer_id, funnel_id, stage_id, name, email, phone, source, source_meta,
status, COALESCE(notes, ''), tags, is_active, created_by, created_at, updated_at
FROM crm_leads
WHERE tenant_id = $1 AND is_active = true
ORDER BY created_at DESC
`
rows, err := r.db.Query(query, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
var leads []domain.CRMLead
for rows.Next() {
var l domain.CRMLead
err := rows.Scan(
&l.ID, &l.TenantID, &l.CustomerID, &l.FunnelID, &l.StageID, &l.Name, &l.Email, &l.Phone, &l.Source, &l.SourceMeta,
&l.Status, &l.Notes, pq.Array(&l.Tags), &l.IsActive, &l.CreatedBy, &l.CreatedAt, &l.UpdatedAt,
)
if err != nil {
log.Printf("Error scanning lead: %v", err)
continue
}
leads = append(leads, l)
}
return leads, nil
}
func (r *CRMRepository) GetLeadsWithListsByTenant(tenantID string) ([]domain.CRMLeadWithLists, error) {
leads, err := r.GetLeadsByTenant(tenantID)
if err != nil {
return nil, err
}
var leadsWithLists []domain.CRMLeadWithLists
for _, l := range leads {
lists, err := r.GetListsByLeadID(l.ID)
if err != nil {
lists = []domain.CRMList{}
}
leadsWithLists = append(leadsWithLists, domain.CRMLeadWithLists{
CRMLead: l,
Lists: lists,
})
}
return leadsWithLists, nil
}
func (r *CRMRepository) GetListsByLeadID(leadID string) ([]domain.CRMList, error) {
query := `
SELECT l.id, l.tenant_id, l.customer_id, l.name, l.description, l.color, l.created_by, l.created_at, l.updated_at
FROM crm_lists l
JOIN crm_lead_lists cll ON l.id = cll.list_id
WHERE cll.lead_id = $1
`
rows, err := r.db.Query(query, leadID)
if err != nil {
return nil, err
}
defer rows.Close()
var lists []domain.CRMList
for rows.Next() {
var l domain.CRMList
err := rows.Scan(
&l.ID, &l.TenantID, &l.CustomerID, &l.Name, &l.Description, &l.Color, &l.CreatedBy, &l.CreatedAt, &l.UpdatedAt,
)
if err != nil {
return nil, err
}
lists = append(lists, l)
}
return lists, nil
}
func (r *CRMRepository) GetLeadByID(id string, tenantID string) (*domain.CRMLead, error) {
query := `
SELECT id, tenant_id, customer_id, funnel_id, stage_id, name, email, phone, source, source_meta,
status, COALESCE(notes, ''), tags, is_active, created_by, created_at, updated_at
FROM crm_leads
WHERE id = $1 AND tenant_id = $2
`
var l domain.CRMLead
err := r.db.QueryRow(query, id, tenantID).Scan(
&l.ID, &l.TenantID, &l.CustomerID, &l.FunnelID, &l.StageID, &l.Name, &l.Email, &l.Phone, &l.Source, &l.SourceMeta,
&l.Status, &l.Notes, pq.Array(&l.Tags), &l.IsActive, &l.CreatedBy, &l.CreatedAt, &l.UpdatedAt,
)
if err != nil {
return nil, err
}
return &l, nil
}
func (r *CRMRepository) UpdateLead(lead *domain.CRMLead) error {
query := `
UPDATE crm_leads SET
customer_id = $1,
funnel_id = $2,
stage_id = $3,
name = $4,
email = $5,
phone = $6,
source = $7,
source_meta = $8,
status = $9,
notes = $10,
tags = $11,
is_active = $12
WHERE id = $13 AND tenant_id = $14
`
result, err := r.db.Exec(
query,
lead.CustomerID, lead.FunnelID, lead.StageID, lead.Name, lead.Email, lead.Phone, lead.Source, lead.SourceMeta,
lead.Status, lead.Notes, pq.Array(lead.Tags), lead.IsActive,
lead.ID, lead.TenantID,
)
if err != nil {
return err
}
rows, err := result.RowsAffected()
if err != nil {
return err
}
if rows == 0 {
return fmt.Errorf("lead not found")
}
return nil
}
func (r *CRMRepository) DeleteLead(id string, tenantID string) error {
query := `DELETE FROM crm_leads WHERE id = $1 AND tenant_id = $2`
result, err := r.db.Exec(query, id, tenantID)
if err != nil {
return err
}
rows, err := result.RowsAffected()
if err != nil {
return err
}
if rows == 0 {
return fmt.Errorf("lead not found")
}
return nil
}
func (r *CRMRepository) GetLeadByEmailOrPhone(tenantID, email, phone string) (*domain.CRMLead, error) {
query := `
SELECT id, tenant_id, customer_id, name, email, phone, source, source_meta,
status, COALESCE(notes, ''), tags, is_active, created_by, created_at, updated_at
FROM crm_leads
WHERE tenant_id = $1
AND (
(email IS NOT NULL AND $2 <> '' AND LOWER(email) = LOWER($2))
OR (phone IS NOT NULL AND $3 <> '' AND phone = $3)
)
ORDER BY created_at DESC
LIMIT 1
`
var l domain.CRMLead
err := r.db.QueryRow(query, tenantID, email, phone).Scan(
&l.ID, &l.TenantID, &l.CustomerID, &l.Name, &l.Email, &l.Phone, &l.Source, &l.SourceMeta,
&l.Status, &l.Notes, pq.Array(&l.Tags), &l.IsActive, &l.CreatedBy, &l.CreatedAt, &l.UpdatedAt,
)
if err != nil {
return nil, err
}
return &l, nil
}
func (r *CRMRepository) RemoveLeadFromList(leadID, listID string) error {
query := `DELETE FROM crm_lead_lists WHERE lead_id = $1 AND list_id = $2`
_, err := r.db.Exec(query, leadID, listID)
return err
}
func (r *CRMRepository) GetLeadLists(leadID string) ([]domain.CRMList, error) {
query := `
SELECT l.id, l.tenant_id, l.name, COALESCE(l.description, ''), l.color, l.created_by,
l.created_at, l.updated_at
FROM crm_lists l
INNER JOIN crm_lead_lists ll ON l.id = ll.list_id
WHERE ll.lead_id = $1
ORDER BY l.name
`
rows, err := r.db.Query(query, leadID)
if err != nil {
return nil, err
}
defer rows.Close()
var lists []domain.CRMList
for rows.Next() {
var l domain.CRMList
err := rows.Scan(
&l.ID, &l.TenantID, &l.Name, &l.Description, &l.Color, &l.CreatedBy,
&l.CreatedAt, &l.UpdatedAt,
)
if err != nil {
return nil, err
}
lists = append(lists, l)
}
return lists, nil
}
func (r *CRMRepository) GetListByName(tenantID, name string) (*domain.CRMList, error) {
query := `
SELECT id, tenant_id, name, description, color, created_by, created_at, updated_at
FROM crm_lists
WHERE tenant_id = $1 AND LOWER(name) = LOWER($2)
LIMIT 1
`
var l domain.CRMList
err := r.db.QueryRow(query, tenantID, name).Scan(
&l.ID, &l.TenantID, &l.Name, &l.Description, &l.Color, &l.CreatedBy, &l.CreatedAt, &l.UpdatedAt,
)
if err != nil {
return nil, err
}
return &l, nil
}
// CreateShareToken cria um novo token de compartilhamento
func (r *CRMRepository) CreateShareToken(token *domain.CRMShareToken) error {
query := `
INSERT INTO crm_share_tokens (id, tenant_id, customer_id, token, expires_at, created_by, created_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)
`
_, err := r.db.Exec(query, token.ID, token.TenantID, token.CustomerID, token.Token, token.ExpiresAt, token.CreatedBy, token.CreatedAt)
return err
}
// GetShareTokenByToken busca um token de compartilhamento pelo token
func (r *CRMRepository) GetShareTokenByToken(token string) (*domain.CRMShareToken, error) {
query := `
SELECT id, tenant_id, customer_id, token, expires_at, created_by, created_at
FROM crm_share_tokens
WHERE token = $1
`
var st domain.CRMShareToken
err := r.db.QueryRow(query, token).Scan(
&st.ID, &st.TenantID, &st.CustomerID, &st.Token, &st.ExpiresAt, &st.CreatedBy, &st.CreatedAt,
)
if err != nil {
return nil, err
}
return &st, nil
}
// GetLeadsByCustomerID retorna todos os leads de um cliente específico
func (r *CRMRepository) GetLeadsByCustomerID(customerID string) ([]domain.CRMLead, error) {
query := `
SELECT id, tenant_id, customer_id, name, email, phone, source, source_meta,
status, notes, tags, is_active, created_by, created_at, updated_at
FROM crm_leads
WHERE customer_id = $1 AND is_active = true
ORDER BY created_at DESC
`
rows, err := r.db.Query(query, customerID)
if err != nil {
return nil, err
}
defer rows.Close()
var leads []domain.CRMLead
for rows.Next() {
var l domain.CRMLead
err := rows.Scan(
&l.ID, &l.TenantID, &l.CustomerID, &l.Name, &l.Email, &l.Phone, &l.Source, &l.SourceMeta,
&l.Status, &l.Notes, &l.Tags, &l.IsActive, &l.CreatedBy, &l.CreatedAt, &l.UpdatedAt,
)
if err != nil {
return nil, err
}
leads = append(leads, l)
}
if leads == nil {
leads = []domain.CRMLead{}
}
return leads, nil
}
// GetListsByCustomerID retorna todas as listas que possuem leads de um cliente específico
func (r *CRMRepository) GetListsByCustomerID(customerID string) ([]domain.CRMList, error) {
query := `
SELECT DISTINCT l.id, l.tenant_id, l.name, l.description, l.color, l.created_by,
l.created_at, l.updated_at
FROM crm_lists l
INNER JOIN crm_lead_lists ll ON l.id = ll.list_id
INNER JOIN crm_leads le ON ll.lead_id = le.id
WHERE le.customer_id = $1
ORDER BY l.name
`
rows, err := r.db.Query(query, customerID)
if err != nil {
return nil, err
}
defer rows.Close()
var lists []domain.CRMList
for rows.Next() {
var l domain.CRMList
err := rows.Scan(
&l.ID, &l.TenantID, &l.Name, &l.Description, &l.Color, &l.CreatedBy,
&l.CreatedAt, &l.UpdatedAt,
)
if err != nil {
return nil, err
}
lists = append(lists, l)
}
if lists == nil {
lists = []domain.CRMList{}
}
return lists, nil
}
// GetCustomerByEmail busca um cliente pelo email
func (r *CRMRepository) GetCustomerByEmail(email string) (*domain.CRMCustomer, error) {
query := `
SELECT id, tenant_id, name, email,
COALESCE(phone, '') as phone,
COALESCE(company, '') as company,
COALESCE(position, '') as position,
COALESCE(address, '') as address,
COALESCE(city, '') as city,
COALESCE(state, '') as state,
COALESCE(zip_code, '') as zip_code,
COALESCE(country, '') as country,
COALESCE(notes, '{}') as notes,
COALESCE(tags, '{}') as tags,
is_active,
created_by,
created_at,
updated_at,
COALESCE(password_hash, '') as password_hash,
has_portal_access,
portal_last_login,
portal_created_at
FROM crm_customers
WHERE email = $1 AND is_active = true
`
var c domain.CRMCustomer
var createdBy sql.NullString
err := r.db.QueryRow(query, email).Scan(
&c.ID, &c.TenantID, &c.Name, &c.Email, &c.Phone, &c.Company, &c.Position,
&c.Address, &c.City, &c.State, &c.ZipCode, &c.Country, &c.Notes, pq.Array(&c.Tags),
&c.IsActive, &createdBy, &c.CreatedAt, &c.UpdatedAt,
&c.PasswordHash, &c.HasPortalAccess, &c.PortalLastLogin, &c.PortalCreatedAt,
)
if err != nil {
return nil, err
}
if createdBy.Valid {
c.CreatedBy = createdBy.String
}
return &c, nil
}
// UpdateCustomerLastLogin atualiza o último login do cliente no portal
func (r *CRMRepository) UpdateCustomerLastLogin(customerID string) error {
query := `UPDATE crm_customers SET portal_last_login = NOW() WHERE id = $1`
_, err := r.db.Exec(query, customerID)
return err
}
// SetCustomerPortalAccess define o acesso ao portal e senha para um cliente
func (r *CRMRepository) SetCustomerPortalAccess(customerID, passwordHash string, hasAccess bool) error {
query := `
UPDATE crm_customers
SET password_hash = $1,
has_portal_access = $2,
portal_created_at = CASE
WHEN portal_created_at IS NULL THEN NOW()
ELSE portal_created_at
END
WHERE id = $3
`
_, err := r.db.Exec(query, passwordHash, hasAccess, customerID)
return err
}
// UpdateCustomerPassword atualiza apenas a senha do cliente
func (r *CRMRepository) UpdateCustomerPassword(customerID, passwordHash string) error {
query := `
UPDATE crm_customers
SET password_hash = $1
WHERE id = $2
`
_, err := r.db.Exec(query, passwordHash, customerID)
return err
}
// UpdateCustomerLogo atualiza apenas o logo do cliente
func (r *CRMRepository) UpdateCustomerLogo(customerID, tenantID, logoURL string) error {
query := `
UPDATE crm_customers
SET logo_url = $1
WHERE id = $2 AND tenant_id = $3
`
_, err := r.db.Exec(query, logoURL, customerID, tenantID)
return err
}
// GetCustomerByEmailAndTenant checks if a customer with the given email exists for the tenant
func (r *CRMRepository) GetCustomerByEmailAndTenant(email string, tenantID string) (*domain.CRMCustomer, error) {
query := `
SELECT id, tenant_id, name, email, phone, company, position,
address, city, state, zip_code, country, notes, tags,
is_active, created_by, created_at, updated_at
FROM crm_customers
WHERE LOWER(email) = LOWER($1) AND tenant_id = $2
LIMIT 1
`
var c domain.CRMCustomer
err := r.db.QueryRow(query, email, tenantID).Scan(
&c.ID, &c.TenantID, &c.Name, &c.Email, &c.Phone, &c.Company, &c.Position,
&c.Address, &c.City, &c.State, &c.ZipCode, &c.Country, &c.Notes, pq.Array(&c.Tags),
&c.IsActive, &c.CreatedBy, &c.CreatedAt, &c.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, nil // Not found is not an error
}
if err != nil {
return nil, err
}
return &c, nil
}
// TenantExists checks if a tenant with the given ID exists
func (r *CRMRepository) TenantExists(tenantID string) (bool, error) {
query := `SELECT EXISTS(SELECT 1 FROM tenants WHERE id = $1 AND is_active = true)`
var exists bool
err := r.db.QueryRow(query, tenantID).Scan(&exists)
return exists, err
}
// EnableCustomerPortalAccess habilita o acesso ao portal para um cliente (usado na aprovação)
func (r *CRMRepository) EnableCustomerPortalAccess(customerID string) error {
query := `
UPDATE crm_customers
SET has_portal_access = true,
portal_created_at = COALESCE(portal_created_at, NOW())
WHERE id = $1
`
_, err := r.db.Exec(query, customerID)
return err
}
// GetCustomerByCPF checks if a customer with the given CPF exists for the tenant
func (r *CRMRepository) GetCustomerByCPF(cpf string, tenantID string) (*domain.CRMCustomer, error) {
query := `
SELECT id, tenant_id, name, email, phone, company, position,
address, city, state, zip_code, country, notes, tags,
is_active, created_by, created_at, updated_at
FROM crm_customers
WHERE tenant_id = $1 AND notes LIKE '%"cpf":"' || $2 || '"%'
LIMIT 1
`
var c domain.CRMCustomer
err := r.db.QueryRow(query, tenantID, cpf).Scan(
&c.ID, &c.TenantID, &c.Name, &c.Email, &c.Phone, &c.Company, &c.Position,
&c.Address, &c.City, &c.State, &c.ZipCode, &c.Country, &c.Notes, pq.Array(&c.Tags),
&c.IsActive, &c.CreatedBy, &c.CreatedAt, &c.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return &c, nil
}
// GetCustomerByCNPJ checks if a customer with the given CNPJ exists for the tenant
func (r *CRMRepository) GetCustomerByCNPJ(cnpj string, tenantID string) (*domain.CRMCustomer, error) {
query := `
SELECT id, tenant_id, name, email, phone, company, position,
address, city, state, zip_code, country, notes, tags,
is_active, created_by, created_at, updated_at
FROM crm_customers
WHERE tenant_id = $1 AND notes LIKE '%"cnpj":"' || $2 || '"%'
LIMIT 1
`
var c domain.CRMCustomer
err := r.db.QueryRow(query, tenantID, cnpj).Scan(
&c.ID, &c.TenantID, &c.Name, &c.Email, &c.Phone, &c.Company, &c.Position,
&c.Address, &c.City, &c.State, &c.ZipCode, &c.Country, &c.Notes, pq.Array(&c.Tags),
&c.IsActive, &c.CreatedBy, &c.CreatedAt, &c.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return &c, nil
}
func (r *CRMRepository) GetLeadsByListID(listID string) ([]domain.CRMLead, error) {
query := `
SELECT l.id, l.tenant_id, l.customer_id, l.funnel_id, l.stage_id, l.name, l.email, l.phone,
l.source, l.source_meta, l.status, COALESCE(l.notes, ''), l.tags,
l.is_active, COALESCE(l.created_by::text, '') as created_by, l.created_at, l.updated_at
FROM crm_leads l
INNER JOIN crm_lead_lists ll ON l.id = ll.lead_id
WHERE ll.list_id = $1
ORDER BY l.created_at DESC
`
rows, err := r.db.Query(query, listID)
if err != nil {
return nil, err
}
defer rows.Close()
var leads []domain.CRMLead
for rows.Next() {
var l domain.CRMLead
var sourceMeta []byte
err := rows.Scan(
&l.ID, &l.TenantID, &l.CustomerID, &l.FunnelID, &l.StageID, &l.Name, &l.Email, &l.Phone,
&l.Source, &sourceMeta, &l.Status, &l.Notes, pq.Array(&l.Tags),
&l.IsActive, &l.CreatedBy, &l.CreatedAt, &l.UpdatedAt,
)
if err != nil {
log.Printf("Error scanning lead from list: %v", err)
continue
}
if sourceMeta != nil {
l.SourceMeta = sourceMeta
}
leads = append(leads, l)
}
return leads, nil
}
// ==================== FUNNELS & STAGES ====================
func (r *CRMRepository) CreateFunnel(funnel *domain.CRMFunnel) error {
query := `
INSERT INTO crm_funnels (id, tenant_id, name, description, is_default)
VALUES ($1, $2, $3, $4, $5)
RETURNING created_at, updated_at
`
return r.db.QueryRow(query, funnel.ID, funnel.TenantID, funnel.Name, funnel.Description, funnel.IsDefault).
Scan(&funnel.CreatedAt, &funnel.UpdatedAt)
}
func (r *CRMRepository) GetFunnelsByTenant(tenantID string) ([]domain.CRMFunnel, error) {
query := `SELECT id, tenant_id, name, COALESCE(description, ''), is_default, created_at, updated_at FROM crm_funnels WHERE tenant_id = $1 ORDER BY created_at ASC`
rows, err := r.db.Query(query, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
var funnels []domain.CRMFunnel
for rows.Next() {
var f domain.CRMFunnel
if err := rows.Scan(&f.ID, &f.TenantID, &f.Name, &f.Description, &f.IsDefault, &f.CreatedAt, &f.UpdatedAt); err != nil {
return nil, err
}
funnels = append(funnels, f)
}
return funnels, nil
}
func (r *CRMRepository) GetFunnelByID(id, tenantID string) (*domain.CRMFunnel, error) {
query := `SELECT id, tenant_id, name, COALESCE(description, ''), is_default, created_at, updated_at FROM crm_funnels WHERE id = $1 AND tenant_id = $2`
var f domain.CRMFunnel
err := r.db.QueryRow(query, id, tenantID).Scan(&f.ID, &f.TenantID, &f.Name, &f.Description, &f.IsDefault, &f.CreatedAt, &f.UpdatedAt)
if err != nil {
return nil, err
}
return &f, nil
}
func (r *CRMRepository) UpdateFunnel(funnel *domain.CRMFunnel) error {
query := `UPDATE crm_funnels SET name = $1, description = $2, is_default = $3, updated_at = CURRENT_TIMESTAMP WHERE id = $4 AND tenant_id = $5`
_, err := r.db.Exec(query, funnel.Name, funnel.Description, funnel.IsDefault, funnel.ID, funnel.TenantID)
return err
}
func (r *CRMRepository) DeleteFunnel(id, tenantID string) error {
query := `DELETE FROM crm_funnels WHERE id = $1 AND tenant_id = $2`
_, err := r.db.Exec(query, id, tenantID)
return err
}
func (r *CRMRepository) CreateFunnelStage(stage *domain.CRMFunnelStage) error {
query := `
INSERT INTO crm_funnel_stages (id, funnel_id, name, description, color, order_index)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING created_at, updated_at
`
return r.db.QueryRow(query, stage.ID, stage.FunnelID, stage.Name, stage.Description, stage.Color, stage.OrderIndex).
Scan(&stage.CreatedAt, &stage.UpdatedAt)
}
func (r *CRMRepository) GetStagesByFunnelID(funnelID string) ([]domain.CRMFunnelStage, error) {
query := `SELECT id, funnel_id, name, COALESCE(description, ''), color, order_index, created_at, updated_at FROM crm_funnel_stages WHERE funnel_id = $1 ORDER BY order_index ASC`
rows, err := r.db.Query(query, funnelID)
if err != nil {
return nil, err
}
defer rows.Close()
var stages []domain.CRMFunnelStage
for rows.Next() {
var s domain.CRMFunnelStage
if err := rows.Scan(&s.ID, &s.FunnelID, &s.Name, &s.Description, &s.Color, &s.OrderIndex, &s.CreatedAt, &s.UpdatedAt); err != nil {
return nil, err
}
stages = append(stages, s)
}
return stages, nil
}
func (r *CRMRepository) UpdateFunnelStage(stage *domain.CRMFunnelStage) error {
query := `UPDATE crm_funnel_stages SET name = $1, description = $2, color = $3, order_index = $4, updated_at = CURRENT_TIMESTAMP WHERE id = $5`
_, err := r.db.Exec(query, stage.Name, stage.Description, stage.Color, stage.OrderIndex, stage.ID)
return err
}
func (r *CRMRepository) DeleteFunnelStage(id string) error {
query := `DELETE FROM crm_funnel_stages WHERE id = $1`
_, err := r.db.Exec(query, id)
return err
}
func (r *CRMRepository) UpdateLeadStage(leadID, tenantID, funnelID, stageID string) error {
query := `UPDATE crm_leads SET funnel_id = $1, stage_id = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $3 AND tenant_id = $4`
_, err := r.db.Exec(query, funnelID, stageID, leadID, tenantID)
return err
}
func (r *CRMRepository) EnsureDefaultFunnel(tenantID string) (string, error) {
// Check if tenant already has a funnel
var funnelID string
query := `SELECT id FROM crm_funnels WHERE tenant_id = $1 LIMIT 1`
err := r.db.QueryRow(query, tenantID).Scan(&funnelID)
if err == nil {
return funnelID, nil
}
// If not, create default using the function we defined in migration
query = `SELECT create_default_crm_funnel($1)`
err = r.db.QueryRow(query, tenantID).Scan(&funnelID)
return funnelID, err
}