Pular para o conteúdo principal

Client Service

Gerencia operações relacionadas aos clientes, incluindo busca, cadastro, atualização e sincronização com o banco de dados local.

Visão Geral

O ClientService é responsável por:

  • Buscar clientes da API remota (com ou sem filtros de busca)
  • Cadastrar novos clientes
  • Atualizar clientes existentes
  • Sincronizar clientes com o banco de dados local
  • Gerenciar cache local de clientes (offline-first)
  • Filtrar clientes por diversos critérios
  • Consultar informações de CNPJ e CEP

Localização: services/client/client.service.ts

Endpoints Utilizados

Base URL

Todos os endpoints são relativos à base URL configurada em services/api.service.ts

Métodos

fetchRemote(params)

Busca clientes remotamente com paginação.

Endpoint: GET /cliente

Parâmetros:

type FetchClientsParams = {
page: number; // Número da página (inicia em 0)
size: number; // Quantidade de itens por página
// dataHora: string;
};

Retorno:

Promise<Client[]>;

Exemplo de uso:

import { clientService } from "@/services/client/client.service.instance";

const clients = await clientService.fetchRemote({
page: 0,
size: 20,
});

Tratamento de erros:

  • Lança Error se o status da resposta não for 2xx
  • Mensagem: ClientService.fetchRemote failed: {status}

fetchRemoteById(clientId)

Busca um cliente específico remotamente por ID.

Endpoint: GET /cliente/{clientId}

Parâmetros:

clientId: string; // ID do cliente

Retorno:

Promise<Client>;

Exemplo de uso:

import { clientService } from "@/services/client/client.service.instance";

const client = await clientService.fetchRemoteById("cliente-id-123");
console.log("Cliente encontrado:", client.nomeFantasia);

Tratamento de erros:

  • Lança Error se o status da resposta não for 2xx
  • Mensagem: ClientService.fetchRemoteById failed: {status}

fetchCnpjRemote(params)

Busca informações de um CNPJ remotamente.

Endpoint: GET /cliente/cnpj/{cnpj}

Parâmetros:

type FetchCnpjParams = {
cnpj: string; // CNPJ a ser consultado
uf?: string; // UF para filtrar (opcional)
};

Path Params:

  • cnpj: CNPJ a ser consultado

Query Params enviados:

  • uf: UF (opcional)

Retorno:

Promise<Cnpj>;

Exemplo de uso:

import { clientService } from "@/services/client/client.service.instance";

// Buscar informações de CNPJ
const cnpjInfo = await clientService.fetchCnpjRemote({
cnpj: "12345678000190",
});

console.log("Razão Social:", cnpjInfo.razaoSocial);
console.log("Nome Fantasia:", cnpjInfo.nomeFantasia);
console.log("Situação:", cnpjInfo.situacao);

// Buscar com filtro de UF
const cnpjInfoMG = await clientService.fetchCnpjRemote({
cnpj: "12345678000190",
uf: "MG",
});

Tratamento de erros:

  • Lança Error se o status da resposta não for 2xx
  • Mensagem: ClientService.fetchCnpjRemote failed: {status}

fetchCepRemote(cep)

Busca informações de um CEP remotamente.

Endpoint: GET /cliente/cep/{cep}

Parâmetros:

cep: string; // CEP a ser consultado

Path Params:

  • cep: CEP a ser consultado (sem máscara)

Retorno:

Promise<Cep>;

Exemplo de uso:

import { clientService } from "@/services/client/client.service.instance";

// Buscar informações de CEP
const cepInfo = await clientService.fetchCepRemote("01310100");

console.log("Logradouro:", cepInfo.logradouro);
console.log("Bairro:", cepInfo.bairro);
console.log("Cidade:", cepInfo.localidade);
console.log("UF:", cepInfo.uf);

Tratamento de erros:

  • Lança Error se o status da resposta não for 2xx
  • Mensagem: ClientService.fetchCepRemote failed: {status}

fetchSearchRemote(params)

Busca clientes remotamente com filtros de pesquisa.

Endpoint: GET /cliente/search/clientes

