Skip to content

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

CampoTTLChaveInvalidação
filterCache (Map em memória)5 minutosorganizationIdExpiraçã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 regras
  • percent — percentual do total da base
  • channels — quantidade com e-mail e com WhatsApp habilitado
  • avgTicket — ticket médio dos clientes do segmento
  • rfmBreakdown — distribuição por status RFM

Flow:

  1. Chama queryBuilder.buildAsync(rules) para converter as regras em cláusula WHERE do Prisma (fora de transação — pode fazer lookups de IDs de produto de forma leve).
  2. Abre uma transação Prisma com timeout: 15_000 ms.
  3. Dentro da transação, executa SET LOCAL statement_timeout = '12000' (12s de timeout no banco).
  4. Em paralelo (Promise.all) executa:
    • customer.count() com filtro do segmento
    • customer.count() total sem filtro
    • customer.count() com filtro + email: { not: null }
    • customer.count() com filtro + phone: { not: null } + phoneType: 'MOBILE' + whatsappOptOut: false
    • customer.groupBy(['rfmStatus']) com filtro
    • customer.aggregate() com somas de totalSpent e orderCount
  5. Calcula avgTicket = totalSpent / totalOrders.
  6. 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:

  1. Busca todos os segmentos com active: true da tabela Segment.
  2. Para cada segmento: a. Chama queryBuilder.buildAsync(seg.rules) para obter a cláusula WHERE. b. Executa customer.count({ where: whereClause }). c. Cria registro em SegmentHistory com { segmentId, count, date }. d. Atualiza Segment.lastCount com 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:

  1. Conta o total de clientes na tabela Customer.
  2. 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: 1 para pegar o penúltimo, pois o último é o lastCount)
  3. Para cada segmento calcula:
    • trendPercent = ((currentCount - previousCount) / previousCount) * 100
    • reachPercent = (currentCount / totalCustomers) * 100
    • isCountRecalculating — lido do Set in-process
  4. 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 segmento
  • data.rules — array de blocos de filtros (JSON)
  • data.isDynamic — se o segmento atualiza automaticamente
  • data.lastCount — contagem inicial (opcional, padrão 0)
  • userId — ID do usuário criador (opcional)

Flow:

  1. Busca a primeira loja (store.findFirst()) para obter organizationId. Lança ConflictException se não houver loja configurada.
  2. Verifica se já existe segmento com o mesmo nome (case-insensitive) na organização. Lança ConflictException se existir.
  3. Cria o registro em Segment com logic: 'custom', active: true.
  4. Chama recalculateSegmentCountAsync(created.id, rules) de forma não bloqueante.
  5. 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:

  1. Verifica conflito de nome com outros segmentos (exclui o próprio ID). Lança ConflictException se encontrar duplicata.
  2. Atualiza o registro em Segment. Só inclui lastCount no update se data.lastCount !== undefined.
  3. Chama recalculateSegmentCountAsync(id, rules) de forma não bloqueante.
  4. Retorna o segmento atualizado.

Tabelas lidas: SegmentTabelas escritas: Segment


recalculateSegmentCountAsync(segmentId, rules): void (privado)

Recalcula lastCount em background sem bloquear a resposta HTTP.

Flow:

  1. Adiciona segmentId ao Set recalculatingSegments.
  2. Executa em uma Promise não awaited: a. queryBuilder.buildAsync(rules) para obter o WHERE. b. Transação Prisma com timeout: 65_000 ms e SET LOCAL statement_timeout = '60000'. c. customer.count() com o WHERE. d. segment.update() com o novo lastCount.
  3. Em caso de falha, loga warning sem propagar.
  4. No finally, remove segmentId do 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 por DESAT)

Rodada 2 (4 queries em paralelo):

  • seller.findMany → vendedores com status ACTIVE
  • product.findMany → coleções distintas
  • product.findMany → departamentos distintos
  • product.findMany → marcas distintas

Rodada 3 (4 queries em paralelo):

  • product.findMany → tipos distintos
  • product.findMany → grupos distintos
  • product.findMany → divisões distintas
  • product.findMany → categorias distintas

Rodada 4 (4 queries em paralelo):

  • $queryRawSELECT DISTINCT unnest(colors) da tabela Product
  • $queryRawSELECT DISTINCT unnest(sizes) da tabela Product
  • product.findMany → nomes de produtos ativos
  • product.findMany → códigos de produto (com label código — nome)

