Skip to content

Serviços Comuns (Common Layer)

Serviços compartilhados por múltiplos módulos, localizados em backend/src/common/.

EncryptionService

Arquivo: common/services/encryption.service.ts

Criptografia simétrica AES-256-GCM para dados sensíveis em repouso.

typescript
class EncryptionService implements OnModuleInit {
  constructor(@Inject(SECRET_PROVIDER) private secrets: ISecretProvider) {}

  async onModuleInit(): Promise<void>
  // Deriva masterKey: SHA-256(MASTER_ENCRYPTION_KEY) → 32 bytes

  encrypt(text: string): string
  // Formato: "{iv_hex}:{ciphertext_hex}:{authTag_hex}"
  // IV: 12 bytes aleatórios
  // Auth tag: 16 bytes GCM
  
  decrypt(hash: string): string
  // Parse: split em ":", decifra com IV e authTag
  // Lança InternalServerErrorException em falha
}

Campos cifrados em produção:

TabelaCampoConteúdo
storeWhatsappNumberaccessTokenToken de acesso Meta Cloud API
storeWhatsappNumberappSecretApp Secret do Meta
emailSettingspassSenha SMTP da loja
organizationSmtpSettingspassSenha SMTP da organização
chatwootConfigapiKeyAPI key do Chatwoot (quando não em KV)

MailService

Arquivo: common/services/mail.service.ts

Envio de emails transacionais via SMTP com configuração por organização.

typescript
class MailService {
  constructor(
    private prisma: PrismaService,
    private encryption: EncryptionService,
  ) {}

  // Envia credenciais de novo agente (usuário convidado)
  async sendAgentCredentials(
    organizationId: string,
    to: string,
    payload: { name: string; email: string; tempPassword: string }
  ): Promise<void>

  // Envia email de pesquisa pós-venda
  async sendSurveyEmail(
    organizationId: string,
    to: string,
    payload: { surveyUrl: string; customerName: string }
  ): Promise<void>

  // Testa conexão SMTP (lança em caso de falha)
  async sendTest(organizationId: string, to: string): Promise<void>
}

Retry: até 2 tentativas adicionais em erros transientes (4xx, 503, 421). Métodos sendAgentCredentials e sendSurveyEmail retornam silenciosamente se SMTP não estiver configurado. sendTest lança exceção.

Timeout de verificação: 10 segundos.


MinioService

Arquivo: common/services/minio.service.ts

Upload de arquivos para MinIO (S3-compatible).

typescript
class MinioService {
  // Variáveis de ambiente:
  // MINIO_ENDPOINT    → default: http://minio:9000
  // MINIO_BUCKET      → default: crm-templates
  // MINIO_PUBLIC_URL  → default: https://minio.galdix.com.br
  // MINIO_ACCESS_KEY, MINIO_SECRET_KEY

  async upload(
    key: string,             // caminho no bucket (ex: email-images/org-id/uuid.jpg)
    buffer: Buffer,
    mimeType: string,
  ): Promise<string>          // URL pública

  async delete(key: string): Promise<void>
}

Paths usados:

TipoPath
Imagem de emailemail-images/{orgId}/{uuid}{ext}
Logo da organizaçãoorganization-logos/{orgId}/{uuid}{ext}
Imagem de template WhatsAppwhatsapp-templates/{orgId}/{uuid}{ext}
Thumbnail de template emailemail-thumbnails/{orgId}/{uuid}{ext}

ChatwootDbService

Arquivo: common/services/chatwoot-db.service.ts

Acesso direto ao banco PostgreSQL do Chatwoot (instância separada). Usa pg.Pool em vez do Prisma porque o schema do Chatwoot não está no Prisma do CRM.

typescript
class ChatwootDbService implements OnModuleDestroy {
  // Pool conecta em DATABASE_URL com schema /chatwoot_primicia
  
  async onModuleDestroy(): Promise<void>  // fecha pool

  // Agentes
  async confirmAgentWithPassword(email: string, password: string): Promise<boolean>
  async blockAgentLogins(emails: string[]): Promise<void>
  async unblockAgentLogins(emails: string[]): Promise<void>
  async removeFromAccount(chatwootUserId: number, accountId: number): Promise<void>
  async getAgentPubsubTokens(emails: string[]): Promise<string[]>