Parâmetros:

type FetchSearchClientsParams = {
page: number; // Número da página
size: number; // Quantidade de itens
search: string; // Termo de busca
searchByFantasyName: boolean; // Se true, busca por nomeFantasia; se false, por razaoSocial
where?: any; // Cláusula where adicional (uso interno)
};

Query Params enviados:

  • page: número da página
  • size: tamanho da página
  • nomeFantasia: termo de busca (se searchByFantasyName for true)
  • razaoSocial: termo de busca (se searchByFantasyName for false)

Retorno:

Promise<Client[]>;

Exemplo de uso:

// Buscar por nome fantasia
const clientsByFantasy = await clientService.fetchSearchRemote({
page: 0,
size: 20,
search: "Empresa XYZ",
searchByFantasyName: true,
});

// Buscar por razão social
const clientsBySocial = await clientService.fetchSearchRemote({
page: 0,
size: 20,
search: "XYZ LTDA",
searchByFantasyName: false,
});

Tratamento de erros:

  • Lança Error se o status da resposta não for 2xx
  • Mensagem: ClientService.fetchRemote failed: {status}

registerClient(clientData)

Cadastra um novo cliente.

Endpoint: POST /cliente

Parâmetros:

type DynamicClientRegisterSchema = {
// Dados da empresa
idCompany?: string | null;
idUsuario?: string | null;
idPerfil?: string | null;

// Status e tipo
prospect: "S" | "N"; // Se é prospect
ativo: "S" | "N"; // Se está ativo
bloquear: "S" | "N"; // Se está bloqueado
analise: "S" | "N"; // Se está em análise
tipoPessoa: "F" | "J"; // F = Física, J = Jurídica

// Documentos
cpf?: string | null; // CPF (obrigatório se tipoPessoa = "F")
cnpj?: string | null; // CNPJ (obrigatório se tipoPessoa = "J")
identInscEstadual?: string | null;
inscrMuni?: string | null;
sipeagro?: string | null;
codSuframa?: string | null;

// Classificações
classifCms: "C" | "I" | "P" | "R" | "T" | "X";
temIpi: "S" | "N";

// Dados básicos
razaoSocial: string; // Obrigatório
nomeFantasia: string; // Obrigatório
email: string; // Obrigatório
emailNfe: string; // Obrigatório
emailDanfe: "S" | "N";
telefone?: string | null;
observacoes?: string | null;

// Endereço principal
logradouro?: string | null;
complemento: string; // Obrigatório
codBai: number; // Código do bairro (obrigatório)
nomeBai: string; // Nome do bairro (obrigatório)
codCidade: number; // Código da cidade (obrigatório)
nomeCidade: string; // Nome da cidade (obrigatório)
nomeRegiao?: string | null;
uf: string; // Obrigatório
cep: string; // Obrigatório
pais?: string | null;

// Endereço de entrega
codEnderecoEntrega?: string | null;
numEntrega?: string | null;
logistEntrega?: string | null;
longitudeEntrega?: string | null;
latitudeEntrega?: string | null;
compleEntrega?: string | null;
cepEntrega?: string | null;
nomeCidadeEntrega?: string | null;
paisEntrega?: string | null;
nomeEndEntrega?: string | null;
nomeBaiEntrega?: string | null;

// Endereço de cobrança
codEnderecoReceb?: string | null;
telefoneReceb?: string | null;
logisticaReceb?: string | null;
nomeCidadeReceb?: string | null;
nomeBaiReceb?: string | null;
compleReceb?: string | null;
nomeEnderecReceb?: string | null;
cepReceb?: string | null;
numeroEnderecReceb?: string | null;

// Configurações comerciais
codTipParc: number; // Código do tipo de parceiro (obrigatório)
codTab?: string | null; // Código da tabela de preços
codLocalPadrao?: string | null; // Local padrão
tipoGerarBolCent: "A" | "E" | "I";
tipoGerarBoleto: "E" | "I" | "P";
tipoAnexoNfe: "X" | "Z";

// Grupos e limites
grupoAutor?: string | null;
grupoDescParc?: string | null;
limitCredMes?: number | null;
limiteCred?: number | null;

// Outros
motivoBloq?: string | null;
sugTipNegSaid?: string | null;
percDescFob?: number | null;
vendMin?: number | null;
dtVencReg?: string | null;
tpFrete?: string | null;
percentConsumidor?: number | null;

// Contatos
contatos?: ContactRegisterSchema[] | null;

// Campos dinâmicos do layout (UUID como chave)
[key: string]: any;
};

