Tema
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:
| Tabela | Campo | Conteúdo |
|---|---|---|
storeWhatsappNumber | accessToken | Token de acesso Meta Cloud API |
storeWhatsappNumber | appSecret | App Secret do Meta |
emailSettings | pass | Senha SMTP da loja |
organizationSmtpSettings | pass | Senha SMTP da organização |
chatwootConfig | apiKey | API 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:
| Tipo | Path |
|---|---|
| Imagem de email | email-images/{orgId}/{uuid}{ext} |
| Logo da organização | organization-logos/{orgId}/{uuid}{ext} |
| Imagem de template WhatsApp | whatsapp-templates/{orgId}/{uuid}{ext} |
| Thumbnail de template email | email-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.