Appearance
Módulo CRM / Inteligência
O módulo CRM é responsável por toda a inteligência analítica do sistema: segmentação de clientes com filtros compostos, análise RFM (Recência, Frequência, Monetário), avaliação de qualidade de dados de cadastro e pesquisas de satisfação pós-atendimento. Ele consome dados sincronizados pelo módulo ERP e expõe dashboards acionáveis para equipes de marketing, vendas e operações.
Services
IntelligenceService
Arquivo: backend/src/modules/crm/intelligence/intelligence.service.ts
Orquestra segmentação de clientes, preview de audiência, CRUD de segmentos e o snapshot diário. Delega análise RFM para RfmAnalysisService e exportação CSV para DataExportService. Mantém um cache in-process de opções de filtro por organização.
Cache interno
| Campo | TTL | Chave | Invalidação |
|---|---|---|---|
filterCache (Map em memória) | 5 minutos | organizationId | Expiração por timestamp — recalculado na próxima requisição após expirar |
Rastreamento de estado
recalculatingSegments (Set em memória) — contém IDs de segmentos cujo lastCount está sendo recalculado em background. Exposto no campo isCountRecalculating da resposta de listagem.
Métodos
getRfmAnalysis(): Promise<any>
Delega para RfmAnalysisService.getAnalysis() e retorna a análise RFM atual da organização. Não possui lógica própria.
calculatePreview(rules: any[]): Promise<{ metrics: any }>
Calcula uma prévia da audiência de um segmento sem salvar nada no banco.
Parâmetros:
rules— array de blocos de filtros no formato interno de segmentação.
Retorno: objeto { metrics } com:
total— contagem de clientes que batem com as regraspercent— percentual do total da basechannels— quantidade com e-mail e com WhatsApp habilitadoavgTicket— ticket médio dos clientes do segmentorfmBreakdown— distribuição por status RFM
Flow:
- Chama
queryBuilder.buildAsync(rules)para converter as regras em cláusulaWHEREdo Prisma (fora de transação — pode fazer lookups de IDs de produto de forma leve). - Abre uma transação Prisma com
timeout: 15_000ms. - Dentro da transação, executa
SET LOCAL statement_timeout = '12000'(12s de timeout no banco). - Em paralelo (
Promise.all) executa:customer.count()com filtro do segmentocustomer.count()total sem filtrocustomer.count()com filtro +email: { not: null }customer.count()com filtro +phone: { not: null }+phoneType: 'MOBILE'+whatsappOptOut: falsecustomer.groupBy(['rfmStatus'])com filtrocustomer.aggregate()com somas detotalSpenteorderCount
- Calcula
avgTicket = totalSpent / totalOrders. - Monta e retorna o objeto
metrics.
Tratamento de timeout: Captura erros com código Prisma P2024, mensagens statement timeout, canceling statement ou timed out. Nesses casos retorna { metrics: { ..., timedOut: true } } sem lançar exceção — a requisição HTTP conclui normalmente com flag de timeout.
Tabelas lidas: Customer (count, groupBy, aggregate)
handleDailySegmentSnapshot(): Promise<void> (CRON)
Schedule: EVERY_DAY_AT_3AM — executa às 3h AM todos os dias.
Registra a contagem atual de cada segmento ativo no histórico.
Flow:
- Busca todos os segmentos com
active: trueda tabelaSegment. - Para cada segmento: a. Chama
queryBuilder.buildAsync(seg.rules)para obter a cláusula WHERE. b. Executacustomer.count({ where: whereClause }). c. Cria registro emSegmentHistorycom{ segmentId, count, date }. d. AtualizaSegment.lastCountcom o valor atual. e. Em caso de erro em um segmento específico, loga o erro e continua para o próximo (não propaga a exceção).
Tabelas lidas: SegmentTabelas escritas: SegmentHistory, Segment
findAllSegmentsWithTrend(): Promise<{ segments: any[], totalCustomers: number }>
Lista todos os segmentos com suas métricas de tendência (crescimento/queda em relação ao snapshot anterior).
Flow:
- Conta o total de clientes na tabela
Customer. - Busca todos os segmentos ordenados por
updatedAt desc, incluindo:- Dados do usuário que atualizou (
updatedBy) - Segundo snapshot mais recente do histórico (
history:skip: 1, take: 1para pegar o penúltimo, pois o último é olastCount)
- Dados do usuário que atualizou (
- Para cada segmento calcula:
trendPercent = ((currentCount - previousCount) / previousCount) * 100reachPercent = (currentCount / totalCustomers) * 100isCountRecalculating— lido do Set in-process
- Retorna lista formatada com métricas.
Tabelas lidas: Customer (count), Segment (com include de SegmentHistory e User)
createSegment(data, userId?): Promise<Segment>
Cria um novo segmento de clientes.
Parâmetros:
data.name— nome do segmentodata.rules— array de blocos de filtros (JSON)data.isDynamic— se o segmento atualiza automaticamentedata.lastCount— contagem inicial (opcional, padrão0)userId— ID do usuário criador (opcional)
Flow:
- Busca a primeira loja (
store.findFirst()) para obterorganizationId. LançaConflictExceptionse não houver loja configurada. - Verifica se já existe segmento com o mesmo nome (case-insensitive) na organização. Lança
ConflictExceptionse existir. - Cria o registro em
Segmentcomlogic: 'custom',active: true. - Chama
recalculateSegmentCountAsync(created.id, rules)de forma não bloqueante. - Retorna o segmento criado.
Tabelas lidas: Store, SegmentTabelas escritas: Segment
updateSegment(id, data, userId?): Promise<Segment>
Atualiza nome, regras e configuração de um segmento existente.
Flow:
- Verifica conflito de nome com outros segmentos (exclui o próprio ID). Lança
ConflictExceptionse encontrar duplicata. - Atualiza o registro em
Segment. Só incluilastCountnoupdatesedata.lastCount !== undefined. - Chama
recalculateSegmentCountAsync(id, rules)de forma não bloqueante. - Retorna o segmento atualizado.
Tabelas lidas: SegmentTabelas escritas: Segment
recalculateSegmentCountAsync(segmentId, rules): void (privado)
Recalcula lastCount em background sem bloquear a resposta HTTP.
Flow:
- Adiciona
segmentIdao SetrecalculatingSegments. - Executa em uma Promise não awaited: a.
queryBuilder.buildAsync(rules)para obter o WHERE. b. Transação Prisma comtimeout: 65_000ms eSET LOCAL statement_timeout = '60000'. c.customer.count()com o WHERE. d.segment.update()com o novolastCount. - Em caso de falha, loga warning sem propagar.
- No
finally, removesegmentIddo Set.
Nota de herança de contexto: A Promise herda o AsyncLocalStorage do request (tenant context), portanto o banco correto é consultado mesmo em background.
getFilterOptions(): Promise<object>
Retorna todas as opções de filtro para popular selects na interface de segmentação.
Cache: Resultado armazenado em filterCache (Map in-process) por organizationId. TTL de 5 minutos. Retorna diretamente do cache se ainda válido.
Quando o cache expira, executa 3 rodadas de queries em paralelo (4 por rodada para não esgotar o pool de conexões do Prisma, padrão de 5 conexões):
Rodada 1 (4 queries em paralelo):
customer.findMany→ cidades distintas (não nulas)customer.findMany→ estados distintos (não nulos)segment.findMany→ segmentos ativos (id, name)store.findMany→ lojas ativas (excluindo código iniciado porDESAT)
Rodada 2 (4 queries em paralelo):
seller.findMany→ vendedores com statusACTIVEproduct.findMany→ coleções distintasproduct.findMany→ departamentos distintosproduct.findMany→ marcas distintas
Rodada 3 (4 queries em paralelo):
product.findMany→ tipos distintosproduct.findMany→ grupos distintosproduct.findMany→ divisões distintasproduct.findMany→ categorias distintas
Rodada 4 (4 queries em paralelo):
$queryRaw→SELECT DISTINCT unnest(colors)da tabelaProduct$queryRaw→SELECT DISTINCT unnest(sizes)da tabelaProductproduct.findMany→ nomes de produtos ativosproduct.findMany→ códigos de produto (com labelcódigo — nome)
Rodada 5 (3 queries em paralelo):
transaction.findMany→ nomes de fornecedor distintos (vendorName)campaign.findMany→ campanhas não-drafttransaction.findMany→ canais distintos
Tabelas lidas: Customer, Segment, Store, Seller, Product, Transaction, Campaign
getSegmentById(id: string): Promise<Segment & { isCountRecalculating: boolean }>
Busca um segmento pelo ID. Lança Error com mensagem 'Segmento não encontrado' se não existir. Inclui o flag isCountRecalculating.
toggleSegmentStatus(id: string): Promise<Segment>
Inverte o campo active do segmento. Lança Error se não existir.
deleteSegment(id: string): Promise<Segment>
Remove o segmento permanentemente (segment.delete).
updateCustomerMetrics(customerId: string): Promise<any>
Delega para RfmAnalysisService.updateCustomerMetrics(customerId).
exportSegmentToCsv(segmentId: string): Promise<string>
Delega para DataExportService.exportSegmentToCsv(segmentId). Retorna o CSV como string (e-mails e telefones mascarados).
RfmService
Arquivo: backend/src/modules/crm/rfm/rfm.service.ts
Calcula e persiste segmentos RFM para todos os clientes da organização. Usa CTEs PostgreSQL puras para classificar clientes em 10 segmentos com base em transações históricas em relação a uma data de referência.
Segmentos RFM e thresholds
| Segmento | Condição |
|---|---|
Champions | Última compra < 30 dias E (gasto total > R$1.000 OU pedidos > 5) |
Loyal Customers | Última compra < 90 dias E pedidos > 5 |
Potential Loyalist | Última compra < 60 dias E pedidos entre 2 e 4 |
Recent Customers | Última compra < 30 dias (sem critério de valor) |
Promising | Última compra entre 30 e 90 dias E pedidos entre 2 e 4 |
Need Attention | Última compra entre 90 e 120 dias E pedidos > 2 |
Can't Lose Them | Última compra > 180 dias E (gasto > R$1.000 OU pedidos > 5) |
At Risk | Última compra entre 120 e 180 dias |
About To Sleep | Última compra entre 90 e 150 dias |
Hibernating | Sem transações OU nenhum outro critério satisfeito |
A avaliação de condições segue ordem de prioridade: Champions é verificado primeiro, Hibernating é o fallback.
Função auxiliar buildSegmentCte(organizationId, periodEnd, prefix)
Constrói dois CTEs PostgreSQL nomeados:
{prefix}_agg— agrega por cliente:last_order(MAX data de transação paga),order_count(COUNT),total_spent(SUM). Considera apenas transações comstatus = 'PAID',totalValue > 0edate < periodEnd. Clientes sem transações têmlast_order = createdAt.{prefix}_classified— estendeaggcom colunadays_since(dias desde a última ordem em relação aperiodEnd) e colunasegment(resultado da classificação RFM por CASE WHEN).
Valida o organizationId com regex UUID antes de interpolar na query para prevenir SQL injection.
Métodos
handleDailyRecompute(): Promise<void> (CRON)
Schedule: 0 7 * * * — executa às 7h AM todos os dias.
Flow:
- Verifica
DISABLE_SYNC_CRON === 'true'e retorna sem fazer nada se verdadeiro. - Busca todas as organizações do banco (
organization.findMany). - Para cada organização, chama
recomputeAll(org.id, new Date()). - Loga o número de clientes atualizados. Em caso de erro em uma organização, loga e continua para a próxima.
Tabelas lidas: OrganizationTabelas escritas: Customer (via recomputeAll)
getOverviewMetrics(organizationId, referencePeriod): Promise<object>
Retorna KPIs e perfis de segmento para o mês de referência fornecido.
Parâmetros:
organizationId— UUID da organizaçãoreferencePeriod— qualquer data dentro do mês de referência
Lógica de datas:
periodStart= 1º dia do mês às 3h UTC (equivale a meia-noite BRT)periodEnd= 1º dia do mês seguinte às 3h UTC (fim do mês em BRT)
Flow:
- Conta clientes criados antes de
periodEnd(customer.count). - Em paralelo, executa duas queries parametrizadas:
newRows— clientes com compra entreperiodStarteperiodEndque NÃO tinham compra anterior aperiodStart(compradores de primeira vez).recurringRows— clientes com compra no período que JÁ tinham compra anterior.
- Executa a CTE
buildSegmentCte(organizationId, periodEnd)sem prefixo, fazendoGROUP BY segmentpara obter métricas por segmento:COUNT,AVG(total_spent),AVG(order_count),SUM(total_spent),SUM(order_count),AVG(days_since). - Constrói array
profilescom todos os 10 segmentos (usando zeros para segmentos ausentes no resultado). - Calcula
summarycom totais ponderados.
Retorno:
json
{
"kpis": { "total": 0, "new": 0, "recurring": 0 },
"profiles": [{ "segment": "...", "count": 0, "avgSpent": 0, ... }],
"summary": { "count": 0, "totalRevenue": 0, "totalOrders": 0, "avgRecency": 0, "avgOrders": 0, "avgSpent": 0 }
}Tabelas lidas: Customer, Transaction
recomputeAll(organizationId, periodEnd?): Promise<{ updated: number, segments: any[] }>
Persiste o segmento RFM computado de volta ao campo Customer.rfmStatus.
Flow:
- Executa
UPDATE "Customer" c SET "previousRfmStatus" = c."rfmStatus", "rfmStatus" = cl.segment, "lastOrder" = cl.last_order, "orderCount" = cl.order_count, "totalSpent" = cl.total_spent FROM classified cl WHERE c.id = cl.id AND c."organizationId" = '...'— usa a CTE completa dobuildSegmentCte. - Faz
customer.groupBy(['rfmStatus'])para contar por segmento e retornar o total de clientes atualizados.
Tabelas lidas: Customer, TransactionTabelas escritas: Customer (rfmStatus, previousRfmStatus, lastOrder, orderCount, totalSpent)
getMigrationsMatrix(organizationId, currentPeriod, previousPeriod): Promise<object>
Calcula a matriz de migração entre segmentos RFM de dois períodos.
Flow:
- Calcula
currStart,currEndeprevEndcomo limites de mês às 3h UTC. - Inicializa matriz 10×10 zerada.
- Executa query com dois CTEs:
prev_classified(snapshot emprevEnd) ecurr_classified(snapshot emcurrEnd). FazJOINnos dois porcustomer.ide conta pares(prev_segment, curr_segment). - Popula
matrixMap[prevSegment][currSegment]com os contadores. - Calcula
positiveMigration— clientes que saíram de segmento negativo (Need Attention, About To Sleep, At Risk, Can't Lose Them, Hibernating) para positivo (Champions, Loyal Customers, Potential Loyalist, Promising, Recent Customers). - Conta clientes novos: registrados entre
currStartecurrEnd. - Em paralelo, busca distribuição de segmentos em
currEndeprevEndpara o gráfico de comparação.
Retorno: { kpis: { totalMigrated, positiveMigration, newCustomers }, chartData, matrix }
Tabelas lidas: Customer, Transaction
QualityService
Arquivo: backend/src/modules/crm/quality/quality.service.ts
Avalia a completude e validade dos dados de cadastro nas transações: CPF, e-mail, telefone e data de nascimento. Todas as validações são aplicadas via SQL (regex e extração de ano) para consistência entre os endpoints de sumário, breakdown por loja, ranking de vendedores e listagem paginada.
Funções utilitárias internas
dateRange(startDate, endDate) — Converte strings YYYY-MM-DD para objetos Date com bounds de início (00:00:00.000) e fim (23:59:59.999) em horário de Brasília (UTC-3 → UTC). Aceita também objetos Date diretamente.
scoreTransaction(customer) — Avalia um registro de cliente retornando { complete, cpf, email, phone }:
cpf—cpfHash IS NOT NULLemail—isValidEmail(customer.email)(regex)phone—isValidPhone(customer.phone)(DDD + dígitos)complete— todos os três verdadeiros
Regras de validação (SQL)
| Campo | Regra |
|---|---|
c.email ~ '^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$' | |
| Telefone | `regexp_replace(c.phone, '\D', '', 'g') ~ '^(1[1-9] |
| Data de nascimento | EXTRACT(YEAR FROM c."birthDate") < 1900 OR > EXTRACT(YEAR FROM NOW()) - 14 — qualquer um desses invalida |
| CPF | c."cpfHash" IS NULL — ausência do hash indica CPF ausente |
Métodos
getSummary(startDate, endDate, storeIds?): Promise<object>
Retorna sumário global de completude de dados no período.
Parâmetros:
startDate,endDate— stringsYYYY-MM-DDou objetos DatestoreIds— array de IDs de loja para filtrar (opcional); IDs são expandidos viaStoreAliasResolver.expandStoreIds()para incluir lojas alias
Flow:
- Converte datas com
dateRange(). - Expande
storeIdsse fornecidos. - Executa query
$queryRawna tabelaTransaction JOIN Customercom filtros de período e loja. - Usa
COUNT(*) FILTER (WHERE ...)do PostgreSQL para calcular em uma única passagem:total,complete,cpf_missing,email_missing,phone_missing,birthdate_invalid.
Retorno:
json
{
"totalTransactions": 0,
"completeTransactions": 0,
"incompleteTransactions": 0,
"errors": { "cpf": 0, "email": 0, "phone": 0, "birthdate": 0 }
}Tabelas lidas: Transaction, Customer
getStorePerformance(startDate, endDate, storeIds?): Promise<object[]>
Retorna breakdown de completude por loja.
Flow:
- Similar ao
getSummary, mas fazLEFT JOIN "Store"e agrupa pors.id, s.code, s."tradeName", s.name. - Filtra apenas lojas com
s."isActive" = true. - Retorna array com uma entrada por loja, cada uma contendo os mesmos campos de qualidade do sumário.
Tabelas lidas: Store, Transaction, Customer
getTransactions(params): Promise<object>
Retorna lista paginada de transações com flags de validade de dados por campo.
Parâmetros:
startDate,endDate— períodosellerId— filtro por vendedor (opcional)storeId— filtro por loja (opcional)missingField—'cpf' | 'email' | 'phone' | 'birthdate'(opcional) — filtra somente transações com aquele campo inválidopage— número da página (página começa em 1)
Tamanho de página: 50 registros.
Flow:
- Monta
filteredWherepara otransaction.findMany(filtro loose: para CPF/email/phone usacustomer.fieldX: null; para birthdate usacustomer.birthDate: null). - Em paralelo (
Promise.all) executa:- Query SQL de contagem de chips (
cpf_missing,email_missing,phone_missing,birthdate_missing) sobre todo o período/loja/vendedor. - Query SQL de
filteredTotal— total de registros que passam pelos filtros demissingFieldcom validação exata. transaction.findManycom paginação, include decustomer(id, name, cpfHash, cpfMasked, email, phone, birthDate) estore(name, tradeName).
- Query SQL de contagem de chips (
- Mapeia cada transação chamando
scoreTransaction()e calculavalidBirthdate(ano entre 1900 eanoAtual - 14). - Retorna
{ data, total, page, pageSize, counts }.
Tabelas lidas: Transaction, Customer, Store
getSellerPerformance(startDate, endDate, storeIds?): Promise<object[]>
Retorna ranking de vendedores por percentual de transações com dados incompletos.
Flow:
- Busca todas as transações do período com
salespersonId: { not: null }, incluindo dados do cliente. - Agrupa em memória (
Map<salespersonId, accumulador>) calculando:total,complete,errCpf,errEmail,errPhone,errBirthdate. - Separa as chaves em UUIDs válidos (busca por
seller.id) e não-UUIDs (busca porseller.externalId). - Busca os vendedores com
OR [{ id: { in: uuidKeys } }, { externalId: { in: nonUuidKeys } }]. - Constrói mapa de vendedor por ambas as chaves para lookup eficiente.
- Retorna array ordenado por
incompleteTransactions DESC.
Tabelas lidas: Transaction, Customer, Seller
SurveyService
Arquivo: backend/src/modules/crm/survey/survey.service.ts
Gerencia pesquisas de satisfação pós-atendimento (1–5 estrelas). Responsável por geração e validação de tokens HMAC, criação de URLs de pesquisa com expiração de 7 dias, submissão idempotente de respostas e dashboard de resultados.
Segurança do token
- Algoritmo: HMAC-SHA256
- Chave: variável de ambiente
SURVEY_SECRET(fallback para'survey-default-secret'em desenvolvimento) - Payload: string
"${conversationId}:${exp}"ondeexpé timestamp Unix em ms de expiração - Idempotência: o token é hashed com SHA-256 antes de ser armazenado em
SurveyResponse.tokenHash— submissão duplicada do mesmo token retorna silenciosamente sem criar novo registro
Métodos
generateToken(conversationId: number, exp: number): string
Gera o token HMAC-SHA256 de "${conversationId}:${exp}". Retorna hex string.
buildSurveyUrl(params): string
Constrói a URL completa da pesquisa com todos os parâmetros.
Parâmetros:
conversationId— ID da conversa no sistema de atendimentoorganizationId— UUID da organizaçãocustomerName,sellerName,storeName— dados para exibição na pesquisastoreId— ID interno da loja (opcional, incluído comosid)customerPhone— telefone do cliente (apenas últimos 4 dígitos incluídos comoph)
Flow:
- Calcula
exp = Date.now() + 7 dias em ms. - Chama
generateToken(conversationId, exp). - Constrói
URLSearchParamscom:cid,org,exp,t(token),name,seller,store,sid?,ph?. - Concatena
base(variávelSURVEY_BASE_URL, padrãohttps://galdix.com.br) +/pesquisa?+ querystring.
getResults(organizationId, days?, recentLimit?): Promise<object>
Retorna dashboard de resultados das pesquisas.
Parâmetros:
organizationId— UUID da organizaçãodays— janela de tempo em dias (padrão 30)recentLimit— quantidade de respostas recentes a detalhar (padrão 50, máx 500)
Flow:
- Busca todas as
SurveyResponseda organização nos últimosdaysdias, ordenadas porsubmittedAt desc. - Calcula
avgRatingcom precisão de 1 decimal. - Calcula
distribution— array de 5 elementos (rating 1 a 5) com contagem de cada um. - Agrupa em memória por
storeNameesellerNamepara calcular médias por loja e vendedor. - Para as
recentLimitrespostas mais recentes: a. Extrai telefones únicos. b. Busca clientes no CRM pelo telefone (com e sem prefixo+). c. Constrói mapaphone → { id, name }. d. Para cada resposta, substitui o nome pelo nome atual no CRM se encontrar o cliente. - Retorna
{ total, avgRating, distribution, byStore, bySeller, recent }.
Tabelas lidas: SurveyResponse, Customer
getWindowExpiryResults(organizationId, days?): Promise<object>
Retorna contagem de janelas de atendimento expiradas sem pesquisa respondida.
Flow:
- Busca
WindowExpiryda organização nos últimosdaysdias, ordenados porresolvedAt desc. - Agrupa por
storeNamee por dia (convertendo para horário de Brasília UTC-3 para o agrupamento diário). - Retorna
{ total, byStore: [...], byDay: [...] }.
Tabelas lidas: WindowExpiry
submit(dto: SubmitSurveyDto): Promise<void>
Registra resposta de pesquisa com validação completa do token.
Parâmetros do DTO:
cid— conversationIdexp— timestamp de expiraçãotoken— HMAC recebidoorganizationId— UUID da organizaçãorating— nota de 1 a 5comment,name,seller,store,phone,storeId— opcionais
Flow:
- Valida
ratingentre 1 e 5. LançaBadRequestExceptionse inválido. - Valida
exp > Date.now(). LançaBadRequestExceptioncom'Survey link has expired'se expirado. - Recalcula
expectedToken = generateToken(dto.cid, dto.exp)e compara comdto.token. LançaBadRequestExceptioncom'Invalid survey token'se não bater. - Calcula
tokenHash = SHA256(dto.token). - Verifica se já existe
SurveyResponsecom essetokenHash. Se existir, retorna silenciosamente (idempotência). - Cria
SurveyResponsecom todos os campos do DTO.
Tabelas lidas: SurveyResponse (verificação de idempotência) Tabelas escritas: SurveyResponse
Tipos de filtro de segmentação
A engine de segmentação (SegmentationQueryBuilder) suporta os seguintes blocos de filtro:
| Categoria | Filtros disponíveis |
|---|---|
characteristic | email (presença), telefone (presença), cidade, estado, CPF (via hash), aniversário, mês de nascimento |
behavioral | Transações, produtos, valor, data, contagem, canal |
rfm / previousRfm | Segmento RFM atual (rfmStatus) ou anterior (previousRfmStatus) |
purchase_product | Coleção, departamento, marca, tipo, grupo, divisão, categoria, cor, tamanho |
purchase_store / purchase_seller | Transações por loja ou vendedor específico |
purchase_value / purchase_date / purchase_count | Filtros de transação por valor, data ou quantidade |
segment_ref | Referência a outro segmento salvo (inclusão ou exclusão) |
Lógica AND/OR:
- Entre blocos: AND ou OR configurável por segmento
- Dentro de um bloco: AND ou OR configurável por bloco
Tabelas do banco de dados
| Tabela | Módulo | Operações |
|---|---|---|
Segment | Intelligence | CRUD + listagem com histórico |
SegmentHistory | Intelligence | Escrita pelo cron diário de snapshot |
Customer | Intelligence, RFM, Quality | Leitura para contagem/análise; escrita pelo RFM (rfmStatus, lastOrder, etc.) |
Transaction | RFM, Quality, Survey | Leitura para análise histórica |
Organization | RFM | Leitura pelo cron diário |
Store, Seller, Product, Campaign | Intelligence (filtros) | Leitura para popular opções de filtro |
SurveyResponse | Survey | CRUD de respostas pós-atendimento |
WindowExpiry | Survey | Leitura para janelas expiradas |
Variáveis de ambiente
| Variável | Padrão | Uso |
|---|---|---|
SURVEY_SECRET | 'survey-default-secret' | Chave HMAC para tokens de pesquisa |
SURVEY_BASE_URL | 'https://galdix.com.br' | Base da URL de pesquisa |
DISABLE_SYNC_CRON | 'false' | Desabilita o cron de recompute RFM |
Agendamento de crons
| Serviço | Schedule | Horário BRT | Função |
|---|---|---|---|
IntelligenceService | EVERY_DAY_AT_3AM | 3h | handleDailySegmentSnapshot — snapshot de contagem de todos os segmentos ativos |
RfmService | 0 7 * * * | 7h | handleDailyRecompute — recomputa RFM de todos os clientes de todas as organizações |