Body: JSON com os dados do cliente (conforme schema acima)

Retorno:

Promise<Client>; // Cliente criado com ID

Exemplo de uso:

const newClient = await clientService.registerClient({
tipoPessoa: "J",
cnpj: "12.345.678/0001-95",
razaoSocial: "Empresa Exemplo LTDA",
nomeFantasia: "Empresa Exemplo",
email: "contato@exemplo.com",
emailNfe: "nfe@exemplo.com",
emailDanfe: "S",
prospect: "N",
ativo: "S",
bloquear: "N",
analise: "N",
classifCms: "C",
temIpi: "S",
complemento: "Sala 101",
codBai: 123,
nomeBai: "Centro",
codCidade: 456,
nomeCidade: "São Paulo",
uf: "SP",
cep: "01000-000",
codTipParc: 1,
tipoGerarBolCent: "A",
tipoGerarBoleto: "E",
tipoAnexoNfe: "X",
contatos: [
{
nomeContato: "João Silva",
email: "joao@exemplo.com",
telefone: "(11) 98888-8888",
},
],
});

console.log("Cliente criado:", newClient.id);

Tratamento de erros:

  • Lança Error se o status da resposta não for 2xx
  • Mensagem: ClientService.registerClient failed: {status}
  • Validações do schema são realizadas antes da chamada

updateClient(clientId, updatedClientData)

Atualiza um cliente existente.

Endpoint: PUT /cliente/{clientId}

Parâmetros:

  • clientId (string): ID do cliente a ser atualizado
  • updatedClientData (Partial<DynamicClientRegisterSchema>): Dados a serem atualizados (pode ser parcial)

Body: JSON com os dados do cliente a serem atualizados

Retorno:

Promise<Client>; // Cliente atualizado

Exemplo de uso:

// Atualizar apenas alguns campos
const updatedClient = await clientService.updateClient("cliente-id-123", {
nomeFantasia: "Novo Nome Fantasia",
telefone: "(11) 99999-9999",
observacoes: "Cliente atualizado",
});

// Atualizar endereço
const updatedAddress = await clientService.updateClient("cliente-id-123", {
logradouro: "Nova Rua",
complemento: "Novo Complemento",
nomeCidade: "Nova Cidade",
uf: "SP",
cep: "02000-000",
});

Tratamento de erros:

  • Lança Error se o status da resposta não for 2xx
  • Mensagem: ClientService.updateClient failed: {status}

sync(params)

Sincroniza clientes da API remota com o banco de dados local.

Descrição: Busca clientes remotamente e salva em lote no banco local.

Parâmetros:

type FetchClientsParams = {
page: number;
size: number;
};

Retorno:

Promise<{ data: Client[] }>;

Exemplo de uso:

const result = await clientService.sync({
page: 0,
size: 100,
});

console.log(`${result.data.length} clientes sincronizados`);

findById(id)

Busca um cliente no banco de dados local por ID.

Parâmetros:

  • id (string): ID do cliente

Retorno:

Promise<Client | null>;

Exemplo de uso:

const client = await clientService.findById("cliente-id-123");

if (client) {
console.log("Cliente encontrado:", client.nomeFantasia);
} else {
console.log("Cliente não encontrado");
}

list(params)

Lista clientes do banco de dados local com opções de filtro e paginação.

Parâmetros:

{
limit?: number; // Limite de resultados
offset?: number; // Offset para paginação
orderBy?: { // Ordenação
column: any;
direction?: "ASC" | "DESC";
};
where?: any; // Cláusula where do Prisma
}

Retorno:

