1160 lines
33 KiB
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
|
|
}
|
|
|
|
|