feat: versão 1.5 - CRM Beta com leads, funis, campanhas e portal do cliente
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"aggios-app/backend/internal/domain"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
@@ -23,25 +24,34 @@ func (r *CRMRepository) CreateCustomer(customer *domain.CRMCustomer) error {
|
||||
INSERT INTO crm_customers (
|
||||
id, tenant_id, name, email, phone, company, position,
|
||||
address, city, state, zip_code, country, notes, tags,
|
||||
is_active, created_by
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
||||
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, customer.CreatedBy,
|
||||
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, created_by, created_at, updated_at
|
||||
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
|
||||
@@ -59,7 +69,7 @@ func (r *CRMRepository) GetCustomersByTenant(tenantID string) ([]domain.CRMCusto
|
||||
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.IsActive, &c.CreatedBy, &c.CreatedAt, &c.UpdatedAt, &c.LogoURL,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -73,8 +83,9 @@ func (r *CRMRepository) GetCustomersByTenant(tenantID string) ([]domain.CRMCusto
|
||||
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, created_by, created_at, updated_at
|
||||
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
|
||||
`
|
||||
@@ -83,7 +94,7 @@ func (r *CRMRepository) GetCustomerByID(id string, tenantID string) (*domain.CRM
|
||||
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.IsActive, &c.CreatedBy, &c.CreatedAt, &c.UpdatedAt, &c.LogoURL,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
@@ -98,15 +109,15 @@ func (r *CRMRepository) UpdateCustomer(customer *domain.CRMCustomer) error {
|
||||
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
|
||||
WHERE id = $14 AND tenant_id = $15
|
||||
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.Notes, pq.Array(customer.Tags), customer.IsActive, customer.LogoURL,
|
||||
customer.ID, customer.TenantID,
|
||||
)
|
||||
|
||||
@@ -150,26 +161,27 @@ func (r *CRMRepository) DeleteCustomer(id string, tenantID string) error {
|
||||
|
||||
func (r *CRMRepository) CreateList(list *domain.CRMList) error {
|
||||
query := `
|
||||
INSERT INTO crm_lists (id, tenant_id, name, description, color, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
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.Name, list.Description, list.Color, list.CreatedBy,
|
||||
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.name, l.description, l.color, l.created_by,
|
||||
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,
|
||||
COUNT(cl.customer_id) as customer_count
|
||||
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_customer_lists cl ON l.id = cl.list_id
|
||||
LEFT JOIN crm_customers c ON l.customer_id = c.id
|
||||
WHERE l.tenant_id = $1
|
||||
GROUP BY l.id
|
||||
ORDER BY l.created_at DESC
|
||||
`
|
||||
|
||||
@@ -183,8 +195,8 @@ func (r *CRMRepository) GetListsByTenant(tenantID string) ([]domain.CRMListWithC
|
||||
for rows.Next() {
|
||||
var l domain.CRMListWithCustomers
|
||||
err := rows.Scan(
|
||||
&l.ID, &l.TenantID, &l.Name, &l.Description, &l.Color, &l.CreatedBy,
|
||||
&l.CreatedAt, &l.UpdatedAt, &l.CustomerCount,
|
||||
&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
|
||||
@@ -197,14 +209,14 @@ func (r *CRMRepository) GetListsByTenant(tenantID string) ([]domain.CRMListWithC
|
||||
|
||||
func (r *CRMRepository) GetListByID(id string, tenantID string) (*domain.CRMList, error) {
|
||||
query := `
|
||||
SELECT id, tenant_id, name, description, color, created_by, created_at, updated_at
|
||||
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.Name, &l.Description, &l.Color, &l.CreatedBy,
|
||||
&l.ID, &l.TenantID, &l.CustomerID, &l.FunnelID, &l.Name, &l.Description, &l.Color, &l.CreatedBy,
|
||||
&l.CreatedAt, &l.UpdatedAt,
|
||||
)
|
||||
|
||||
@@ -218,11 +230,11 @@ func (r *CRMRepository) GetListByID(id string, tenantID string) (*domain.CRMList
|
||||
func (r *CRMRepository) UpdateList(list *domain.CRMList) error {
|
||||
query := `
|
||||
UPDATE crm_lists SET
|
||||
name = $1, description = $2, color = $3
|
||||
WHERE id = $4 AND tenant_id = $5
|
||||
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.ID, list.TenantID)
|
||||
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
|
||||
}
|
||||
@@ -315,7 +327,8 @@ func (r *CRMRepository) GetListCustomers(listID string, tenantID string) ([]doma
|
||||
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
|
||||
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
|
||||
@@ -334,7 +347,7 @@ func (r *CRMRepository) GetListCustomers(listID string, tenantID string) ([]doma
|
||||
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.IsActive, &c.CreatedBy, &c.CreatedAt, &c.UpdatedAt, &c.LogoURL,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -344,3 +357,803 @@ func (r *CRMRepository) GetListCustomers(listID string, tenantID string) ([]doma
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user