Compare commits
No commits in common. "82aca0f94d1813e06043fb1f297d311b66780c3a" and "dea436debed4a69ef5d28378a83b003f9e8e4d04" have entirely different histories.
82aca0f94d
...
dea436debe
446
README.md
446
README.md
@ -2,35 +2,26 @@
|
|||||||
|
|
||||||
## Visão Geral
|
## Visão Geral
|
||||||
|
|
||||||
A **Sothis Contratação API** é uma solução robusta para automatizar e facilitar o processo de novas contratações. A aplicação gerencia consultas de viabilidade técnica e a criação de prospectos de clientes no sistema integrado Hubsoft.
|
Esta API foi desenvolvida para automatizar e facilitar o processo de novas contratações. Suas duas funcionalidades principais são:
|
||||||
|
|
||||||
### Funcionalidades Principais
|
1. **Consulta de Viabilidade**: Verifica se um determinado endereço (baseado em CEP e número) possui viabilidade técnica para instalação de serviços.
|
||||||
|
2. **Criação de Prospectos**: Registra um novo cliente potencial (prospect) no sistema de gestão Hubsoft.
|
||||||
|
|
||||||
1. **Consulta de Viabilidade por CEP e Número**: Verifica a viabilidade técnica de um endereço e retorna informações sobre disponibilidade de serviços (dedicado e não-dedicado) com base na proximidade de pontos de presença.
|
A aplicação é construída em Node.js com Express e segue uma arquitetura modular para separação de responsabilidades.
|
||||||
2. **Consulta de Viabilidade por Coordenadas**: Consulta viabilidade usando latitude e longitude com reverse geocoding.
|
|
||||||
3. **Criação de Prospectos**: Registra novos clientes (Pessoa Física ou Jurídica) no sistema de gestão Hubsoft com suporte a diferentes planos de banda larga.
|
|
||||||
|
|
||||||
## Features Técnicas
|
## Features
|
||||||
|
|
||||||
- **Express.js 5.1**: Framework web moderno para construção de APIs RESTful.
|
- **Servidor Express**: API RESTful robusta e performática.
|
||||||
- **Arquitetura Modular**: Estrutura baseada em MVC com separação clara entre Controller, Service, Repository e Model.
|
- **Arquitetura Modular**: Lógica de negócio organizada no módulo `contratacao`, facilitando a manutenção e expansão.
|
||||||
- **Integração com Múltiplas APIs**:
|
- **Serviços Externos**: Integração com múltiplas APIs de terceiros para consulta de CEP, geolocalização e gestão de clientes.
|
||||||
- **Google Maps Geocoding**: Para geocodificação e reverse geocoding de endereços.
|
- **Logging Avançado**: Utiliza `winston` para registrar logs da aplicação e de erros em arquivos diários rotacionados.
|
||||||
- **GeoGrid API**: Consulta de pontos de presença e viabilidade técnica.
|
- **Gestão de Ambiente**: Usa `dotenv` para gerenciar configurações de desenvolvimento e produção de forma segura e isolada.
|
||||||
- **Hubsoft API**: Gestão de prospectos e clientes.
|
- **Desenvolvimento Otimizado**: `nodemon` para reinicialização automática do servidor em ambiente de desenvolvimento.
|
||||||
- **CEP Validation**: Integração com `cep-promise` para validação e busca de endereços brasileiros.
|
|
||||||
- **Logging Avançado**: Sistema de logging estruturado com `winston` e arquivo rotativo diário.
|
|
||||||
- **Configuração por Ambiente**: Suporte a múltiplos ambientes (desenvolvimento/produção) via `dotenv`.
|
|
||||||
- **CORS Configurado**: Habilitado para domínios específicos (`sothis.com.br` e `bandalarga.srv.br`).
|
|
||||||
- **PostgreSQL Ready**: Preparado para persistência de dados com `pg`.
|
|
||||||
- **Desenvolvimento Otimizado**: Hot-reload automático com `nodemon`.
|
|
||||||
|
|
||||||
## Pré-requisitos
|
## Pré-requisitos
|
||||||
|
|
||||||
- Node.js (versão 18.x ou superior recomendada)
|
- Node.js (versão 18.x ou superior recomendada)
|
||||||
- npm (geralmente instalado junto com o Node.js)
|
- npm (geralmente instalado junto com o Node.js)
|
||||||
- PostgreSQL (opcional, para persistência de dados)
|
|
||||||
- Chaves de API: Google Maps, GeoGrid, Hubsoft
|
|
||||||
|
|
||||||
## 1. Instalação
|
## 1. Instalação
|
||||||
|
|
||||||
@ -47,56 +38,29 @@ A aplicação utiliza arquivos `.env` para carregar suas configurações. Você
|
|||||||
- `.env.development` (para ambiente de desenvolvimento)
|
- `.env.development` (para ambiente de desenvolvimento)
|
||||||
- `.env.production` (para ambiente de produção)
|
- `.env.production` (para ambiente de produção)
|
||||||
|
|
||||||
### Variáveis de Ambiente Obrigatórias
|
Preencha os arquivos com as seguintes variáveis de ambiente:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
# Servidor
|
# Porta da API (padrão: 3000)
|
||||||
PORT=3000
|
PORT=3000
|
||||||
NODE_ENV=development
|
|
||||||
|
|
||||||
# Google Maps Geocoding API (para geocodificação e reverse geocoding)
|
# Chave da API do Google Maps Geocoding
|
||||||
GOOGLE_API_KEY=sua_chave_google_maps_aqui
|
GOOGLE_API_KEY=SUA_CHAVE_AQUI
|
||||||
|
|
||||||
# GeoGrid API (para consulta de viabilidade técnica)
|
# Configurações da API GeoGrid
|
||||||
GEOGRID_API_URL=https://.../viabilidade/raio # URL COMPLETA da consulta por raio
|
GEOGRID_API_URL=URL_DA_API_GEOGRID
|
||||||
GEOGRID_API_KEY=sua_chave_geogrid_aqui
|
GEOGRID_API_KEY=SUA_CHAVE_GEOGRID_AQUI
|
||||||
GEOGRID_API_COOKIE=seu_cookie_geogrid_aqui
|
GEOGRID_API_COOKIE=SEU_COOKIE_GEOGRID_AQUI
|
||||||
|
|
||||||
# Distância REAL de trajeto (opcionais — têm defaults seguros)
|
# Configurações da API Hubsoft
|
||||||
GEOGRID_API_BASE_URL=https://.../vale/api/v3 # base p/ o endpoint de trajeto; se ausente, derivada da URL de consulta
|
HUBSOFT_URL=URL_DO_HUBSOFT
|
||||||
GEOGRID_TRAJETO_MODO=walking # modo do trajeto
|
HUBSOFT_CLIENT_ID=SEU_CLIENT_ID_HUBSOFT
|
||||||
GEOGRID_TRAJETO_TOP_N=3 # nº de caixas roteadas na consulta individual (precisão)
|
HUBSOFT_CLIENT_SECRET=SEU_CLIENT_SECRET_HUBSOFT
|
||||||
GEOGRID_TRAJETO_TOP_N_LOTE=2 # nº de caixas roteadas por item no lote (a planilha mostra 2 caixas)
|
HUBSOFT_USERNAME=SEU_USUARIO_HUBSOFT
|
||||||
GEOGRID_CONCURRENCY=5 # chamadas externas simultâneas no lote
|
HUBSOFT_PASSWORD=SUA_SENHA_HUBSOFT
|
||||||
|
|
||||||
# Hubsoft API (para gestão de prospectos)
|
|
||||||
HUBSOFT_URL=https://seu-dominio.hubsoft.com.br
|
|
||||||
HUBSOFT_CLIENT_ID=seu_client_id_hubsoft
|
|
||||||
HUBSOFT_CLIENT_SECRET=seu_client_secret_hubsoft
|
|
||||||
HUBSOFT_USERNAME=seu_usuario_hubsoft
|
|
||||||
HUBSOFT_PASSWORD=sua_senha_hubsoft
|
|
||||||
HUBSOFT_GRANT_TYPE=password
|
HUBSOFT_GRANT_TYPE=password
|
||||||
|
|
||||||
# Database PostgreSQL (opcional, para persistência de viabilidades)
|
|
||||||
DB_HOST=localhost
|
|
||||||
DB_PORT=5432
|
|
||||||
DB_USER=seu_usuario_postgres
|
|
||||||
DB_PASSWORD=sua_senha_postgres
|
|
||||||
DB_NAME=contratacao_db
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Classificação de Pastas (provedor)
|
|
||||||
|
|
||||||
O provedor é resolvido pelo **prefixo da sigla da pasta** no GeoGrid (não há configuração no `.env`):
|
|
||||||
|
|
||||||
- `s-...` → **Sothis** (rede própria).
|
|
||||||
- `p-<nome>...` → **parceiro**; o provedor é o nome que vem depois do `-` (ex.: `p-Vivo` → `Vivo`).
|
|
||||||
- **Meio de transmissão** (marcador logo após o prefixo):
|
|
||||||
- `sub` → rede **subterrânea**, prefixa `(Subterrâneo)` (ex.: `s-sub-Barueri` → `(Subterrâneo) Sothis`).
|
|
||||||
- `a` → **Aéreo**, sufixa ` - Aéreo` (ex.: `S-A-ABC` → `Sothis - Aéreo`).
|
|
||||||
- `r` → **Rádio**, sufixa ` - Rádio` (ex.: `P-R-Vivo` → `Vivo - Rádio`).
|
|
||||||
- Qualquer sigla fora dessa convenção é considerada **não elegível** (ignorada).
|
|
||||||
|
|
||||||
## 3. Rodando a Aplicação
|
## 3. Rodando a Aplicação
|
||||||
|
|
||||||
### Modo de Desenvolvimento
|
### Modo de Desenvolvimento
|
||||||
@ -107,11 +71,6 @@ Este comando inicia a API com `nodemon`, que reinicia o servidor automaticamente
|
|||||||
npm run dev:api
|
npm run dev:api
|
||||||
```
|
```
|
||||||
|
|
||||||
A API será iniciada na porta configurada (padrão `3000`) e exibirá:
|
|
||||||
```
|
|
||||||
🚀 Servidor API rodando na porta 3000 em modo development
|
|
||||||
```
|
|
||||||
|
|
||||||
### Modo de Produção
|
### Modo de Produção
|
||||||
|
|
||||||
Este comando inicia a API de forma otimizada para produção usando `node`. As configurações serão carregadas do `.env.production`.
|
Este comando inicia a API de forma otimizada para produção usando `node`. As configurações serão carregadas do `.env.production`.
|
||||||
@ -122,96 +81,21 @@ npm run start:api
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Logging e Monitoramento
|
|
||||||
|
|
||||||
A aplicação utiliza **Winston** para logging estruturado com as seguintes características:
|
|
||||||
|
|
||||||
- **Arquivos de Log**: Criados automaticamente no diretório `logs/`
|
|
||||||
- **Rotação Diária**: Novos arquivos criados a cada dia com formato `application-YYYY-MM-DD.log`
|
|
||||||
- **Níveis de Log**:
|
|
||||||
- `info`: Requisições recebidas, operações bem-sucedidas
|
|
||||||
- `warn`: Advertências e situações inesperadas
|
|
||||||
- `error`: Erros e exceções
|
|
||||||
|
|
||||||
### Exemplo de Logs
|
|
||||||
|
|
||||||
```
|
|
||||||
2026-05-20T10:30:45.123Z [info]: 🚀 Servidor API rodando na porta 3000 em modo development
|
|
||||||
2026-05-20T10:31:20.456Z [info]: Requisição recebida: POST /api/viabilidade - IP: 127.0.0.1
|
|
||||||
2026-05-20T10:31:21.789Z [info]: Endereço obtido com sucesso via cep-promise
|
|
||||||
2026-05-20T10:31:23.234Z [info]: Coordenadas obtidas com sucesso - lat: -23.5070, lon: -46.4036
|
|
||||||
2026-05-20T10:31:25.567Z [info]: Viabilidade salva com sucesso
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tratamento de Erros
|
|
||||||
|
|
||||||
A API implementa tratamento robusto de erros:
|
|
||||||
|
|
||||||
1. **Validação de Entrada**: CEP e número são obrigatórios na consulta de viabilidade
|
|
||||||
2. **Erros de Integração**: Erros de terceiros são tratados e retornam respostas HTTP apropriadas
|
|
||||||
3. **Logging de Erros**: Todos os erros são registrados com stack trace completo
|
|
||||||
4. **Respostas Consistentes**: Erros retornam status HTTP apropriado e mensagem descritiva
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Integração com Serviços Externos
|
|
||||||
|
|
||||||
### Google Maps Geocoding
|
|
||||||
- **Uso**: Converter endereço em coordenadas e vice-versa (reverse geocoding)
|
|
||||||
- **Rate Limit**: Respeitar limites da Google API
|
|
||||||
|
|
||||||
### GeoGrid API
|
|
||||||
- **Uso**: Consultar pontos de presença e viabilidade de instalação
|
|
||||||
- **Autenticação**: Via API Key e Cookie
|
|
||||||
|
|
||||||
### Hubsoft API
|
|
||||||
- **Uso**: Criação e gestão de prospectos
|
|
||||||
- **Autenticação**: OAuth 2.0 com grant_type=password
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Configuração de CORS
|
|
||||||
|
|
||||||
A API está configurada para aceitar requisições apenas de domínios específicos:
|
|
||||||
|
|
||||||
```
|
|
||||||
✅ https://sothis.com.br
|
|
||||||
✅ https://bandalarga.srv.br
|
|
||||||
```
|
|
||||||
|
|
||||||
**Métodos HTTP Permitidos**:
|
|
||||||
- GET, POST, PUT, DELETE, OPTIONS
|
|
||||||
|
|
||||||
**Headers Permitidos**:
|
|
||||||
- Content-Type
|
|
||||||
- Authorization
|
|
||||||
|
|
||||||
Para adicionar novos domínios, edite o arquivo [app.js:18](src/app.js#L18).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Documentação da API
|
## Documentação da API
|
||||||
|
|
||||||
O prefixo para todos os endpoints é `/api`.
|
O prefixo para todos os endpoints é `/api`.
|
||||||
|
|
||||||
### 1. Consulta de Viabilidade por CEP
|
### Consulta de Viabilidade
|
||||||
|
|
||||||
Verifica a viabilidade técnica de um endereço usando CEP e número.
|
Verifica a viabilidade de serviço para um endereço.
|
||||||
|
|
||||||
- **Endpoint**: `POST /viabilidade`
|
- **Endpoint**: `POST /viabilidade`
|
||||||
- **Descrição**: Realiza geocodificação do endereço e consulta viabilidade na GeoGrid baseado em pontos de presença.
|
|
||||||
- **Corpo da Requisição** (`application/json`):
|
- **Corpo da Requisição** (`application/json`):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"nome": "João Silva",
|
|
||||||
"email": "joao@email.com",
|
|
||||||
"telefone": "11987654321",
|
|
||||||
"cep": "06419240",
|
"cep": "06419240",
|
||||||
"numero": "303",
|
"numero": 303
|
||||||
"source": ""
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -219,289 +103,53 @@ Verifica a viabilidade técnica de um endereço usando CEP e número.
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"nome": "João Silva",
|
|
||||||
"email": "joao@email.com",
|
|
||||||
"telefone": "11987654321",
|
|
||||||
"logradouro": "Rua Imirim",
|
|
||||||
"numero": "303",
|
|
||||||
"bairro": "Chácaras Marco",
|
"bairro": "Chácaras Marco",
|
||||||
"cidade": "Barueri",
|
"cidade": "Barueri",
|
||||||
"estado": "SP",
|
"estado": "SP",
|
||||||
"cep": "06419240",
|
"logradouro": "Rua Imirim",
|
||||||
"naoDedicado": true,
|
"naoDedicado": true,
|
||||||
"dedicado": true,
|
"dedicado": true
|
||||||
"distancia": 250,
|
|
||||||
"provedor": "Sothis"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Campos da Resposta**:
|
|
||||||
- `naoDedicado`: Serviço disponível em banda compartilhada (distância real ≤ 500m)
|
|
||||||
- `dedicado`: Serviço disponível em banda dedicada (distância real ≤ 1000m)
|
|
||||||
- `distancia`: Distância **real de trajeto** em metros até a caixa mais próxima (ou "5KM+" se não houver caixa no raio). Se o trajeto falhar, cai para a distância em linha reta.
|
|
||||||
- `distanciaReta`: Distância em **linha reta** em metros (aditivo, para comparação).
|
|
||||||
- `provedor`: Identificação do provedor pela convenção de prefixo da sigla de pasta (ver "Classificação de Pastas")
|
|
||||||
|
|
||||||
> **Nota:** a regra de viabilidade e o campo `distancia` passaram a usar a **distância real de trajeto** (via GeoGrid `/integracao/trajeto/cordenada`). Como a real é sempre ≥ a linha reta, endereços de borda podem deixar de viabilizar em relação ao comportamento anterior.
|
|
||||||
|
|
||||||
- **Respostas de Erro**:
|
- **Respostas de Erro**:
|
||||||
- `400 Bad Request`: Se `cep` ou `numero` não forem fornecidos.
|
- `400 Bad Request`: Se `cep` ou `numero` não forem fornecidos.
|
||||||
- `422 Unprocessable Entity`: Se os dados de endereço estiverem incompletos ou inválidos.
|
- `404 Not Found`: Se o endereço não for encontrado para o CEP informado.
|
||||||
- `502 Bad Gateway`: Se o CEP for inválido ou não encontrado na biblioteca `cep-promise`.
|
- `500 Internal Server Error`: Para outras falhas no processo.
|
||||||
- `500 Internal Server Error`: Para outros erros no processo.
|
|
||||||
|
|
||||||
---
|
### Criação de Prospecto
|
||||||
|
|
||||||
### 1.1. Consulta de Viabilidade em Lote
|
Cria um novo cliente potencial no Hubsoft.
|
||||||
|
|
||||||
Consulta viabilidade para vários endereços/coordenadas de uma vez (usado pelo viabiliza). Reusa o mesmo core de viabilidade, com top-N de trajeto reduzido e concorrência limitada.
|
|
||||||
|
|
||||||
- **Endpoint**: `POST /viabilidade/lote`
|
|
||||||
- **Corpo da Requisição** (`application/json`):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"itens": [
|
|
||||||
{ "cep": "06419240", "numero": "303" },
|
|
||||||
{ "latitude": "-23.5070", "longitude": "-46.4036" }
|
|
||||||
],
|
|
||||||
"modo": "walking"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Resposta de Sucesso** (`200 OK`): array alinhado por índice ao `itens` enviado.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"resultados": [
|
|
||||||
{
|
|
||||||
"index": 0, "provedor": "Sothis",
|
|
||||||
"distancia": 630, "distanciaReta": 300, "distanciaReal": 630,
|
|
||||||
"dedicado": true, "naoDedicado": false,
|
|
||||||
"caixas": [
|
|
||||||
{ "provedor": "Sothis", "sigla": "A", "distancia": 420, "distanciaReta": 300, "distanciaReal": 420, "dedicado": true, "naoDedicado": true },
|
|
||||||
{ "provedor": "Parceiro - X", "sigla": "B", "distancia": 900, "distanciaReta": 700, "distanciaReal": 900, "dedicado": true, "naoDedicado": false }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{ "index": 1, "erro": "Item inválido: informe cep+numero ou latitude+longitude." }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- `caixas`: detalhe por caixa (as mais próximas em linha reta primeiro), cada uma com **seu próprio provedor**, distância de trajeto e classificação. No lote a seleção é **mista** (as N mais próximas de *qualquer* provedor — rede própria e parceiros), então caixas na mesma linha podem ter provedores diferentes. O viabiliza usa as **2 primeiras** para montar as colunas da planilha (Provedor/Não Dedicado/Dedicado/Distância por caixa). A consulta individual (`/viabilidade`) continua priorizando Sothis.
|
|
||||||
- Cada item é resolvido de forma independente: um erro em um item não derruba os demais (vem no campo `erro`).
|
|
||||||
- Coordenadas/CEPs repetidos são deduplicados (uma chamada externa por chave única).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. Consulta de Viabilidade por Coordenadas
|
|
||||||
|
|
||||||
Verifica a viabilidade técnica usando latitude e longitude com reverse geocoding.
|
|
||||||
|
|
||||||
- **Endpoint**: `POST /viabilidade/lat-long`
|
|
||||||
- **Descrição**: Consulta viabilidade baseado em coordenadas geográficas e realiza reverse geocoding.
|
|
||||||
- **Corpo da Requisição** (`application/json`):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"latitude": "-23.5070",
|
|
||||||
"longitude": "-46.4036"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Resposta de Sucesso** (`200 OK`):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"endereco": "Avenida Paulista, 1000 - Bela Vista - São Paulo, SP",
|
|
||||||
"naoDedicado": false,
|
|
||||||
"dedicado": true,
|
|
||||||
"distancia": 750,
|
|
||||||
"provedor": "Sothis"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Respostas de Erro**:
|
|
||||||
- `500 Internal Server Error`: Se ocorrer erro na consulta ou reverse geocoding.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. Criação de Prospecto
|
|
||||||
|
|
||||||
Cria um novo cliente (Pessoa Física ou Jurídica) no Hubsoft.
|
|
||||||
|
|
||||||
- **Endpoint**: `POST /prospecto`
|
- **Endpoint**: `POST /prospecto`
|
||||||
- **Descrição**: Registra um novo prospecto no sistema Hubsoft com dados de contato e plano selecionado.
|
|
||||||
- **Corpo da Requisição** (`application/json`):
|
- **Corpo da Requisição** (`application/json`):
|
||||||
|
|
||||||
**Para Pessoa Física (CPF não vazio)**:
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"prospectData": {
|
"cep": "06419240",
|
||||||
"Nome completo": "João Silva Santos",
|
"servicoId": 1,
|
||||||
"CPF": "123.456.789-00",
|
"servicoValor": 100.00,
|
||||||
"Nome da mãe": "Maria Silva",
|
"numero": "303",
|
||||||
"Data de nascimento": "15/05/1985",
|
"endereco": "Rua Imirim",
|
||||||
"E-mail": "joao@email.com",
|
"bairro": "Chácaras Marco",
|
||||||
"Celular": "(11) 98765-4321",
|
"tipoPessoa": "F",
|
||||||
"CEP": "06419240",
|
"nomeRazaoSocial": "Nome do Cliente",
|
||||||
"Logradouro": "Rua Imirim",
|
"cpfCnpj": "123.456.789-00",
|
||||||
"Número": "303",
|
"email": "cliente@email.com",
|
||||||
"Complemento": "Apto 42",
|
"telefone": "11999998888"
|
||||||
"Bairro": "Chácaras Marco",
|
|
||||||
"Descrição": "100 Mega + Super WiFi",
|
|
||||||
"Valor (mensal)": "R$ 119,90"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Para Pessoa Jurídica (CPF vazio, CNPJ preenchido)**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"prospectData": {
|
|
||||||
"Razão Social": "Empresa LTDA",
|
|
||||||
"CNPJ": "12.345.678/0001-90",
|
|
||||||
"E-mail": "contato@empresa.com",
|
|
||||||
"Celular": "(11) 98765-4321",
|
|
||||||
"CEP": "06419240",
|
|
||||||
"Logradouro": "Rua Imirim",
|
|
||||||
"Número": "303",
|
|
||||||
"Complemento": "",
|
|
||||||
"Bairro": "Chácaras Marco",
|
|
||||||
"Descrição": "500 Mega + Super WiFi + IP Fixo",
|
|
||||||
"Valor (mensal)": "R$ 249,90",
|
|
||||||
"CPF:": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Planos Disponíveis**:
|
|
||||||
- `100 Mega + Super WiFi` (ID: 22)
|
|
||||||
- `200 Mega + Super WiFi` (ID: 94)
|
|
||||||
- `500 Mega + Super WiFi` (ID: 96)
|
|
||||||
- `700 Mega + Super WiFi` (ID: 97)
|
|
||||||
- `1 Gb + Super WiFi` (ID: 32)
|
|
||||||
- Variações com `+ IP Fixo`
|
|
||||||
|
|
||||||
- **Resposta de Sucesso** (`200 OK`):
|
- **Resposta de Sucesso** (`200 OK`):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"message": "Prospecto criado com sucesso",
|
"message": "Prospecto criado com sucesso",
|
||||||
"data": {
|
"data": {
|
||||||
"tipo_pessoa": "pf",
|
// ... dados retornados pela API do Hubsoft
|
||||||
"nome_razao_social": "João Silva Santos",
|
|
||||||
"cpf_cnpj": "123.456.789-00",
|
|
||||||
"email": "joao@email.com",
|
|
||||||
"telefone": "11987654321",
|
|
||||||
"endereco": "Rua Imirim",
|
|
||||||
"numero": "303",
|
|
||||||
"bairro": "Chácaras Marco",
|
|
||||||
"cep": "06419240",
|
|
||||||
"servico": {
|
|
||||||
"id_servico": 22,
|
|
||||||
"valor": 119.90
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Respostas de Erro**:
|
- **Respostas de Erro**:
|
||||||
- `400 Bad Request`: Se o plano informado for inválido ou não encontrado.
|
- `500 Internal Server Error`: Se ocorrer um erro na comunicação com o Hubsoft.
|
||||||
- `500 Internal Server Error`: Se ocorrer erro na comunicação com Hubsoft.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Estrutura do Projeto
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── app.js # Ponto de entrada da aplicação
|
|
||||||
├── routes/
|
|
||||||
│ └── routes.js # Definição de rotas
|
|
||||||
├── modules/
|
|
||||||
│ └── contratacao/
|
|
||||||
│ ├── contratacao.controller.js # Controladoras HTTP
|
|
||||||
│ ├── contratacao.service.js # Lógica de negócio
|
|
||||||
│ ├── contratacao.repository.js # Acesso a dados
|
|
||||||
│ └── contratacao.model.js # Modelos de dados
|
|
||||||
└── shared/
|
|
||||||
├── apis/
|
|
||||||
│ ├── googleService.js # Integração Google Maps
|
|
||||||
│ ├── geogridService.js # Integração GeoGrid
|
|
||||||
│ └── hubsoftService.js # Integração Hubsoft
|
|
||||||
├── config/
|
|
||||||
│ ├── environment.js # Carregamento de variáveis
|
|
||||||
│ ├── apiConfig.js # Configurações de API
|
|
||||||
│ └── dbConfig.js # Configurações de banco
|
|
||||||
└── utils/
|
|
||||||
└── logger.js # Sistema de logging
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Boas Práticas e Considerações
|
|
||||||
|
|
||||||
### Validação de CEP
|
|
||||||
- A biblioteca `cep-promise` realiza consulta em múltiplas bases de dados brasileiras
|
|
||||||
- Retorna erro 502 se o CEP for inválido ou não encontrado
|
|
||||||
|
|
||||||
### Distância de Viabilidade
|
|
||||||
- **≤ 500m**: Ambos serviços disponíveis (dedicado e não-dedicado)
|
|
||||||
- **500m - 1000m**: Apenas serviço dedicado disponível
|
|
||||||
- **> 1000m**: Nenhum serviço disponível (retorna "5KM+")
|
|
||||||
|
|
||||||
### Criação de Prospectos
|
|
||||||
- O tipo de pessoa é detectado automaticamente: CPF não vazio = Pessoa Física, CNPJ com CPF vazio = Pessoa Jurídica
|
|
||||||
- Números de telefone são sanitizados automaticamente (removem caracteres não-numéricos)
|
|
||||||
- Valores monetários aceitam diversos formatos (R$ 119,90 ou 119.90)
|
|
||||||
|
|
||||||
### Segurança
|
|
||||||
- CORS restrito a domínios específicos
|
|
||||||
- Todas as requisições são logadas com IP e método
|
|
||||||
- Erros não expõem informações sensíveis ao cliente
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Erro: "CEP não encontrado"
|
|
||||||
- Verifique se o CEP está correto e tem 8 dígitos
|
|
||||||
- Alguns CEPs novos podem não estar na base de dados
|
|
||||||
|
|
||||||
### Erro: "Sem caixas dentro de 5km"
|
|
||||||
- Significa que não há ponto de presença próximo ao endereço
|
|
||||||
- O serviço não é viável para esta localização
|
|
||||||
|
|
||||||
### Erro: "Plano inválido"
|
|
||||||
- Verifique se a descrição do plano corresponde exatamente a um dos planos mapeados
|
|
||||||
- Referência: [contratacao.service.js:159-170](src/modules/contratacao/contratacao.service.js#L159-L170)
|
|
||||||
|
|
||||||
### Logs não aparecem
|
|
||||||
- Verifique se a pasta `logs/` foi criada (criada automaticamente)
|
|
||||||
- Verifique permissões de escrita no diretório do projeto
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Histórico de Mudanças
|
|
||||||
|
|
||||||
### Recent Updates
|
|
||||||
- ✅ Adicionado tratamento de erro para casos sem caixa em 5km
|
|
||||||
- ✅ Implementada variável de sigla de pasta no .env com mapeamento de provedor
|
|
||||||
- ✅ Refatorada busca de CEP usando `cep-promise`
|
|
||||||
- ✅ Adicionada URL de banda larga na configuração de CORS
|
|
||||||
- ✅ Atualizado suporte de CORS para Express 5.x
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Suporte e Contribuições
|
|
||||||
|
|
||||||
Para reportar bugs ou sugerir melhorias, entre em contato com a equipe de desenvolvimento Sothis.
|
|
||||||
|
|
||||||
**Contato**: gabriel.amancio@sothis.com.br
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Licença
|
|
||||||
|
|
||||||
ISC
|
|
||||||
@ -7,7 +7,7 @@
|
|||||||
"dev:api": "cross-env NODE_ENV=development nodemon src/app.js",
|
"dev:api": "cross-env NODE_ENV=development nodemon src/app.js",
|
||||||
"start:worker": "cross-env NODE_ENV=production node src/worker.js",
|
"start:worker": "cross-env NODE_ENV=production node src/worker.js",
|
||||||
"dev:worker": "cross-env NODE_ENV=development nodemon src/worker.js",
|
"dev:worker": "cross-env NODE_ENV=development nodemon src/worker.js",
|
||||||
"test": "node --test"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@ -30,19 +30,6 @@ async function handleViabilidadeLatLong(req, res) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleViabilidadeLote(req, res) {
|
|
||||||
const { itens, modo } = req.body || {};
|
|
||||||
logger.info('Recebida requisição de viabilidade em lote', { total: Array.isArray(itens) ? itens.length : 0, modo });
|
|
||||||
try {
|
|
||||||
const resultados = await contratacaoService.verificarViabilidadeLote(itens, { modo });
|
|
||||||
return res.json({ resultados });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Erro no controller ao processar viabilidade em lote', { message: error.message, stack: error.stack });
|
|
||||||
const statusCode = error.statusCode || 500;
|
|
||||||
return res.status(statusCode).json({ error: error.message || "Erro interno ao processar a viabilidade em lote." });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleCriarProspecto(req, res) {
|
async function handleCriarProspecto(req, res) {
|
||||||
const prospectData = req.body;
|
const prospectData = req.body;
|
||||||
|
|
||||||
@ -65,6 +52,27 @@ async function handleCriarProspecto(req, res) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
handleViabilidade,
|
handleViabilidade,
|
||||||
handleCriarProspecto,
|
handleCriarProspecto,
|
||||||
handleViabilidadeLatLong,
|
handleViabilidadeLatLong
|
||||||
handleViabilidadeLote
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
DESCRIÇÃO:
|
||||||
|
Este arquivo é o "Controller" do módulo de contratação. Ele atua como a camada intermediária que conecta as rotas da API com a lógica de negócio (serviços). Sua principal responsabilidade é receber as requisições HTTP, extrair os dados necessários (do corpo, parâmetros, etc.), chamar o serviço correspondente e formatar a resposta a ser enviada de volta ao cliente.
|
||||||
|
|
||||||
|
FUNÇÕES:
|
||||||
|
- handleViabilidade:
|
||||||
|
1. É acionado por uma rota (ex: POST /api/contratacao/viabilidade).
|
||||||
|
2. Extrai `cep` e `numero` do corpo da requisição (`req.body`).
|
||||||
|
3. Chama a função `verificarViabilidade` do `contratacao.service`, passando os dados recebidos.
|
||||||
|
4. Se a verificação for bem-sucedida, retorna o resultado como um JSON com status 200.
|
||||||
|
5. Se ocorrer um erro (lançado pelo serviço), captura o erro, loga a mensagem e retorna uma resposta de erro em JSON com o `statusCode` apropriado (ex: 400, 404, 500).
|
||||||
|
|
||||||
|
- handleCriarProspecto:
|
||||||
|
1. É acionado por uma rota (ex: POST /api/contratacao/prospecto).
|
||||||
|
2. Pega todos os dados do corpo da requisição (`req.body`), que representam os dados do prospecto.
|
||||||
|
3. Chama a função `criarProspecto` do `contratacao.service`.
|
||||||
|
4. Se a criação for bem-sucedida, retorna uma mensagem de sucesso com os dados do resultado.
|
||||||
|
5. Em caso de erro, segue um fluxo de tratamento de erro similar ao `handleViabilidade`.
|
||||||
|
|
||||||
|
Este controller garante que a lógica de negócio permaneça desacoplada do Express, focando apenas na orquestração do fluxo da requisição e resposta.
|
||||||
|
*/
|
||||||
@ -1,8 +1,6 @@
|
|||||||
// classe construtor para o modelo de viabilidade
|
// classe construtor para o modelo de viabilidade
|
||||||
class ViabilidadeModel {
|
class ViabilidadeModel {
|
||||||
// `distancia` passou a representar a distância REAL de trajeto (fallback: linha reta).
|
constructor(nome, email, telefone, logradouro, numero, bairro, city, state, cep, naoDedicado, dedicado, distancia, provedor) {
|
||||||
// `distanciaReta` é aditivo (linha reta), para comparação/depuração — não quebra consumidores.
|
|
||||||
constructor(nome, email, telefone, logradouro, numero, bairro, city, state, cep, naoDedicado, dedicado, distancia, provedor, distanciaReta = null) {
|
|
||||||
// Inicialização de propriedades do modelo pode ser feita aqui
|
// Inicialização de propriedades do modelo pode ser feita aqui
|
||||||
this.nome = nome;
|
this.nome = nome;
|
||||||
this.email = email;
|
this.email = email;
|
||||||
@ -17,19 +15,17 @@ class ViabilidadeModel {
|
|||||||
this.dedicado = dedicado;
|
this.dedicado = dedicado;
|
||||||
this.distancia = distancia;
|
this.distancia = distancia;
|
||||||
this.provedor = provedor;
|
this.provedor = provedor;
|
||||||
this.distanciaReta = distanciaReta;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ViabilidadeReverseGeocodeModel {
|
class ViabilidadeReverseGeocodeModel {
|
||||||
constructor(endereco, naoDedicado, dedicado, distancia, provedor, distanciaReta = null) {
|
constructor(endereco, naoDedicado, dedicado, distancia, provedor) {
|
||||||
// Inicialização de propriedades do modelo pode ser feita aqui
|
// Inicialização de propriedades do modelo pode ser feita aqui
|
||||||
this.endereco = endereco;
|
this.endereco = endereco;
|
||||||
this.naoDedicado = naoDedicado;
|
this.naoDedicado = naoDedicado;
|
||||||
this.dedicado = dedicado;
|
this.dedicado = dedicado;
|
||||||
this.distancia = distancia;
|
this.distancia = distancia;
|
||||||
this.provedor = provedor;
|
this.provedor = provedor;
|
||||||
this.distanciaReta = distanciaReta;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,3 +156,19 @@ class ProspectModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {ViabilidadeReverseGeocodeModel, ViabilidadeModel, ClientModelPf, ClientModelPj, ProspectModel};
|
module.exports = {ViabilidadeReverseGeocodeModel, ViabilidadeModel, ClientModelPf, ClientModelPj, ProspectModel};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
DESCRIÇÃO:
|
||||||
|
Este arquivo é destinado a definir o "Model" ou o esquema de dados para o módulo de contratação.
|
||||||
|
|
||||||
|
FLUXO:
|
||||||
|
- Em uma arquitetura com banco de dados, este arquivo normalmente conteria a definição de um modelo de dados usando um ORM (Object-Relational Mapper) como Sequelize (para SQL) ou Mongoose (para MongoDB).
|
||||||
|
- O modelo definiria os campos, tipos de dados, validações e relacionamentos para a entidade "contratação" ou "prospecto".
|
||||||
|
- Por exemplo, poderia definir que um prospecto deve ter campos como `nome` (String, obrigatório), `email` (String, formato de email), `cep` (String), etc.
|
||||||
|
- Este modelo seria então utilizado pelo `contratacao.repository.js` para realizar operações de CRUD (Create, Read, Update, Delete) no banco de dados de forma estruturada e segura.
|
||||||
|
|
||||||
|
ESTADO ATUAL:
|
||||||
|
Atualmente, o arquivo está como um placeholder e não contém uma implementação de modelo. A lógica de negócio não depende de uma estrutura de dados formalmente definida aqui.
|
||||||
|
*/
|
||||||
@ -46,3 +46,22 @@ async function insertViabilidadeData(viabilidadeData) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
insertViabilidadeData
|
insertViabilidadeData
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
DESCRIÇÃO:
|
||||||
|
Este arquivo implementa o padrão de "Repository" (Repositório) para o módulo de contratação. A camada de repositório é responsável por toda a comunicação com a fonte de dados, que geralmente é um banco de dados.
|
||||||
|
|
||||||
|
FLUXO:
|
||||||
|
1. Seria chamado pelo `contratacao.service.js` para persistir ou recuperar dados.
|
||||||
|
2. Utilizaria o `contratacao.model.js` para interagir com o banco de dados de forma estruturada. Por exemplo, usaria um modelo Sequelize ou Mongoose para executar queries.
|
||||||
|
3. Conteria métodos para operações de CRUD (Create, Read, Update, Delete), como:
|
||||||
|
- `create(prospectData)`: Para salvar um novo prospecto no banco de dados.
|
||||||
|
- `findById(id)`: Para buscar um prospecto pelo seu ID.
|
||||||
|
- `findAll()`: Para listar todos os prospectos.
|
||||||
|
- `update(id, newData)`: Para atualizar os dados de um prospecto.
|
||||||
|
- `delete(id)`: Para remover um prospecto.
|
||||||
|
4. Abstrairia os detalhes de implementação do banco de dados, permitindo que o serviço solicite os dados sem se preocupar em como eles são armazenados ou recuperados.
|
||||||
|
|
||||||
|
ESTADO ATUAL:
|
||||||
|
Atualmente, o arquivo é um placeholder. Como a lógica atual da aplicação se baseia em chamadas a APIs externas (Hubsoft) e não em um banco de dados local para prospectos, este repositório não tem uma implementação ativa. Se a aplicação precisasse armazenar dados localmente, este arquivo seria implementado.
|
||||||
|
*/
|
||||||
@ -1,10 +1,8 @@
|
|||||||
|
const geogridService = require("../../shared/apis/geogridService.js");
|
||||||
const googleService = require("../../shared/apis/googleService.js");
|
const googleService = require("../../shared/apis/googleService.js");
|
||||||
const hubsoftService = require("../../shared/apis/hubsoftService.js");
|
const hubsoftService = require("../../shared/apis/hubsoftService.js");
|
||||||
const logger = require('../../shared/utils/logger.js');
|
const logger = require('../../shared/utils/logger.js');
|
||||||
const apiConfig = require('../../shared/config/apiConfig.js');
|
|
||||||
const repository = require('./contratacao.repository.js');
|
const repository = require('./contratacao.repository.js');
|
||||||
const core = require('../viabilidade/viabilidade.core.js');
|
|
||||||
const { mapWithConcurrency } = require('../../shared/utils/concurrency.js');
|
|
||||||
const { ViabilidadeModel, ClientModelPf, ClientModelPj, ProspectModel, ViabilidadeReverseGeocodeModel } = require('./contratacao.model');
|
const { ViabilidadeModel, ClientModelPf, ClientModelPj, ProspectModel, ViabilidadeReverseGeocodeModel } = require('./contratacao.model');
|
||||||
|
|
||||||
// utilitário para ler chaves do payload tolerante a ":" e espaços
|
// utilitário para ler chaves do payload tolerante a ":" e espaços
|
||||||
@ -32,10 +30,21 @@ class ServiceError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve as coordenadas de um endereço a partir de CEP + número:
|
// Função para verificar a viabilidade de um endereço
|
||||||
// cep-promise (dados do endereço) -> Google Geocoding (lat/lon).
|
|
||||||
async function resolverCoordenadasPorCep(rawCep, rawNumero) {
|
async function verificarViabilidade(rawViabilidadeData) {
|
||||||
|
const rawCep = rawViabilidadeData.cep;
|
||||||
|
const rawNumero = rawViabilidadeData.numero;
|
||||||
|
const source = rawViabilidadeData.source || '';
|
||||||
|
|
||||||
let address;
|
let address;
|
||||||
|
let addressString;
|
||||||
|
|
||||||
|
if (!rawCep || !rawNumero) {
|
||||||
|
logger.warn('CEP ou número não fornecidos na verificação de viabilidade.');
|
||||||
|
throw new ServiceError('CEP e número são obrigatórios.', 400);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cep = require('cep-promise');
|
const cep = require('cep-promise');
|
||||||
address = await cep(rawCep);
|
address = await cep(rawCep);
|
||||||
@ -45,8 +54,13 @@ async function resolverCoordenadasPorCep(rawCep, rawNumero) {
|
|||||||
throw new ServiceError('Não foi possível obter endereço para o CEP informado ou o CEP é inválido.', 502);
|
throw new ServiceError('Não foi possível obter endereço para o CEP informado ou o CEP é inválido.', 502);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Obtém as coordenadas geográficas do endereço usando o Google Geocoding API
|
||||||
|
|
||||||
const { street, neighborhood, city, state, cep } = address;
|
const { street, neighborhood, city, state, cep } = address;
|
||||||
const addressString = `${street}, ${rawNumero}, ${neighborhood}, ${city}, ${state}, ${rawCep}`;
|
|
||||||
|
|
||||||
|
addressString = `${street}, ${rawNumero}, ${neighborhood}, ${city}, ${state}, ${rawCep}`;
|
||||||
logger.info('Endereço montado para geocodificação', { addressString });
|
logger.info('Endereço montado para geocodificação', { addressString });
|
||||||
|
|
||||||
if (!street || !neighborhood || !city || !state) {
|
if (!street || !neighborhood || !city || !state) {
|
||||||
@ -61,33 +75,31 @@ async function resolverCoordenadasPorCep(rawCep, rawNumero) {
|
|||||||
}
|
}
|
||||||
logger.info('Coordenadas obtidas com sucesso', { coords });
|
logger.info('Coordenadas obtidas com sucesso', { coords });
|
||||||
|
|
||||||
return { coords, address: { street, neighborhood, city, state, cep } };
|
const viabilidade = await geogridService.consultaViabilidade(coords.lat, coords.lon);
|
||||||
}
|
|
||||||
|
|
||||||
// Função para verificar a viabilidade de um endereço.
|
let naoDedicado = false;
|
||||||
// A distância usada na regra é a REAL (trajeto); `distanciaReta` vai junto no resultado.
|
let dedicado = false;
|
||||||
async function verificarViabilidade(rawViabilidadeData) {
|
|
||||||
const rawCep = rawViabilidadeData.cep;
|
|
||||||
const rawNumero = rawViabilidadeData.numero;
|
|
||||||
const source = rawViabilidadeData.source || '';
|
|
||||||
|
|
||||||
if (!rawCep || !rawNumero) {
|
if (viabilidade.data && viabilidade.data.distancia !== undefined) {
|
||||||
logger.warn('CEP ou número não fornecidos na verificação de viabilidade.');
|
var distancia = viabilidade.data.distancia;
|
||||||
throw new ServiceError('CEP e número são obrigatórios.', 400);
|
logger.info(`Distância para o ponto de presença: ${distancia}m`);
|
||||||
|
if (distancia <= 500) {
|
||||||
|
naoDedicado = true;
|
||||||
|
dedicado = true;
|
||||||
|
} else if (distancia <= 1000) {
|
||||||
|
naoDedicado = false;
|
||||||
|
dedicado = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
naoDedicado = false;
|
||||||
|
dedicado = false;
|
||||||
|
var distancia = "5KM+";
|
||||||
|
logger.warn('Dados de viabilidade não contêm informação de distância', { viabilidadeData: viabilidade.data });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { coords, address } = await resolverCoordenadasPorCep(rawCep, rawNumero);
|
|
||||||
const { street, neighborhood, city, state, cep } = address;
|
|
||||||
|
|
||||||
const avaliacao = await core.avaliarPorCoordenadas(coords);
|
const viabilidadeResult = new ViabilidadeModel(rawViabilidadeData.nome, rawViabilidadeData.email, rawViabilidadeData.telefone, street, rawNumero, neighborhood, city, state, cep, naoDedicado, dedicado, distancia || null, viabilidade.data.pasta.sigla || null);
|
||||||
logger.info('Avaliação de viabilidade concluída', { avaliacao });
|
|
||||||
|
|
||||||
const viabilidadeResult = new ViabilidadeModel(
|
|
||||||
rawViabilidadeData.nome, rawViabilidadeData.email, rawViabilidadeData.telefone,
|
|
||||||
street, rawNumero, neighborhood, city, state, cep,
|
|
||||||
avaliacao.naoDedicado, avaliacao.dedicado, avaliacao.distancia, avaliacao.provedor,
|
|
||||||
avaliacao.distanciaReta
|
|
||||||
);
|
|
||||||
|
|
||||||
if (source) {
|
if (source) {
|
||||||
return viabilidadeResult;
|
return viabilidadeResult;
|
||||||
@ -109,99 +121,33 @@ async function verificarGeocodeReverso(lat, lon) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function verificarViabilidadeLatLong(latitude, longitude) {
|
async function verificarViabilidadeLatLong(latitude, longitude) {
|
||||||
const coords = { lat: Number(latitude), lon: Number(longitude) };
|
const viabilidade = await geogridService.consultaViabilidade(latitude, longitude);
|
||||||
|
|
||||||
const avaliacao = await core.avaliarPorCoordenadas(coords);
|
let naoDedicado = false;
|
||||||
logger.info('Avaliação de viabilidade (lat/long) concluída', { avaliacao });
|
let dedicado = false;
|
||||||
|
|
||||||
|
if (viabilidade.data && viabilidade.data.distancia !== undefined) {
|
||||||
|
var distancia = viabilidade.data.distancia;
|
||||||
|
logger.info(`Distância para o ponto de presença: ${distancia}m`);
|
||||||
|
if (distancia <= 500) {
|
||||||
|
naoDedicado = true;
|
||||||
|
dedicado = true;
|
||||||
|
} else if (distancia <= 1000) {
|
||||||
|
naoDedicado = false;
|
||||||
|
dedicado = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
naoDedicado = false;
|
||||||
|
dedicado = false;
|
||||||
|
var distancia = "5KM+";
|
||||||
|
logger.warn('Dados de viabilidade não contêm informação de distância', { viabilidadeData: viabilidade.data });
|
||||||
|
}
|
||||||
|
|
||||||
const address = await verificarGeocodeReverso(latitude, longitude);
|
const address = await verificarGeocodeReverso(latitude, longitude);
|
||||||
|
|
||||||
return new ViabilidadeReverseGeocodeModel(
|
const viabilidadeResult = new ViabilidadeReverseGeocodeModel(address, naoDedicado, dedicado, distancia, viabilidade.data.pasta.sigla || null);
|
||||||
address, avaliacao.naoDedicado, avaliacao.dedicado, avaliacao.distancia, avaliacao.provedor,
|
|
||||||
avaliacao.distanciaReta
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Formata o resultado do core para o item de resposta do lote.
|
return viabilidadeResult;
|
||||||
function formatarResultadoLote(avaliacao) {
|
|
||||||
return {
|
|
||||||
provedor: avaliacao.provedor,
|
|
||||||
distancia: avaliacao.distancia,
|
|
||||||
distanciaReta: avaliacao.distanciaReta,
|
|
||||||
distanciaReal: avaliacao.distanciaReal,
|
|
||||||
dedicado: avaliacao.dedicado,
|
|
||||||
naoDedicado: avaliacao.naoDedicado,
|
|
||||||
// detalhe por caixa (mais próxima em linha reta primeiro) — usado nas colunas da planilha
|
|
||||||
caixas: (avaliacao.caixas || []).map(c => ({
|
|
||||||
provedor: c.provedor,
|
|
||||||
sigla: c.sigla,
|
|
||||||
distancia: c.distancia,
|
|
||||||
distanciaReta: c.distanciaReta,
|
|
||||||
distanciaReal: c.distanciaReal,
|
|
||||||
dedicado: c.dedicado,
|
|
||||||
naoDedicado: c.naoDedicado
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verificação em LOTE (usada pelo viabiliza). Aceita itens {cep, numero} ou
|
|
||||||
// {latitude, longitude}. Deduplica coordenadas/CEPs iguais, executa com
|
|
||||||
// concorrência limitada e usa top-N reduzido (mais barato) para o trajeto.
|
|
||||||
async function verificarViabilidadeLote(itens, { modo } = {}) {
|
|
||||||
if (!Array.isArray(itens)) {
|
|
||||||
throw new ServiceError('O campo "itens" deve ser um array.', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
const topN = apiConfig.geogridTrajetoTopNLote;
|
|
||||||
|
|
||||||
const chaveDe = (item) => {
|
|
||||||
if (item && item.latitude !== undefined && item.longitude !== undefined) {
|
|
||||||
const lat = Number(item.latitude), lon = Number(item.longitude);
|
|
||||||
if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null;
|
|
||||||
return `geo:${lat.toFixed(6)},${lon.toFixed(6)}`;
|
|
||||||
}
|
|
||||||
if (item && item.cep && item.numero) {
|
|
||||||
return `cep:${String(item.cep).replace(/\D/g, '')}-${String(item.numero).trim()}`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const avaliarItem = async (item) => {
|
|
||||||
// Lote/planilha: mistura provedores (2 caixas mais próximas de qualquer provedor).
|
|
||||||
const opts = { modo, topN, misturarProvedores: true };
|
|
||||||
if (item && item.latitude !== undefined && item.longitude !== undefined) {
|
|
||||||
const a = await core.avaliarPorCoordenadas({ lat: Number(item.latitude), lon: Number(item.longitude) }, opts);
|
|
||||||
return formatarResultadoLote(a);
|
|
||||||
}
|
|
||||||
const { coords } = await resolverCoordenadasPorCep(item.cep, item.numero);
|
|
||||||
const a = await core.avaliarPorCoordenadas(coords, opts);
|
|
||||||
return formatarResultadoLote(a);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Resolve apenas as chaves únicas (evita chamadas externas repetidas) e mapeia de volta.
|
|
||||||
const chaves = itens.map(chaveDe);
|
|
||||||
const unicas = [...new Set(chaves.filter(Boolean))];
|
|
||||||
const primeiroItemDaChave = unicas.map(chave => itens[chaves.indexOf(chave)]);
|
|
||||||
|
|
||||||
const resultadosUnicos = await mapWithConcurrency(primeiroItemDaChave, apiConfig.geogridConcurrency, async (item) => {
|
|
||||||
try {
|
|
||||||
return await avaliarItem(item);
|
|
||||||
} catch (e) {
|
|
||||||
logger.warn('Falha ao avaliar item do lote', { message: e.message });
|
|
||||||
return { erro: e.message || 'Erro ao avaliar item.' };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const porChave = {};
|
|
||||||
unicas.forEach((chave, i) => { porChave[chave] = resultadosUnicos[i]; });
|
|
||||||
|
|
||||||
return itens.map((item, index) => {
|
|
||||||
const chave = chaves[index];
|
|
||||||
if (!chave) {
|
|
||||||
return { index, erro: 'Item inválido: informe cep+numero ou latitude+longitude.' };
|
|
||||||
}
|
|
||||||
return { index, ...porChave[chave] };
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -318,6 +264,5 @@ module.exports = {
|
|||||||
verificarViabilidade,
|
verificarViabilidade,
|
||||||
criarProspecto,
|
criarProspecto,
|
||||||
verificarViabilidadeLatLong,
|
verificarViabilidadeLatLong,
|
||||||
verificarViabilidadeLote,
|
|
||||||
verificarGeocodeReverso,
|
verificarGeocodeReverso,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,95 +0,0 @@
|
|||||||
const geogridService = require('../../shared/apis/geogridService.js');
|
|
||||||
const apiConfig = require('../../shared/config/apiConfig.js');
|
|
||||||
const logger = require('../../shared/utils/logger.js');
|
|
||||||
const { mapWithConcurrency } = require('../../shared/utils/concurrency.js');
|
|
||||||
|
|
||||||
// Regra de negócio PURA: dada uma distância (em metros), define quais serviços
|
|
||||||
// estão disponíveis. É a mesma regra de sempre — o que muda é a distância que
|
|
||||||
// alimenta ela (agora a real de trajeto). Isolada e testável.
|
|
||||||
function classificarPorDistancia(distancia) {
|
|
||||||
if (distancia == null || typeof distancia !== 'number' || Number.isNaN(distancia)) {
|
|
||||||
return { dedicado: false, naoDedicado: false };
|
|
||||||
}
|
|
||||||
if (distancia <= 500) return { dedicado: true, naoDedicado: true };
|
|
||||||
if (distancia <= 1000) return { dedicado: true, naoDedicado: false };
|
|
||||||
return { dedicado: false, naoDedicado: false }; // 1001–5000m: caixa existe, mas fora do alcance
|
|
||||||
}
|
|
||||||
|
|
||||||
// Núcleo de orquestração: a partir de coordenadas de origem (endereço),
|
|
||||||
// 1) acha as N caixas mais próximas em linha reta (GeoGrid),
|
|
||||||
// 2) calcula a distância REAL de trajeto até cada uma e pega a menor,
|
|
||||||
// 3) classifica a viabilidade sobre a distância real (com fallback p/ reta).
|
|
||||||
//
|
|
||||||
// Retorna um resultado de domínio NEUTRO (sem formato de resposta HTTP):
|
|
||||||
// { distancia, distanciaReta, distanciaReal, dedicado, naoDedicado, provedor }
|
|
||||||
// - `distancia` é a EFETIVA usada na regra: real quando disponível, senão reta.
|
|
||||||
async function avaliarPorCoordenadas(origem, { modo = apiConfig.geogridTrajetoModo, topN, misturarProvedores = false } = {}) {
|
|
||||||
const { provedor, caixas } = await geogridService.consultarCaixasProximas(origem.lat, origem.lon, { topN, misturarProvedores });
|
|
||||||
|
|
||||||
// Sem caixa no raio: mantém o comportamento histórico ("5KM+", nada viável).
|
|
||||||
if (!caixas || caixas.length === 0) {
|
|
||||||
return {
|
|
||||||
distancia: "5KM+",
|
|
||||||
distanciaReta: null,
|
|
||||||
distanciaReal: null,
|
|
||||||
dedicado: false,
|
|
||||||
naoDedicado: false,
|
|
||||||
provedor,
|
|
||||||
caixas: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Para cada caixa (na ordem do GeoGrid = por distância em linha reta),
|
|
||||||
// calcula a distância real de trajeto e classifica individualmente.
|
|
||||||
const detalhes = await mapWithConcurrency(caixas, apiConfig.geogridConcurrency, async (caixa) => {
|
|
||||||
let distanciaReal = null;
|
|
||||||
if (caixa.lat != null && caixa.lon != null) {
|
|
||||||
try {
|
|
||||||
const trajeto = await geogridService.consultaTrajeto(origem, caixa, modo);
|
|
||||||
const d = Number(trajeto && trajeto.distancia);
|
|
||||||
distanciaReal = Number.isFinite(d) ? d : null;
|
|
||||||
} catch (err) {
|
|
||||||
logger.warn('Trajeto falhou para uma caixa, usando linha reta nesta candidata', {
|
|
||||||
message: err.message,
|
|
||||||
status: err.response?.status,
|
|
||||||
data: err.response?.data, // corpo do erro do GeoGrid (motivo do 400)
|
|
||||||
url: err.config?.url,
|
|
||||||
caixa: { lat: caixa.lat, lon: caixa.lon }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const distancia = distanciaReal ?? caixa.distanciaReta; // efetiva por caixa (real, senão reta)
|
|
||||||
return {
|
|
||||||
provedor: caixa.provedor,
|
|
||||||
sigla: caixa.sigla,
|
|
||||||
lat: caixa.lat,
|
|
||||||
lon: caixa.lon,
|
|
||||||
distanciaReta: caixa.distanciaReta,
|
|
||||||
distanciaReal,
|
|
||||||
distancia,
|
|
||||||
...classificarPorDistancia(distancia)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Agregado (compatibilidade WordPress): menor distância efetiva entre as caixas.
|
|
||||||
const distanciaReta = detalhes[0].distanciaReta ?? null; // caixa mais próxima em linha reta
|
|
||||||
const reais = detalhes.map(d => d.distanciaReal).filter(d => d != null);
|
|
||||||
const distanciaReal = reais.length ? Math.min(...reais) : null;
|
|
||||||
|
|
||||||
if (distanciaReal == null) {
|
|
||||||
logger.warn('Nenhum trajeto disponível; usando distância em linha reta como fallback', { origem, distanciaReta });
|
|
||||||
}
|
|
||||||
|
|
||||||
const distanciaEfetiva = distanciaReal ?? distanciaReta;
|
|
||||||
|
|
||||||
return {
|
|
||||||
distancia: distanciaEfetiva,
|
|
||||||
distanciaReta,
|
|
||||||
distanciaReal,
|
|
||||||
...classificarPorDistancia(distanciaEfetiva),
|
|
||||||
provedor,
|
|
||||||
caixas: detalhes // detalhe por caixa (mais próxima em linha reta primeiro)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { classificarPorDistancia, avaliarPorCoordenadas };
|
|
||||||
@ -5,9 +5,6 @@ const contratacaoController = require('../modules/contratacao/contratacao.contro
|
|||||||
// Rota para consulta de viabilidade
|
// Rota para consulta de viabilidade
|
||||||
router.post('/viabilidade', contratacaoController.handleViabilidade);
|
router.post('/viabilidade', contratacaoController.handleViabilidade);
|
||||||
|
|
||||||
// Rota para consulta de viabilidade em lote (array de itens cep+numero ou lat/long)
|
|
||||||
router.post('/viabilidade/lote', contratacaoController.handleViabilidadeLote);
|
|
||||||
|
|
||||||
// Rota para criação de prospecto no Hubsoft
|
// Rota para criação de prospecto no Hubsoft
|
||||||
router.post('/prospecto', contratacaoController.handleCriarProspecto);
|
router.post('/prospecto', contratacaoController.handleCriarProspecto);
|
||||||
|
|
||||||
|
|||||||
@ -3,190 +3,75 @@ const axios = require("axios");
|
|||||||
const qs = require("qs");
|
const qs = require("qs");
|
||||||
const logger = require('../utils/logger.js');
|
const logger = require('../utils/logger.js');
|
||||||
|
|
||||||
// Busca bruta dos registros (caixas) no GeoGrid por raio. Mantém exatamente os
|
|
||||||
// mesmos parâmetros/serialização/headers da consulta original.
|
|
||||||
const fetchRegistros = async (lat, lon) => {
|
|
||||||
const params = {
|
|
||||||
raio: 5000,
|
|
||||||
latitude: lat,
|
|
||||||
longitude: lon,
|
|
||||||
"itens[]": "caixa",
|
|
||||||
ordenarCampos: ["distancia"],
|
|
||||||
ordenarPor: ["asc"],
|
|
||||||
consultarPasta: "S",
|
|
||||||
consultarIndividual: "S"
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await axios.get(apiConfig.geogridApiUrl, {
|
|
||||||
params,
|
|
||||||
// força a serialização do tipo `itens[]=caixa`
|
|
||||||
paramsSerializer: p => qs.stringify(p, { arrayFormat: 'brackets' }),
|
|
||||||
headers: {
|
|
||||||
'api-key': apiConfig.geogridApiKey,
|
|
||||||
Cookie: apiConfig.geogridApiCookie
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const registros = response.data?.registros || [];
|
|
||||||
|
|
||||||
logger.info("Resposta do GeoGrid", {
|
|
||||||
totalRegistros: registros.length,
|
|
||||||
siglas: registros.map(r => r.pasta?.sigla)
|
|
||||||
});
|
|
||||||
|
|
||||||
return registros;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Classificação da sigla da pasta por convenção de PREFIXO:
|
|
||||||
// "s-..." => Sothis (rede própria)
|
|
||||||
// "p-<nome>..." => Parceiro; o provedor é o nome que vem depois do "-"
|
|
||||||
// Marcador de MEIO DE TRANSMISSÃO logo após o prefixo:
|
|
||||||
// "a" => Aéreo (sufixo " - Aéreo")
|
|
||||||
// "r" => Rádio (sufixo " - Rádio")
|
|
||||||
// "sub" => Subterrâneo (prefixa "(Subterrâneo) ")
|
|
||||||
// Exemplos: "s-Barueri" => "Sothis"; "p-Vivo" => "Vivo"; "s-sub-Barueri" => "(Subterrâneo) Sothis";
|
|
||||||
// "S-A-ABC" => "Sothis - Aéreo"; "P-R-Vivo" => "Vivo - Rádio".
|
|
||||||
// Qualquer outra coisa => não elegível (tipo "nenhum").
|
|
||||||
const classificarSigla = (sigla) => {
|
|
||||||
const partes = String(sigla || '').split('-').map(p => p.trim()).filter(Boolean);
|
|
||||||
const prefixo = (partes[0] || '').toLowerCase();
|
|
||||||
|
|
||||||
let tipo;
|
|
||||||
if (prefixo === 's') tipo = 'autorizado';
|
|
||||||
else if (prefixo === 'p') tipo = 'parceiro';
|
|
||||||
else return { tipo: 'nenhum', provedor: sigla || 'Desconhecido' };
|
|
||||||
|
|
||||||
// Marcador de meio de transmissão logo após o prefixo (partes[1]).
|
|
||||||
const marcador = (partes[1] || '').toLowerCase();
|
|
||||||
const aereo = marcador === 'a';
|
|
||||||
const radio = marcador === 'r';
|
|
||||||
const subterraneo = partes.slice(1).some(p => p.toLowerCase() === 'sub');
|
|
||||||
|
|
||||||
// Nome: Sothis é fixo; parceiro = segmentos após o prefixo, sem os marcadores.
|
|
||||||
let nome = 'Sothis';
|
|
||||||
if (tipo === 'parceiro') {
|
|
||||||
const resto = partes.slice(1).filter((p, i) => {
|
|
||||||
const pl = p.toLowerCase();
|
|
||||||
if (pl === 'sub') return false;
|
|
||||||
if (i === 0 && (pl === 'a' || pl === 'r')) return false; // marcador de meio
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
nome = resto.join(' ') || 'Parceiro';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Formatação: subterrâneo prefixa "(Subterrâneo)"; aéreo/rádio sufixam " - Meio".
|
|
||||||
let provedor = subterraneo ? `(Subterrâneo) ${nome}` : nome;
|
|
||||||
if (aereo) provedor = `${provedor} - Aéreo`;
|
|
||||||
else if (radio) provedor = `${provedor} - Rádio`;
|
|
||||||
|
|
||||||
return { tipo, provedor };
|
|
||||||
};
|
|
||||||
|
|
||||||
const ehAutorizado = (sigla) => classificarSigla(sigla).tipo === 'autorizado';
|
|
||||||
const ehParceiro = (sigla) => classificarSigla(sigla).tipo === 'parceiro';
|
|
||||||
const ehElegivel = (registro) => classificarSigla(registro && registro.pasta && registro.pasta.sigla).tipo !== 'nenhum';
|
|
||||||
const provedorDoRegistro = (registro) => classificarSigla(registro && registro.pasta && registro.pasta.sigla).provedor;
|
|
||||||
|
|
||||||
const selecionarRegistros = (registros) => {
|
|
||||||
const autorizados = registros.filter(r => ehAutorizado(r.pasta && r.pasta.sigla));
|
|
||||||
if (autorizados.length > 0) {
|
|
||||||
return { provedor: provedorDoRegistro(autorizados[0]), tipo: "autorizado", registros: autorizados };
|
|
||||||
}
|
|
||||||
|
|
||||||
const parceiros = registros.filter(r => ehParceiro(r.pasta && r.pasta.sigla));
|
|
||||||
if (parceiros.length > 0) {
|
|
||||||
return { provedor: provedorDoRegistro(parceiros[0]), tipo: "parceiro", registros: parceiros };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { provedor: "Nenhum provedor disponível", tipo: "nenhum", registros: [] };
|
|
||||||
};
|
|
||||||
|
|
||||||
// Normaliza um registro do GeoGrid para o que o core precisa: coordenadas da
|
|
||||||
// caixa (destino do trajeto) + distância em linha reta + sigla + provedor resolvido.
|
|
||||||
const mapCaixa = (registro) => {
|
|
||||||
const lat = Number(registro?.latitude);
|
|
||||||
const lon = Number(registro?.longitude);
|
|
||||||
// O GeoGrid retorna a distância como string (ex.: "395.14") — converte para número.
|
|
||||||
const dReta = Number(registro?.distancia);
|
|
||||||
return {
|
|
||||||
lat: Number.isFinite(lat) ? lat : null,
|
|
||||||
lon: Number.isFinite(lon) ? lon : null,
|
|
||||||
distanciaReta: Number.isFinite(dReta) ? dReta : null,
|
|
||||||
sigla: registro?.pasta?.sigla ?? null,
|
|
||||||
provedor: provedorDoRegistro(registro)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// NOVO: retorna as N caixas mais próximas (em linha reta) já com coordenadas,
|
|
||||||
// para que o core possa calcular a distância real via trajeto.
|
|
||||||
const consultarCaixasProximas = async (lat, lon, { topN = apiConfig.geogridTrajetoTopN, misturarProvedores = false } = {}) => {
|
|
||||||
try {
|
|
||||||
const registros = await fetchRegistros(lat, lon);
|
|
||||||
|
|
||||||
if (misturarProvedores) {
|
|
||||||
// As N caixas mais próximas dentre APENAS autorizadas (Sothis) + parceiros,
|
|
||||||
// sem priorizar entre elas; cada uma com seu provedor resolvido.
|
|
||||||
const elegiveis = registros.filter(ehElegivel);
|
|
||||||
const caixas = elegiveis.slice(0, Math.max(1, topN)).map(mapCaixa);
|
|
||||||
const provedor = caixas.length ? caixas[0].provedor : "Nenhum provedor disponível";
|
|
||||||
return { provedor, tipo: "misto", caixas };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Padrão (WordPress): prioriza rede própria (Sothis); só cai para parceiros se não houver.
|
|
||||||
const { provedor, tipo, registros: selecionados } = selecionarRegistros(registros);
|
|
||||||
const caixas = selecionados.slice(0, Math.max(1, topN)).map(mapCaixa);
|
|
||||||
return { provedor, tipo, caixas };
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Erro ao consultar caixas próximas no GeoGrid", { message: error.message, stack: error.stack, lat, lon });
|
|
||||||
throw new Error("Erro ao consultar viabilidade");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// NOVO: distância REAL de trajeto entre origem (endereço) e destino (caixa).
|
|
||||||
const consultaTrajeto = async (origem, destino, modo = apiConfig.geogridTrajetoModo) => {
|
|
||||||
const base = apiConfig.geogridApiBaseUrl;
|
|
||||||
if (!base) {
|
|
||||||
throw new Error("GEOGRID_API_BASE_URL não configurada para consulta de trajeto.");
|
|
||||||
}
|
|
||||||
const url = `${base.replace(/\/$/, '')}/integracao/trajeto/cordenada`;
|
|
||||||
const body = {
|
|
||||||
origem: { latitude: String(origem.lat), longitude: String(origem.lon) },
|
|
||||||
destino: { latitude: String(destino.lat), longitude: String(destino.lon) },
|
|
||||||
modo
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await axios.post(url, body, {
|
|
||||||
headers: {
|
|
||||||
'api-key': apiConfig.geogridApiKey,
|
|
||||||
Cookie: apiConfig.geogridApiCookie,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
timeout: 10000
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.data; // { distancia, pontosIntermediario, pontos }
|
|
||||||
};
|
|
||||||
|
|
||||||
// LEGADO: mantido com a forma de retorno original (`{ data: registro }`) para
|
|
||||||
// preservar compatibilidade. O fluxo novo usa consultarCaixasProximas + consultaTrajeto.
|
|
||||||
const consultaViabilidade = async (lat, lon) => {
|
const consultaViabilidade = async (lat, lon) => {
|
||||||
try {
|
|
||||||
const registros = await fetchRegistros(lat, lon);
|
|
||||||
const { tipo, registros: selecionados } = selecionarRegistros(registros);
|
|
||||||
|
|
||||||
if (tipo === "autorizado") {
|
const url = apiConfig.geogridApiUrl;
|
||||||
const resultado = selecionados[0];
|
const apiKey = apiConfig.geogridApiKey;
|
||||||
|
const apiCookie = apiConfig.geogridApiCookie;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Parâmetros da consulta
|
||||||
|
const params = {
|
||||||
|
raio: 5000,
|
||||||
|
latitude: lat,
|
||||||
|
longitude: lon,
|
||||||
|
"itens[]": "caixa",
|
||||||
|
ordenarCampos: ["distancia"],
|
||||||
|
ordenarPor: ["asc"],
|
||||||
|
consultarPasta: "S",
|
||||||
|
consultarIndividual: "S"
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await axios.get(url, {
|
||||||
|
params,
|
||||||
|
// força a serialização do tipo `itens[]=caixa`
|
||||||
|
paramsSerializer: p => qs.stringify(p, { arrayFormat: 'brackets' }),
|
||||||
|
headers: {
|
||||||
|
'api-key': apiKey,
|
||||||
|
Cookie: apiCookie
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extrai o primeiro registro da resposta em que a pasta.sigla seja igual as siglas autorizadas no .env
|
||||||
|
const registros = response.data?.registros || [];
|
||||||
|
|
||||||
|
logger.info("Resposta do GeoGrid", {
|
||||||
|
totalRegistros: registros.length,
|
||||||
|
siglas: registros.map(r => r.pasta?.sigla),
|
||||||
|
autorizadas: apiConfig.geogridAuthorizedSiglasPastas
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filtra registros autorizados (ex: SOTHIS, VIVO, OI)
|
||||||
|
const registros_autorizados = registros.filter(r => {
|
||||||
|
const sigla = r.pasta && r.pasta.sigla;
|
||||||
|
return apiConfig.geogridAuthorizedSiglasPastas.includes(sigla);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Se encontrar autorizado, retorna com sigla "Sothis"
|
||||||
|
if (registros_autorizados.length > 0) {
|
||||||
|
const resultado = registros_autorizados[0];
|
||||||
resultado.pasta.sigla = "Sothis";
|
resultado.pasta.sigla = "Sothis";
|
||||||
return { data: resultado };
|
return { data: resultado };
|
||||||
}
|
}
|
||||||
if (tipo === "parceiro") {
|
|
||||||
return { data: selecionados[0] };
|
// Se não encontrar autorizado, tenta buscar parceiro
|
||||||
|
const registros_parceiros = registros.filter(r => {
|
||||||
|
const sigla = r.pasta && r.pasta.sigla;
|
||||||
|
return sigla && (sigla.toLowerCase().includes("parceiro -") || sigla.toLowerCase().includes("parceiros - outros"));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (registros_parceiros.length > 0) {
|
||||||
|
return { data: registros_parceiros[0] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Se não encontrar autorizado nem parceiro, retorna mensagem padrão
|
||||||
return { data: { pasta: { sigla: "Nenhum provedor disponível" } } };
|
return { data: { pasta: { sigla: "Nenhum provedor disponível" } } };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Erro ao consultar viabilidade no GeoGrid", { message: error.message, stack: error.stack, lat, lon });
|
logger.error("Erro ao consultar viabilidade no GeoGrid", { message: error.message, stack: error.stack, lat, lon });
|
||||||
throw new Error("Erro ao consultar viabilidade");
|
throw new Error("Erro ao consultar viabilidade");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { consultaViabilidade, consultarCaixasProximas, consultaTrajeto, mapCaixa, classificarSigla };
|
module.exports = { consultaViabilidade };
|
||||||
|
|
||||||
|
|||||||
@ -5,20 +5,10 @@ dotenv.config();
|
|||||||
const googleApiKey = process.env.GOOGLE_API_KEY;
|
const googleApiKey = process.env.GOOGLE_API_KEY;
|
||||||
|
|
||||||
// Geogrid API Configs
|
// Geogrid API Configs
|
||||||
const geogridApiUrl = process.env.GEOGRID_API_URL; // URL completa da consulta por raio (inalterada)
|
const geogridApiUrl = process.env.GEOGRID_API_URL;
|
||||||
const geogridApiKey = process.env.GEOGRID_API_KEY;
|
const geogridApiKey = process.env.GEOGRID_API_KEY;
|
||||||
const geogridApiCookie = process.env.GEOGRID_API_COOKIE;
|
const geogridApiCookie = process.env.GEOGRID_API_COOKIE;
|
||||||
|
const geogridAuthorizedSiglasPastas = process.env.GEOGRID_AUTHORIZED_SIGLAS_PASTAS ? process.env.GEOGRID_AUTHORIZED_SIGLAS_PASTAS.split(',').map(s => s.trim()) : [];
|
||||||
// Base da API GeoGrid: usada para montar endpoints além da consulta (ex.: trajeto).
|
|
||||||
// Se GEOGRID_API_BASE_URL não for definida, deriva da URL de consulta removendo o sufixo /viabilidade/raio.
|
|
||||||
const geogridApiBaseUrl = process.env.GEOGRID_API_BASE_URL
|
|
||||||
|| (geogridApiUrl ? geogridApiUrl.replace(/\/viabilidade\/raio\/?$/, '') : undefined);
|
|
||||||
|
|
||||||
// Trajeto (distância real): configuráveis, com defaults seguros.
|
|
||||||
const geogridTrajetoModo = process.env.GEOGRID_TRAJETO_MODO || 'walking';
|
|
||||||
const geogridTrajetoTopN = Number(process.env.GEOGRID_TRAJETO_TOP_N) || 3; // consulta individual: mais preciso
|
|
||||||
const geogridTrajetoTopNLote = Number(process.env.GEOGRID_TRAJETO_TOP_N_LOTE) || 2; // lote/CSV: 2 caixas (colunas da planilha)
|
|
||||||
const geogridConcurrency = Number(process.env.GEOGRID_CONCURRENCY) || 5; // chamadas externas simultâneas
|
|
||||||
|
|
||||||
// Hubsoft API Configs
|
// Hubsoft API Configs
|
||||||
const hubsoftUrl = process.env.HUBSOFT_URL;
|
const hubsoftUrl = process.env.HUBSOFT_URL;
|
||||||
@ -32,13 +22,9 @@ const hubsoftGrantType = process.env.HUBSOFT_GRANT_TYPE;
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
googleApiKey: googleApiKey,
|
googleApiKey: googleApiKey,
|
||||||
geogridApiUrl: geogridApiUrl,
|
geogridApiUrl: geogridApiUrl,
|
||||||
geogridApiBaseUrl: geogridApiBaseUrl,
|
|
||||||
geogridApiKey: geogridApiKey,
|
geogridApiKey: geogridApiKey,
|
||||||
geogridApiCookie: geogridApiCookie,
|
geogridApiCookie: geogridApiCookie,
|
||||||
geogridTrajetoModo: geogridTrajetoModo,
|
geogridAuthorizedSiglasPastas: geogridAuthorizedSiglasPastas,
|
||||||
geogridTrajetoTopN: geogridTrajetoTopN,
|
|
||||||
geogridTrajetoTopNLote: geogridTrajetoTopNLote,
|
|
||||||
geogridConcurrency: geogridConcurrency,
|
|
||||||
hubsoftUrl: hubsoftUrl,
|
hubsoftUrl: hubsoftUrl,
|
||||||
|
|
||||||
hubsoftAuthUrl: hubsoftAuthUrl,
|
hubsoftAuthUrl: hubsoftAuthUrl,
|
||||||
|
|||||||
@ -1,18 +0,0 @@
|
|||||||
// Executa `fn` sobre cada item de `items` com no máximo `limit` execuções
|
|
||||||
// simultâneas, preservando a ordem dos resultados. Sem dependências externas.
|
|
||||||
async function mapWithConcurrency(items, limit, fn) {
|
|
||||||
const results = new Array(items.length);
|
|
||||||
let cursor = 0;
|
|
||||||
|
|
||||||
const workers = new Array(Math.min(Math.max(1, limit), items.length || 1)).fill(null).map(async () => {
|
|
||||||
while (cursor < items.length) {
|
|
||||||
const index = cursor++;
|
|
||||||
results[index] = await fn(items[index], index);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(workers);
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { mapWithConcurrency };
|
|
||||||
@ -1,218 +0,0 @@
|
|||||||
const test = require('node:test');
|
|
||||||
const assert = require('node:assert/strict');
|
|
||||||
|
|
||||||
// --- Mock das integrações externas (property-access em tempo de chamada => patchável) ---
|
|
||||||
const geogridService = require('../src/shared/apis/geogridService.js');
|
|
||||||
const googleService = require('../src/shared/apis/googleService.js');
|
|
||||||
|
|
||||||
// Guarda a implementação real de consultarCaixasProximas (outros testes a substituem por mock).
|
|
||||||
const realConsultarCaixasProximas = geogridService.consultarCaixasProximas;
|
|
||||||
|
|
||||||
// cep-promise é chamado como função default => substitui no cache de módulos
|
|
||||||
const cepPath = require.resolve('cep-promise');
|
|
||||||
require.cache[cepPath] = {
|
|
||||||
id: cepPath, filename: cepPath, loaded: true,
|
|
||||||
exports: async () => ({ street: 'Rua Imirim', neighborhood: 'Chácaras Marco', city: 'Barueri', state: 'SP', cep: '06419240' })
|
|
||||||
};
|
|
||||||
googleService.geocodeWithGoogle = async () => ({ lat: -12.9, lon: -46.9 });
|
|
||||||
|
|
||||||
const core = require('../src/modules/viabilidade/viabilidade.core.js');
|
|
||||||
const service = require('../src/modules/contratacao/contratacao.service.js');
|
|
||||||
|
|
||||||
function mockGeogrid({ caixas, provedor = 'Sothis', trajeto = 630 } = {}) {
|
|
||||||
geogridService.consultarCaixasProximas = async () => ({
|
|
||||||
provedor, tipo: 'autorizado',
|
|
||||||
caixas: caixas ?? [{ lat: -12.9, lon: -46.9, distanciaReta: 300, sigla: 'X', provedor: 'Sothis' }]
|
|
||||||
});
|
|
||||||
geogridService.consultaTrajeto = async () => ({ distancia: trajeto });
|
|
||||||
}
|
|
||||||
|
|
||||||
test('classificarPorDistancia aplica a regra 500/1000', () => {
|
|
||||||
assert.deepEqual(core.classificarPorDistancia(0), { dedicado: true, naoDedicado: true });
|
|
||||||
assert.deepEqual(core.classificarPorDistancia(500), { dedicado: true, naoDedicado: true });
|
|
||||||
assert.deepEqual(core.classificarPorDistancia(501), { dedicado: true, naoDedicado: false });
|
|
||||||
assert.deepEqual(core.classificarPorDistancia(1000), { dedicado: true, naoDedicado: false });
|
|
||||||
assert.deepEqual(core.classificarPorDistancia(1001), { dedicado: false, naoDedicado: false });
|
|
||||||
assert.deepEqual(core.classificarPorDistancia('5KM+'), { dedicado: false, naoDedicado: false });
|
|
||||||
assert.deepEqual(core.classificarPorDistancia(null), { dedicado: false, naoDedicado: false });
|
|
||||||
});
|
|
||||||
|
|
||||||
test('core usa a distância REAL (trajeto), não a linha reta', async () => {
|
|
||||||
mockGeogrid({ caixas: [{ lat: -12.9, lon: -46.9, distanciaReta: 300, sigla: 'X' }], trajeto: 630 });
|
|
||||||
const r = await core.avaliarPorCoordenadas({ lat: -12.9, lon: -46.9 });
|
|
||||||
assert.equal(r.distanciaReta, 300);
|
|
||||||
assert.equal(r.distanciaReal, 630);
|
|
||||||
assert.equal(r.distancia, 630); // efetiva = real
|
|
||||||
assert.equal(r.dedicado, true); // 630 <= 1000
|
|
||||||
assert.equal(r.naoDedicado, false); // 630 > 500 (a reta 300 diria true => prova que usa a real)
|
|
||||||
});
|
|
||||||
|
|
||||||
test('core pega a MENOR distância real entre as top-N caixas', async () => {
|
|
||||||
geogridService.consultarCaixasProximas = async () => ({
|
|
||||||
provedor: 'Sothis', tipo: 'autorizado',
|
|
||||||
caixas: [{ lat: 1, lon: 1, distanciaReta: 100, sigla: 'A' }, { lat: 2, lon: 2, distanciaReta: 200, sigla: 'B' }]
|
|
||||||
});
|
|
||||||
const trajetos = [800, 450];
|
|
||||||
let i = 0;
|
|
||||||
geogridService.consultaTrajeto = async () => ({ distancia: trajetos[i++] });
|
|
||||||
const r = await core.avaliarPorCoordenadas({ lat: 0, lon: 0 });
|
|
||||||
assert.equal(r.distanciaReal, 450);
|
|
||||||
assert.equal(r.naoDedicado, true); // 450 <= 500
|
|
||||||
});
|
|
||||||
|
|
||||||
test('core cai para a linha reta se o trajeto falhar', async () => {
|
|
||||||
geogridService.consultarCaixasProximas = async () => ({
|
|
||||||
provedor: 'Sothis', tipo: 'autorizado', caixas: [{ lat: 1, lon: 1, distanciaReta: 480, sigla: 'A' }]
|
|
||||||
});
|
|
||||||
geogridService.consultaTrajeto = async () => { throw new Error('trajeto indisponível'); };
|
|
||||||
const r = await core.avaliarPorCoordenadas({ lat: 0, lon: 0 });
|
|
||||||
assert.equal(r.distanciaReal, null);
|
|
||||||
assert.equal(r.distancia, 480); // fallback reta
|
|
||||||
assert.equal(r.naoDedicado, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('sem caixa no raio: mantém "5KM+" e nada viável', async () => {
|
|
||||||
geogridService.consultarCaixasProximas = async () => ({ provedor: 'Nenhum provedor disponível', tipo: 'nenhum', caixas: [] });
|
|
||||||
const r = await core.avaliarPorCoordenadas({ lat: 0, lon: 0 });
|
|
||||||
assert.equal(r.distancia, '5KM+');
|
|
||||||
assert.equal(r.dedicado, false);
|
|
||||||
assert.equal(r.naoDedicado, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- CONTRATO WordPress: a resposta precisa manter TODAS as chaves e tipos ---
|
|
||||||
const CONTRATO_VIABILIDADE = {
|
|
||||||
nome: 'string', email: 'string', telefone: 'string',
|
|
||||||
logradouro: 'string', numero: 'string', bairro: 'string',
|
|
||||||
cidade: 'string', estado: 'string', cep: 'string',
|
|
||||||
naoDedicado: 'boolean', dedicado: 'boolean',
|
|
||||||
distancia: 'number', provedor: 'string',
|
|
||||||
distanciaReta: 'number' // aditivo, não-quebra
|
|
||||||
};
|
|
||||||
|
|
||||||
test('contrato /viabilidade: chaves e tipos preservados', async () => {
|
|
||||||
mockGeogrid({ trajeto: 630 });
|
|
||||||
const resp = await service.verificarViabilidade({
|
|
||||||
nome: 'João', email: 'j@x.com', telefone: '11999999999',
|
|
||||||
cep: '06419240', numero: '303', source: 'teste' // source => não persiste no banco
|
|
||||||
});
|
|
||||||
for (const [campo, tipo] of Object.entries(CONTRATO_VIABILIDADE)) {
|
|
||||||
assert.ok(campo in resp, `campo "${campo}" sumiu do response`);
|
|
||||||
assert.equal(typeof resp[campo], tipo, `tipo de "${campo}" mudou`);
|
|
||||||
}
|
|
||||||
assert.equal(resp.distancia, 630); // distancia = real
|
|
||||||
assert.equal(resp.distanciaReta, 300);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('contrato /viabilidade/lat-long: chaves preservadas', async () => {
|
|
||||||
mockGeogrid({ trajeto: 630 });
|
|
||||||
googleService.reverseGeocodeWithGoogle = async () => 'Av. Teste, 1000 - SP';
|
|
||||||
const resp = await service.verificarViabilidadeLatLong('-12.9', '-46.9');
|
|
||||||
for (const campo of ['endereco', 'naoDedicado', 'dedicado', 'distancia', 'provedor', 'distanciaReta']) {
|
|
||||||
assert.ok(campo in resp, `campo "${campo}" faltando`);
|
|
||||||
}
|
|
||||||
assert.equal(resp.distancia, 630);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('mapCaixa converte distância/coords string do GeoGrid para número', () => {
|
|
||||||
const c = geogridService.mapCaixa({ latitude: '-23.66', longitude: '-46.59', distancia: '395.14', pasta: { sigla: 'X' } });
|
|
||||||
assert.equal(typeof c.distanciaReta, 'number');
|
|
||||||
assert.equal(c.distanciaReta, 395.14);
|
|
||||||
assert.equal(c.lat, -23.66);
|
|
||||||
assert.equal(c.lon, -46.59);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('classifica corretamente quando a distância vem como string (regressão)', async () => {
|
|
||||||
// caixa com distanciaReta numérica (já mapeada) e trajeto retornando string
|
|
||||||
geogridService.consultarCaixasProximas = async () => ({ provedor: 'Sothis', tipo: 'autorizado', caixas: [{ lat: 1, lon: 1, distanciaReta: 395.14, sigla: 'X' }] });
|
|
||||||
geogridService.consultaTrajeto = async () => ({ distancia: '420' }); // string
|
|
||||||
const r = await core.avaliarPorCoordenadas({ lat: 0, lon: 0 });
|
|
||||||
assert.equal(r.distanciaReal, 420); // convertido para número
|
|
||||||
assert.equal(r.dedicado, true); // 420 <= 500
|
|
||||||
assert.equal(r.naoDedicado, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('core retorna detalhe por caixa (caixas[]) com provedor por caixa', async () => {
|
|
||||||
geogridService.consultarCaixasProximas = async () => ({
|
|
||||||
provedor: 'Sothis', tipo: 'misto',
|
|
||||||
caixas: [
|
|
||||||
{ lat: 1, lon: 1, distanciaReta: 300, sigla: 'A', provedor: 'Sothis' },
|
|
||||||
{ lat: 2, lon: 2, distanciaReta: 700, sigla: 'B', provedor: 'Parceiro - X' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
const trajetos = [420, 900];
|
|
||||||
let i = 0;
|
|
||||||
geogridService.consultaTrajeto = async () => ({ distancia: trajetos[i++] });
|
|
||||||
const r = await core.avaliarPorCoordenadas({ lat: 0, lon: 0 });
|
|
||||||
assert.equal(r.caixas.length, 2);
|
|
||||||
assert.equal(r.caixas[0].provedor, 'Sothis');
|
|
||||||
assert.equal(r.caixas[0].distancia, 420);
|
|
||||||
assert.equal(r.caixas[0].naoDedicado, true); // 420 <= 500
|
|
||||||
assert.equal(r.caixas[1].provedor, 'Parceiro - X');
|
|
||||||
assert.equal(r.caixas[1].distancia, 900);
|
|
||||||
assert.equal(r.caixas[1].dedicado, true); // 900 <= 1000
|
|
||||||
assert.equal(r.caixas[1].naoDedicado, false); // 900 > 500
|
|
||||||
});
|
|
||||||
|
|
||||||
test('classificarSigla: convenção de prefixos s-/p-/sub', () => {
|
|
||||||
assert.deepEqual(geogridService.classificarSigla('s-Barueri'), { tipo: 'autorizado', provedor: 'Sothis' });
|
|
||||||
assert.deepEqual(geogridService.classificarSigla('p-Vivo'), { tipo: 'parceiro', provedor: 'Vivo' });
|
|
||||||
assert.deepEqual(geogridService.classificarSigla('s-sub-Barueri'), { tipo: 'autorizado', provedor: '(Subterrâneo) Sothis' });
|
|
||||||
assert.deepEqual(geogridService.classificarSigla('p-sub-Vivo'), { tipo: 'parceiro', provedor: '(Subterrâneo) Vivo' });
|
|
||||||
// meio de transmissão: aéreo / rádio (marcador logo após o prefixo)
|
|
||||||
assert.deepEqual(geogridService.classificarSigla('S-A-ABC'), { tipo: 'autorizado', provedor: 'Sothis - Aéreo' });
|
|
||||||
assert.deepEqual(geogridService.classificarSigla('P-R-Vivo'), { tipo: 'parceiro', provedor: 'Vivo - Rádio' });
|
|
||||||
assert.deepEqual(geogridService.classificarSigla('p-a-Vivo'), { tipo: 'parceiro', provedor: 'Vivo - Aéreo' });
|
|
||||||
assert.equal(geogridService.classificarSigla('Sub Alameda Santos').tipo, 'nenhum');
|
|
||||||
assert.equal(geogridService.classificarSigla('São Paulo - SP').tipo, 'nenhum'); // convenção antiga não é mais elegível
|
|
||||||
assert.equal(geogridService.classificarSigla('S-SP').provedor, 'Sothis'); // case-insensitive
|
|
||||||
});
|
|
||||||
|
|
||||||
test('seleção MISTA: só s-/p- (ignora siglas fora da convenção), com subterrâneo', async () => {
|
|
||||||
const axios = require('axios');
|
|
||||||
const orig = axios.get;
|
|
||||||
axios.get = async () => ({ data: { registros: [
|
|
||||||
{ latitude: '0', longitude: '0', distancia: '50', pasta: { sigla: 'Sub Alameda Santos' } }, // NÃO elegível (mais próxima)
|
|
||||||
{ latitude: '1', longitude: '1', distancia: '100', pasta: { sigla: 'p-Vivo' } }, // parceiro
|
|
||||||
{ latitude: '2', longitude: '2', distancia: '200', pasta: { sigla: 's-sub-Barueri' } } // Sothis subterrâneo
|
|
||||||
] } });
|
|
||||||
try {
|
|
||||||
const r = await realConsultarCaixasProximas(0, 0, { topN: 2, misturarProvedores: true });
|
|
||||||
assert.equal(r.caixas.length, 2); // sub-alameda excluída
|
|
||||||
assert.equal(r.caixas[0].provedor, 'Vivo');
|
|
||||||
assert.equal(r.caixas[1].provedor, '(Subterrâneo) Sothis');
|
|
||||||
} finally {
|
|
||||||
axios.get = orig;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('seleção PADRÃO (WordPress): prioriza Sothis e ignora parceiro mais próximo', async () => {
|
|
||||||
const axios = require('axios');
|
|
||||||
const orig = axios.get;
|
|
||||||
axios.get = async () => ({ data: { registros: [
|
|
||||||
{ latitude: '1', longitude: '1', distancia: '100', pasta: { sigla: 'p-Vivo' } },
|
|
||||||
{ latitude: '2', longitude: '2', distancia: '200', pasta: { sigla: 's-Barueri' } }
|
|
||||||
] } });
|
|
||||||
try {
|
|
||||||
const r = await realConsultarCaixasProximas(0, 0, { topN: 2 });
|
|
||||||
assert.equal(r.provedor, 'Sothis');
|
|
||||||
assert.equal(r.caixas.length, 1); // só a autorizada (Sothis)
|
|
||||||
assert.equal(r.caixas[0].provedor, 'Sothis');
|
|
||||||
} finally {
|
|
||||||
axios.get = orig;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('lote: dedup + resultado por item (inclui item inválido) e traz caixas[]', async () => {
|
|
||||||
mockGeogrid({ trajeto: 630 });
|
|
||||||
const resultados = await service.verificarViabilidadeLote([
|
|
||||||
{ latitude: '-12.9', longitude: '-46.9' },
|
|
||||||
{ latitude: '-12.9', longitude: '-46.9' }, // duplicado
|
|
||||||
{ foo: 'bar' } // inválido
|
|
||||||
], {});
|
|
||||||
assert.equal(resultados.length, 3);
|
|
||||||
assert.equal(resultados[0].distancia, 630);
|
|
||||||
assert.ok(Array.isArray(resultados[0].caixas));
|
|
||||||
assert.equal(resultados[0].caixas[0].provedor, 'Sothis');
|
|
||||||
assert.equal(resultados[1].distancia, 630);
|
|
||||||
assert.ok(resultados[2].erro);
|
|
||||||
});
|
|
||||||
Loading…
Reference in New Issue
Block a user