Rodada 5 (3 queries em paralelo):

  • transaction.findMany → nomes de fornecedor distintos (vendorName)
  • campaign.findMany → campanhas não-draft
  • transaction.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

SegmentoCondiçã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
HibernatingSem 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 com status = 'PAID', totalValue > 0 e date < periodEnd. Clientes sem transações têm last_order = createdAt.
  • {prefix}_classified — estende agg com coluna days_since (dias desde a última ordem em relação a periodEnd) e coluna segment (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:

  1. Verifica DISABLE_SYNC_CRON === 'true' e retorna sem fazer nada se verdadeiro.
  2. Busca todas as organizações do banco (organization.findMany).
  3. Para cada organização, chama recomputeAll(org.id, new Date()).
  4. 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ção
  • referencePeriod — 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:

  1. Conta clientes criados antes de periodEnd (customer.count).
  2. Em paralelo, executa duas queries parametrizadas:
    • newRows — clientes com compra entre periodStart e periodEnd que NÃO tinham compra anterior a periodStart (compradores de primeira vez).
    • recurringRows — clientes com compra no período que JÁ tinham compra anterior.
  3. Executa a CTE buildSegmentCte(organizationId, periodEnd) sem prefixo, fazendo GROUP BY segment para obter métricas por segmento: COUNT, AVG(total_spent), AVG(order_count), SUM(total_spent), SUM(order_count), AVG(days_since).
  4. Constrói array profiles com todos os 10 segmentos (usando zeros para segmentos ausentes no resultado).
  5. Calcula summary com 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:

  1. 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 do buildSegmentCte.
  2. 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:

  1. Calcula currStart, currEnd e prevEnd como limites de mês às 3h UTC.
  2. Inicializa matriz 10×10 zerada.
  3. Executa query com dois CTEs: prev_classified (snapshot em prevEnd) e curr_classified (snapshot em currEnd). Faz JOIN nos dois por customer.id e conta pares (prev_segment, curr_segment).
  4. Popula matrixMap[prevSegment][currSegment] com os contadores.
  5. 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).
  6. Conta clientes novos: registrados entre currStart e currEnd.
  7. Em paralelo, busca distribuição de segmentos em currEnd e prevEnd para 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 }:

  • cpfcpfHash IS NOT NULL
  • emailisValidEmail(customer.email) (regex)
  • phoneisValidPhone(customer.phone) (DDD + dígitos)
  • complete — todos os três verdadeiros

Regras de validação (SQL)

CampoRegra
E-mailc.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 nascimentoEXTRACT(YEAR FROM c."birthDate") < 1900 OR > EXTRACT(YEAR FROM NOW()) - 14 — qualquer um desses invalida
CPFc."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 — strings YYYY-MM-DD ou objetos Date
  • storeIds — array de IDs de loja para filtrar (opcional); IDs são expandidos via StoreAliasResolver.expandStoreIds() para incluir lojas alias

Flow:

  1. Converte datas com dateRange().
  2. Expande storeIds se fornecidos.
  3. Executa query $queryRaw na tabela Transaction JOIN Customer com filtros de período e loja.
  4. 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:

  1. Similar ao getSummary, mas faz LEFT JOIN "Store" e agrupa por s.id, s.code, s."tradeName", s.name.
  2. Filtra apenas lojas com s."isActive" = true.
  3. 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íodo
  • sellerId — filtro por vendedor (opcional)
  • storeId — filtro por loja (opcional)
  • missingField'cpf' | 'email' | 'phone' | 'birthdate' (opcional) — filtra somente transações com aquele campo inválido
  • page — número da página (página começa em 1)

Tamanho de página: 50 registros.

Flow:

  1. Monta filteredWhere para o transaction.findMany (filtro loose: para CPF/email/phone usa customer.fieldX: null; para birthdate usa customer.birthDate: null).
  2. 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 de missingField com validação exata.
    • transaction.findMany com paginação, include de customer (id, name, cpfHash, cpfMasked, email, phone, birthDate) e store (name, tradeName).
  3. Mapeia cada transação chamando scoreTransaction() e calcula validBirthdate (ano entre 1900 e anoAtual - 14).
  4. 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:

  1. Busca todas as transações do período com salespersonId: { not: null }, incluindo dados do cliente.
  2. Agrupa em memória (Map<salespersonId, accumulador>) calculando: total, complete, errCpf, errEmail, errPhone, errBirthdate.
  3. Separa as chaves em UUIDs válidos (busca por seller.id) e não-UUIDs (busca por seller.externalId).
  4. Busca os vendedores com OR [{ id: { in: uuidKeys } }, { externalId: { in: nonUuidKeys } }].
  5. Constrói mapa de vendedor por ambas as chaves para lookup eficiente.
  6. 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}" onde exp é 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 atendimento
  • organizationId — UUID da organização
  • customerName, sellerName, storeName — dados para exibição na pesquisa
  • storeId — ID interno da loja (opcional, incluído como sid)
  • customerPhone — telefone do cliente (apenas últimos 4 dígitos incluídos como ph)