Promise<Client[]>;

Exemplo de uso:

// Listar primeiros 20 clientes ordenados por nome
const clients = await clientService.list({
limit: 20,
offset: 0,
orderBy: {
column: "nomeFantasia",
direction: "ASC",
},
});

// Listar clientes de uma cidade específica
const clientsByCidade = await clientService.list({
where: {
nomeCidade: "São Paulo",
},
});

searchAndList(params)

Busca e lista clientes combinando busca remota com cache local.

Descrição: Realiza busca remota se necessário e retorna resultados do banco local com filtros aplicados.

Parâmetros:

{
page: number;
size: number;
search: string;
searchByFantasyName: boolean;
letter?: string; // Filtro por letra inicial
searchScope?: "todos" | "idExterno" | "nomeFantasia"; // Escopo da busca
where?: any; // Cláusula where adicional
}

Retorno:

Promise<Client[]>;

Exemplo de uso:

// Buscar por termo geral em todos os campos
const results = await clientService.searchAndList({
page: 0,
size: 20,
search: "Silva",
searchByFantasyName: false,
searchScope: "todos",
});

// Buscar clientes que começam com letra 'A'
const clientsA = await clientService.searchAndList({
page: 0,
size: 50,
search: "",
searchByFantasyName: true,
letter: "A",
});

// Buscar por ID externo
const clientByExternal = await clientService.searchAndList({
page: 0,
size: 10,
search: "EXT-123",
searchByFantasyName: false,
searchScope: "idExterno",
});

Campos pesquisados quando searchScope = "todos":

  • nomeFantasia
  • razaoSocial
  • idExterno
  • cnpj
  • cpf
  • email
  • telefone
  • nomeCidade
  • uf

getAvailableLocations()

Obtém localizações disponíveis no banco de dados local.

Retorno:

Promise<{
cidades: string[];
bairros: string[];
regioes: string[];
}>;

Exemplo de uso:

const locations = await clientService.getAvailableLocations();

console.log("Cidades:", locations.cidades);
console.log("Bairros:", locations.bairros);
console.log("Regiões:", locations.regioes);

clearLocal()

Limpa todos os clientes do banco de dados local.

Retorno:

Promise<void>;

Exemplo de uso:

await clientService.clearLocal();
console.log("Cache de clientes limpo");

getOrganization()

Obtém e configura o contexto de organização do usuário.

Descrição: Busca os contextos disponíveis e gera token de autenticação para a primeira organização.

Endpoints utilizados:

  • POST /auth/contexts
  • POST /auth/token

Retorno:

Promise<void>;

Exemplo de uso:

await clientService.getOrganization();

Tratamento de erros:

  • Lança Error se não conseguir buscar contextos
  • Lança Error se falhar ao trocar token de autenticação

fetchClientHistory(clientId)

Busca o histórico de pedidos e itens de um cliente específico.

Endpoint: GET /cliente/{clientId}/historico-itens

Parâmetros:

clientId: string; // ID do cliente

Retorno:

Promise<ClientHistory[]>;

Exemplo de uso:

import { clientService } from "@/services/client/client.service.instance";

// Buscar histórico de pedidos do cliente
const history = await clientService.fetchClientHistory("cliente-id-123");

console.log(`Total de pedidos: ${history.length}`);

history.forEach((item) => {
console.log(`Nota: ${item.nuNota}`);
console.log(` Produto: ${item.nomeProduto}`);
console.log(` Quantidade: ${item.quantidade}`);
console.log(` Valor unitário: R$ ${item.vlrUnit.toFixed(2)}`);
console.log(` Desconto: ${item.percDesc}% (-R$ ${item.vlrDesc.toFixed(2)})`);
console.log(` Vendedor: ${item.vendedorNome}`);
console.log(` Data: ${item.dataPedido}`);
});

Tratamento de erros:

  • Lança Error se o status da resposta não for 2xx
  • Mensagem: ClientService.fetchClientHistory failed: {status}

Estruturas de Dados

Interface Cnpj