  // Inboxes e membros
  async addAgentToInbox(userId: number, inboxId: number): Promise<void>
  async removeAgentFromAllInboxes(chatwootUserId: number): Promise<void>
  async getInboxAgentIds(inboxId: number): Promise<number[]>

  // Busca de agentes
  async findAgentInInbox(
    sellerName: string,
    inboxId: number,
  ): Promise<{ id: number; name: string } | null>
  // Busca por match exato ou por sufixo (ex: "João" casa "Vendedor João")

  // Conversas
  async getOpenConversationsForAgent(
    agentId: number,
    inboxIds: number[],
  ): Promise<{ id: number; inboxId: number }[]>

  async assignAndNoteConversation(
    convId: number,
    accountId: number,
    assigneeId: number,
    note: string,
  ): Promise<void>

  async getLastReopenActor(conversationId: number): Promise<'agent' | 'admin' | null>
  async getLastResolveActor(conversationId: number): Promise<'agent' | 'admin' | null>

  // Mensagens
  async setMessageSourceId(messageId: number, sourceId: string): Promise<void>
  // Correlaciona mensagem do Chatwoot ao WAM ID do Meta

  async updateMessageStatusBySourceId(
    sourceId: string,
    status: 'sent' | 'delivered' | 'read' | 'failed',
  ): Promise<void>
  // Tenta via Chatwoot API primeiro, cai para SQL se falhar

  // Contatos
  async getContactPhone(contactId: number): Promise<string | null>

  // Janelas de 24h (Meta)
  async getExpiringConversations(
    inboxIds: number[],
    minutesBefore: number,
  ): Promise<{ id: number; contactId: number }[]>

  // Labels e timestamps
  async getWaitingLabelTimestamps(
    convIds: number[],
  ): Promise<Map<number, Date>>
}

Erro handling: todos os métodos capturam erros, logam e retornam valor padrão (null, [], false). Nunca lançam para o caller.


StoreAliasResolver

Arquivo: common/services/store-alias-resolver.service.ts

Cache em memória (TTL 60s) para resolução de aliases de lojas.

typescript
class StoreAliasResolver {
  constructor(private prisma: PrismaService) {}

  // Expande lista de storeIds incluindo todos os aliases:
  async expandStoreIds(storeIds: string[]): Promise<string[]>

  // Dado qualquer storeId (alias ou primário), retorna o primário:
  async resolvePrimary(storeId: string): Promise<string>

  // Map completo alias → primary:
  async getPrimaryMap(): Promise<Map<string, string>>

  // Invalida cache (chamar após mudanças de alias):
  invalidate(): void
}

Uso típico: ao filtrar vendas por loja, o SalesService expande os IDs para incluir alias.


Secrets Management

Arquivos: common/secrets/

ISecretProvider (interface)

typescript
interface ISecretProvider {
  get(key: SecretKey): Promise<string>          // lança se não existir
  getOptional(key: SecretKey): Promise<string | undefined>
}

EnvSecretProvider

Implementação atual: lê de variáveis de ambiente via ConfigService.

typescript
class EnvSecretProvider implements ISecretProvider, OnModuleInit {
  async onModuleInit(): Promise<void> {
    // Valida secrets obrigatórios na inicialização:
    // - DATABASE_URL
    // - MASTER_ENCRYPTION_KEY
    // - CLERK_SECRET_KEY
    // Lança erro fatal se algum estiver ausente
  }
}

SecretKey (enum)

typescript
enum SecretKey {
  DATABASE_URL,
  MASTER_ENCRYPTION_KEY,
  JWT_SECRET,
  CLERK_SECRET_KEY,
  NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
  FRONTEND_URL,
  CLERK_WEBHOOK_SECRET,
}

Placeholder para Azure Key Vault

O SecretsModule define SECRET_PROVIDER como token de injeção. Para migrar para Azure Key Vault, basta trocar o provider no módulo sem alterar os consumers.


CustomerRepository

Arquivo: common/repositories/customer.repository.ts

Repositório para queries de clientes, injetável via token CUSTOMER_REPOSITORY.

typescript
interface ICustomerRepository {
  count(args: { where: Prisma.CustomerWhereInput }): Promise<number>
  
  findMany(args: {
    where: Prisma.CustomerWhereInput
    select?: Prisma.CustomerSelect
    take?: number
    skip?: number
  }): Promise<any[]>
}

Usado pelo BehaviorFilterService para construção de audiências de campanha com cursor-based pagination.

Documentação interna — Galdix CRM