Flow:

  1. Calcula exp = Date.now() + 7 dias em ms.
  2. Chama generateToken(conversationId, exp).
  3. Constrói URLSearchParams com: cid, org, exp, t (token), name, seller, store, sid?, ph?.
  4. Concatena base (variável SURVEY_BASE_URL, padrão https://galdix.com.br) + /pesquisa? + querystring.

getResults(organizationId, days?, recentLimit?): Promise<object>

Retorna dashboard de resultados das pesquisas.

Parâmetros:

  • organizationId — UUID da organização
  • days — janela de tempo em dias (padrão 30)
  • recentLimit — quantidade de respostas recentes a detalhar (padrão 50, máx 500)

Flow:

  1. Busca todas as SurveyResponse da organização nos últimos days dias, ordenadas por submittedAt desc.
  2. Calcula avgRating com precisão de 1 decimal.
  3. Calcula distribution — array de 5 elementos (rating 1 a 5) com contagem de cada um.
  4. Agrupa em memória por storeName e sellerName para calcular médias por loja e vendedor.
  5. Para as recentLimit respostas mais recentes: a. Extrai telefones únicos. b. Busca clientes no CRM pelo telefone (com e sem prefixo +). c. Constrói mapa phone → { id, name }. d. Para cada resposta, substitui o nome pelo nome atual no CRM se encontrar o cliente.
  6. 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:

  1. Busca WindowExpiry da organização nos últimos days dias, ordenados por resolvedAt desc.
  2. Agrupa por storeName e por dia (convertendo para horário de Brasília UTC-3 para o agrupamento diário).
  3. 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 — conversationId
  • exp — timestamp de expiração
  • token — HMAC recebido
  • organizationId — UUID da organização
  • rating — nota de 1 a 5
  • comment, name, seller, store, phone, storeId — opcionais

Flow:

  1. Valida rating entre 1 e 5. Lança BadRequestException se inválido.
  2. Valida exp > Date.now(). Lança BadRequestException com 'Survey link has expired' se expirado.
  3. Recalcula expectedToken = generateToken(dto.cid, dto.exp) e compara com dto.token. Lança BadRequestException com 'Invalid survey token' se não bater.
  4. Calcula tokenHash = SHA256(dto.token).
  5. Verifica se já existe SurveyResponse com esse tokenHash. Se existir, retorna silenciosamente (idempotência).
  6. Cria SurveyResponse com 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:

CategoriaFiltros disponíveis
characteristicemail (presença), telefone (presença), cidade, estado, CPF (via hash), aniversário, mês de nascimento
behavioralTransações, produtos, valor, data, contagem, canal
rfm / previousRfmSegmento RFM atual (rfmStatus) ou anterior (previousRfmStatus)
purchase_productColeção, departamento, marca, tipo, grupo, divisão, categoria, cor, tamanho
purchase_store / purchase_sellerTransações por loja ou vendedor específico
purchase_value / purchase_date / purchase_countFiltros de transação por valor, data ou quantidade
segment_refReferê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

TabelaMóduloOperações
SegmentIntelligenceCRUD + listagem com histórico
SegmentHistoryIntelligenceEscrita pelo cron diário de snapshot
CustomerIntelligence, RFM, QualityLeitura para contagem/análise; escrita pelo RFM (rfmStatus, lastOrder, etc.)
TransactionRFM, Quality, SurveyLeitura para análise histórica
OrganizationRFMLeitura pelo cron diário
Store, Seller, Product, CampaignIntelligence (filtros)Leitura para popular opções de filtro
SurveyResponseSurveyCRUD de respostas pós-atendimento
WindowExpirySurveyLeitura para janelas expiradas

Variáveis de ambiente

VariávelPadrãoUso
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çoScheduleHorário BRTFunção
IntelligenceServiceEVERY_DAY_AT_3AM3hhandleDailySegmentSnapshot — snapshot de contagem de todos os segmentos ativos
RfmService0 7 * * *7hhandleDailyRecompute — recomputa RFM de todos os clientes de todas as organizações