interface Cnpj {
cnpj: string;
razaoSocial: string;
nomeFantasia: string;
situacao: string;
endereco: string;
cidade: string;
uf: string;
telefone: string;
email: string;
abertura: string;
inscricaoMunicipal: string;
inscricaoEstadual: string;
}

Interface Cep

interface Cep {
cep: string;
logradouro: string;
complemento: string;
bairro: string;
localidade: string; // Cidade
uf: string;
ibge: string;
gia: string;
ddd: string;
siafi: string;
}

Interface Client

interface Client {
id: string;
idOrganization: string;
idExterno?: string;
prospect: "S" | "N";
ativo: "S" | "N";
bloquear: "S" | "N";
tipoPessoa: "F" | "J";
cpf?: string;
cnpj?: string;
razaoSocial: string;
nomeFantasia: string;
email: string;
emailNfe?: string;
telefone?: string;
nomeCidade?: string;
uf?: string;
cep?: string;
logradouro?: string;
complemento?: string;
nomeBai?: string;
nomeRegiao?: string;
// ... outros campos
}

Interface ClientHistory

interface ClientHistory {
nuNota: string; // Número da nota/pedido
dataPedido: string; // Data do pedido (ISO string)
vendedorNome: string; // Nome do vendedor
idProduto: string; // ID do produto
idExternoProduto: string; // ID externo do produto
nomeProduto: string; // Nome do produto
quantidade: number; // Quantidade vendida
percDesc: number; // Percentual de desconto
vlrDesc: number; // Valor do desconto
vlrUnit: number; // Valor unitário
}

Interface PageResponse

interface PageResponse<T> {
content: T[]; // Array de itens da página
page?: number; // Número da página atual
size?: number; // Tamanho da página
totalElements?: number; // Total de elementos
}

Componentes Relacionados

ClientRepository

Repositório para acesso ao banco de dados local (SQLite).

Localização: repositories/client/client.repository.ts

Métodos utilizados:

  • saveBatch(clients): Salva clientes em lote
  • findById(id): Busca por ID
  • findMany(params): Busca com filtros
  • clearAll(): Limpa todos os registros
  • getAvailableLocations(): Obtém localizações únicas

buildQueryParams Helper

Função utilitária que converte objeto de parâmetros em URLSearchParams.

Localização: helpers/buildQueryParams.ts


Fluxo de Integração

Fluxo de Cadastro de Cliente

1. App chama registerClient() com dados do formulário
2. Validação do schema (Zod) é aplicada
3. POST /cliente é enviado para a API
4. API valida e cria o cliente
5. Cliente criado é retornado com ID
6. App pode sincronizar para cache local se necessário

Fluxo de Atualização de Cliente

1. App chama updateClient() com ID e dados atualizados
2. PUT /cliente/{id} é enviado para a API
3. API valida e atualiza o cliente
4. Cliente atualizado é retornado
5. Cache local pode ser atualizado posteriormente

Fluxo de Sincronização (Offline-First)

1. App chama sync() periodicamente
2. fetchRemote() busca clientes da API
3. Clientes são salvos no banco local via saveBatch()
4. Cache local está atualizado
5. App pode trabalhar offline usando list() e findById()

Fluxo de Busca Híbrida

1. App chama searchAndList() com termo de busca
2. Se necessário, busca remota é realizada (fetchSearchRemote)
3. Resultados são salvos no cache local
4. Filtros são aplicados no banco local
5. Resultados filtrados são retornados

Testes

Localização

services/client/client.service.test.ts

Executar Testes

# Teste específico do serviço
npx jest services/client/client.service.test.ts

# Modo watch
npx jest services/client/client.service.test.ts --watch

# Com coverage
npx jest services/client/client.service.test.ts --coverage

Casos de Teste

  • fetchRemote: busca com paginação
  • fetchRemoteById: busca de cliente específico por ID
  • fetchCnpjRemote: busca de informações de CNPJ
  • fetchCnpjRemote: tratamento de erros para status não-2xx
  • fetchCepRemote: busca de informações de CEP
  • fetchCepRemote: tratamento de erros para status não-2xx
  • fetchSearchRemote: busca com filtros
  • fetchClientHistory: busca histórico de pedidos do cliente
  • fetchClientHistory: tratamento de erros para status não-2xx
  • registerClient: cadastro de novo cliente
  • updateClient: atualização de cliente existente
  • sync: sincronização com banco local
  • ✅ Tratamento de erros para todos os métodos

Mocks

Localização

__mocks__/client.mock.ts

Dados Disponíveis

  • clientResponseMock: Cliente completo de exemplo
  • clientRegisterMock: Dados de cadastro de cliente
  • cnpjInfoMock: Informações de CNPJ de exemplo
  • cepInfoMock: Informações de CEP de exemplo
  • clientHistoryMock: Histórico de pedidos e itens de cliente
  • Arrays de clientes para testes de listagem

clientHistoryMock

Estrutura de exemplo com histórico de 5 pedidos:

export const clientHistoryMock = {
content: [
{
nuNota: "0001",
dataPedido: "2026-05-01",
vendedorNome: "Carlos Silva",
idProduto: "P001",
idExternoProduto: "EXT-001",
nomeProduto: "Notebook Dell Inspiron",
quantidade: 2,
percDesc: 10,
vlrDesc: 500,
vlrUnit: 4500,
},
{
nuNota: "0002",
dataPedido: "2026-05-03",
vendedorNome: "Ana Souza",
idProduto: "P002",
idExternoProduto: "EXT-002",
nomeProduto: "Smartphone Samsung Galaxy",
quantidade: 1,
percDesc: 5,
vlrDesc: 100,
vlrUnit: 2000,
},
{
nuNota: "0003",
dataPedido: "2026-05-05",
vendedorNome: "João Pereira",
idProduto: "P003",
idExternoProduto: "EXT-003",
nomeProduto: "Monitor LG 27''",
quantidade: 3,
percDesc: 15,
vlrDesc: 600,
vlrUnit: 1800,
},
{
nuNota: "0004",
dataPedido: "2026-05-10",
vendedorNome: "Mariana Costa",
idProduto: "P004",
idExternoProduto: "EXT-004",
nomeProduto: "Teclado Mecânico Logitech",
quantidade: 5,
percDesc: 20,
vlrDesc: 400,
vlrUnit: 800,
},
{
nuNota: "0005",
dataPedido: "2026-05-15",
vendedorNome: "Ricardo Almeida",
idProduto: "P005",
idExternoProduto: "EXT-005",
nomeProduto: "Mouse Gamer Razer",
quantidade: 4,
percDesc: 12,
vlrDesc: 240,
vlrUnit: 600,
},
],
pageable: {
pageNumber: 0,
pageSize: 10,
sort: {
empty: true,
sorted: false,
unsorted: true,
},
offset: 0,
paged: true,
unpaged: false,
},
last: false,
totalPages: 6,
totalElements: 51,
size: 10,
number: 0,
sort: {
empty: true,
sorted: false,
unsorted: true,
},
first: true,
numberOfElements: 10,
empty: false,
};

Exemplo de uso em testes:

import {
clientRegisterMock,
clientResponseMock,
cnpjInfoMock,
cepInfoMock,
clientHistoryMock,
} from "@/__mocks__/client.mock";

// Mock da API para registerClient
const apiMock = {
post: jest.fn(async () => ({ status: 200, data: clientResponseMock })),
};

const service = new ClientService({ apiWithoutAccessToken: apiMock });
const result = await service.registerClient(clientRegisterMock);

// Mock da API para fetchClientHistory
const apiHistoryMock = {
get: jest.fn(async () => ({ status: 200, data: clientHistoryMock })),
};

const serviceHistory = new ClientService({
apiWithoutAccessToken: apiHistoryMock,
repo: { saveBatch: jest.fn() } as any,
});

const history = await serviceHistory.fetchClientHistory("cliente-123");

expect(history).toEqual(clientHistoryMock.content);
expect(history).toHaveLength(5);
expect(history[0]).toHaveProperty("nuNota");
expect(history[0]).toHaveProperty("nomeProduto");