feat: implement barcode search configuration in 'Almoxarifado', integrating multiple external APIs for enhanced product information retrieval and improving user experience with new modals for data handling
This commit is contained in:
196
packages/backend/convex/actions/README_BUSCAR_PRODUTO.md
Normal file
196
packages/backend/convex/actions/README_BUSCAR_PRODUTO.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# Configuração de APIs para Busca de Produtos por Código de Barras
|
||||
|
||||
Este documento descreve como configurar as variáveis de ambiente necessárias para as diferentes APIs de busca de produtos.
|
||||
|
||||
## Fontes de Dados Disponíveis
|
||||
|
||||
O sistema busca produtos em 5 fontes diferentes, em ordem de prioridade:
|
||||
|
||||
1. **GS1 Brasil** (Prioridade 1 - requer credenciais)
|
||||
2. **Bluesoft Cosmo** (Prioridade 2 - requer credenciais)
|
||||
3. **Product-Search.net** (Prioridade 3 - pode requerer credenciais)
|
||||
4. **Busca Unificada** (Prioridade 4 - **GRATUITA, sem credenciais**)
|
||||
5. **Open Food Facts** (Prioridade 5 - **GRATUITA, sem credenciais**)
|
||||
|
||||
## Configuração das APIs
|
||||
|
||||
### 1. GS1 Brasil (API Verified by GS1)
|
||||
|
||||
A GS1 Brasil oferece a API "Verified by GS1" para consulta de produtos cadastrados.
|
||||
|
||||
#### Passos para obter credenciais:
|
||||
|
||||
1. Acesse o portal: https://apicnp.gs1br.org
|
||||
2. Faça login ou crie uma conta
|
||||
3. No menu "Apps", cadastre uma nova aplicação
|
||||
4. Obtenha o `Client_ID` e `Client_Secret`
|
||||
|
||||
#### Configuração no Convex:
|
||||
|
||||
```bash
|
||||
# Client ID da aplicação registrada
|
||||
npx convex env set GS1_BRASIL_CLIENT_ID "seu-client-id-aqui"
|
||||
|
||||
# Client Secret da aplicação
|
||||
npx convex env set GS1_BRASIL_CLIENT_SECRET "seu-client-secret-aqui"
|
||||
|
||||
# URL do token (opcional, padrão já configurado)
|
||||
npx convex env set GS1_BRASIL_TOKEN_URL "https://apicnp.gs1br.org/oauth/token"
|
||||
|
||||
# URL da API (opcional, padrão já configurado)
|
||||
npx convex env set GS1_BRASIL_API_URL "https://apicnp.gs1br.org/api/v1/products"
|
||||
```
|
||||
|
||||
#### Documentação:
|
||||
- Manual: https://www.gs1br.org/educacao-e-eventos/Documents/Manual%20do%20Usu%C3%A1rio%20-%20API%20Verified%20by%20GS1_v.03.pdf
|
||||
- Portal: https://apicnp.gs1br.org
|
||||
|
||||
---
|
||||
|
||||
### 2. Bluesoft Cosmos
|
||||
|
||||
A Bluesoft oferece uma API REST para consulta de produtos por código de barras (GTIN/EAN).
|
||||
|
||||
#### Passos para obter credenciais:
|
||||
|
||||
1. Acesse: https://api.cosmos.bluesoft.com.br/api
|
||||
2. Faça login na plataforma
|
||||
3. Obtenha seu token de API (X-Cosmos-Token) e User-Agent
|
||||
4. Ambos estarão disponíveis na página após fazer login
|
||||
|
||||
#### Configuração no Convex:
|
||||
|
||||
```bash
|
||||
# Token de autenticação da Bluesoft (X-Cosmos-Token)
|
||||
npx convex env set BLUESOFT_API_KEY "seu-token-aqui"
|
||||
|
||||
# URL da API (opcional, padrão já configurado)
|
||||
npx convex env set BLUESOFT_API_URL "https://api.cosmos.bluesoft.com.br"
|
||||
```
|
||||
|
||||
**Importante**: O sistema usa automaticamente o endpoint correto `/gtins/{codigo}.json` conforme a documentação oficial.
|
||||
|
||||
#### Endpoint:
|
||||
- **GET** `/gtins/{código}.json` - Recupera detalhes do produto através do GTIN/EAN informado
|
||||
|
||||
#### Estrutura de Resposta:
|
||||
A API retorna informações incluindo:
|
||||
- `description`: Nome/descrição do produto
|
||||
- `brand.name`: Nome da marca
|
||||
- `gpc.description`: Categoria do produto
|
||||
- `ncm.code` e `ncm.description`: Código NCM e descrição
|
||||
- `thumbnail`: URL da imagem do produto
|
||||
- `price`: Preço formatado
|
||||
- `gross_weight` e `net_weight`: Peso bruto e líquido
|
||||
|
||||
#### Documentação:
|
||||
- **Documentação Oficial**: https://api.cosmos.bluesoft.com.br/api
|
||||
- Central de Ajuda: https://ajuda.bluesoft.com.br/
|
||||
- Suporte: suporte.api@bluesoft.com.br
|
||||
|
||||
---
|
||||
|
||||
### 3. Product-Search.net
|
||||
|
||||
Plataforma que fornece informações sobre produtos com base em códigos de barras.
|
||||
|
||||
#### Passos para obter credenciais (opcional):
|
||||
|
||||
1. Acesse: https://product-search.net
|
||||
2. Crie uma conta (se necessário)
|
||||
3. Obtenha sua API key (se disponível no seu plano)
|
||||
|
||||
#### Configuração no Convex:
|
||||
|
||||
```bash
|
||||
# API Key (opcional - pode funcionar sem autenticação dependendo do plano)
|
||||
npx convex env set PRODUCT_SEARCH_API_KEY "sua-api-key-aqui"
|
||||
|
||||
# URL da API (opcional, padrão já configurado)
|
||||
npx convex env set PRODUCT_SEARCH_API_URL "https://api.product-search.net/v1/products"
|
||||
```
|
||||
|
||||
#### Documentação:
|
||||
- Site: https://product-search.net
|
||||
|
||||
---
|
||||
|
||||
### 4. Busca Unificada
|
||||
|
||||
API gratuita e pública para busca de produtos por código de barras. **Não requer configuração ou credenciais.**
|
||||
|
||||
#### Características:
|
||||
|
||||
- ✅ Totalmente gratuita para uso pessoal e comercial
|
||||
- ✅ Sem necessidade de cadastro ou credenciais
|
||||
- ✅ Sempre disponível
|
||||
- ✅ Foco em produtos brasileiros
|
||||
|
||||
#### Documentação:
|
||||
- Site: https://api-produtos.seunegocionanuvem.com.br
|
||||
|
||||
---
|
||||
|
||||
### 5. Open Food Facts
|
||||
|
||||
API gratuita e pública, não requer configuração. Sempre disponível como fallback.
|
||||
|
||||
#### Características:
|
||||
|
||||
- ✅ Totalmente gratuita e colaborativa
|
||||
- ✅ Sem necessidade de cadastro ou credenciais
|
||||
- ✅ Foco em produtos alimentícios
|
||||
- ✅ Base de dados colaborativa internacional
|
||||
|
||||
---
|
||||
|
||||
## Ordem de Prioridade
|
||||
|
||||
O sistema tenta buscar em todas as fontes em paralelo e retorna o primeiro resultado válido encontrado, seguindo esta ordem de prioridade:
|
||||
|
||||
1. **GS1 Brasil** (requer credenciais)
|
||||
2. **Bluesoft Cosmo** (requer credenciais)
|
||||
3. **Product-Search.net** (pode requerer credenciais)
|
||||
4. **Busca Unificada** (gratuita, sem credenciais) ⭐
|
||||
5. **Open Food Facts** (gratuita, sem credenciais) ⭐
|
||||
|
||||
> ⭐ **Nota**: As APIs gratuitas (Busca Unificada e Open Food Facts) funcionam sem nenhuma configuração adicional e estão sempre disponíveis.
|
||||
|
||||
## Verificação da Configuração
|
||||
|
||||
Para verificar se as variáveis de ambiente estão configuradas:
|
||||
|
||||
```bash
|
||||
npx convex env list
|
||||
```
|
||||
|
||||
## Notas Importantes
|
||||
|
||||
1. **Limites de Uso**: Cada API pode ter limites de requisições por período. Consulte a documentação de cada serviço.
|
||||
|
||||
2. **Custos**: Algumas APIs podem ter custos associados. Verifique os planos disponíveis.
|
||||
|
||||
3. **Segurança**: As credenciais são armazenadas de forma segura nas variáveis de ambiente do Convex e nunca são expostas ao frontend.
|
||||
|
||||
4. **Fallback**: Se uma API falhar ou não estiver configurada, o sistema automaticamente tenta as outras fontes. As APIs gratuitas (Busca Unificada e Open Food Facts) sempre estarão disponíveis mesmo sem configuração.
|
||||
|
||||
5. **Timeout**: Todas as requisições têm timeout de 5 segundos para evitar travamentos.
|
||||
|
||||
## Testando a Configuração
|
||||
|
||||
Após configurar as variáveis de ambiente, teste a busca com um código de barras conhecido:
|
||||
|
||||
1. Acesse a página de cadastro de materiais
|
||||
2. Digite ou escaneie um código de barras
|
||||
3. O sistema deve buscar em todas as fontes configuradas
|
||||
4. O modal mostrará a fonte dos dados encontrados
|
||||
|
||||
## Suporte
|
||||
|
||||
Em caso de problemas:
|
||||
|
||||
1. Verifique se as credenciais estão corretas
|
||||
2. Verifique se as URLs dos endpoints estão corretas
|
||||
3. Consulte os logs do Convex para erros específicos
|
||||
4. Verifique a documentação oficial de cada API
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import { action } from '../_generated/server';
|
||||
import { v } from 'convex/values';
|
||||
import { api, internal } from '../_generated/api';
|
||||
import { decryptSMTPPassword } from '../auth/utils';
|
||||
|
||||
interface OpenFoodFactsProduct {
|
||||
product?: {
|
||||
@@ -22,6 +24,71 @@ interface OpenFoodFactsProduct {
|
||||
status_verbose?: string;
|
||||
}
|
||||
|
||||
interface GS1BrasilProduct {
|
||||
gtin?: string;
|
||||
description?: string;
|
||||
brand?: string;
|
||||
brandName?: string;
|
||||
category?: string;
|
||||
categoryName?: string;
|
||||
imageUrl?: string;
|
||||
image?: string;
|
||||
ncm?: string;
|
||||
unidadeMedida?: string;
|
||||
pesoBruto?: string;
|
||||
}
|
||||
|
||||
interface BluesoftProduct {
|
||||
gtin?: number | string;
|
||||
description?: string;
|
||||
brand?: {
|
||||
name?: string;
|
||||
picture?: string;
|
||||
};
|
||||
gpc?: {
|
||||
code?: string;
|
||||
description?: string;
|
||||
};
|
||||
ncm?: {
|
||||
code?: string;
|
||||
description?: string;
|
||||
full_description?: string;
|
||||
};
|
||||
thumbnail?: string;
|
||||
price?: string;
|
||||
avg_price?: number;
|
||||
max_price?: number;
|
||||
gross_weight?: number;
|
||||
net_weight?: number;
|
||||
height?: number;
|
||||
width?: number;
|
||||
length?: number;
|
||||
}
|
||||
|
||||
interface ProductSearchProduct {
|
||||
gtin?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
brand?: string;
|
||||
category?: string;
|
||||
imageUrl?: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
interface BuscaUnificadaProduct {
|
||||
produto?: {
|
||||
nome?: string;
|
||||
descricao?: string;
|
||||
categoria?: string;
|
||||
marca?: string;
|
||||
imagem?: string;
|
||||
imagemUrl?: string;
|
||||
ean?: string;
|
||||
};
|
||||
sucesso?: boolean;
|
||||
mensagem?: string;
|
||||
}
|
||||
|
||||
interface ProductInfo {
|
||||
nome?: string;
|
||||
descricao?: string;
|
||||
@@ -30,13 +97,36 @@ interface ProductInfo {
|
||||
marca?: string;
|
||||
quantidade?: string;
|
||||
embalagem?: string;
|
||||
fonte?: string;
|
||||
}
|
||||
|
||||
interface GS1TokenResponse {
|
||||
access_token?: string;
|
||||
token_type?: string;
|
||||
expires_in?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca informações de produto via API externa (Open Food Facts)
|
||||
* Esta é uma funcionalidade opcional que pode ser usada para preencher
|
||||
* automaticamente informações de produtos quando disponível.
|
||||
* Busca informações de produto via múltiplas APIs externas
|
||||
* Fontes: Open Food Facts, Busca Unificada, GS1 Brasil, Bluesoft Cosmo, Product-Search.net
|
||||
*
|
||||
* A busca é feita em paralelo em todas as fontes disponíveis e retorna
|
||||
* o primeiro resultado válido encontrado.
|
||||
*/
|
||||
interface ConfigBuscaCodigoBarras {
|
||||
gs1BrasilClientId?: string;
|
||||
gs1BrasilClientSecret?: string;
|
||||
gs1BrasilTokenUrl?: string;
|
||||
gs1BrasilApiUrl?: string;
|
||||
gs1BrasilAtivo: boolean;
|
||||
bluesoftApiKey?: string;
|
||||
bluesoftApiUrl?: string;
|
||||
bluesoftAtivo: boolean;
|
||||
productSearchApiKey?: string;
|
||||
productSearchApiUrl?: string;
|
||||
productSearchAtivo: boolean;
|
||||
}
|
||||
|
||||
export const buscarInfoProdutoPorCodigoBarras = action({
|
||||
args: {
|
||||
codigoBarras: v.string()
|
||||
@@ -49,72 +139,484 @@ export const buscarInfoProdutoPorCodigoBarras = action({
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Tentar buscar na API Open Food Facts (gratuita, sem autenticação)
|
||||
const response = await fetch(
|
||||
`https://world.openfoodfacts.org/api/v0/product/${codigoBarras}.json`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'User-Agent': 'SGSE-App/1.0 (Almoxarifado)'
|
||||
},
|
||||
signal: AbortSignal.timeout(5000) // Timeout de 5 segundos
|
||||
// Obter configurações do banco de dados
|
||||
const configDb = await ctx.runQuery(
|
||||
internal.configuracaoBuscaCodigoBarras.obterConfigBuscaCodigoBarrasInternal
|
||||
);
|
||||
|
||||
// Preparar configurações (com descriptografia de credenciais)
|
||||
let config: ConfigBuscaCodigoBarras = {
|
||||
gs1BrasilAtivo: false,
|
||||
bluesoftAtivo: false,
|
||||
productSearchAtivo: false
|
||||
};
|
||||
|
||||
if (configDb) {
|
||||
// Descriptografar credenciais se existirem
|
||||
let gs1BrasilClientSecret: string | undefined;
|
||||
if (configDb.gs1BrasilClientSecret) {
|
||||
try {
|
||||
gs1BrasilClientSecret = await decryptSMTPPassword(configDb.gs1BrasilClientSecret);
|
||||
} catch (error) {
|
||||
console.error('Erro ao descriptografar GS1 Client Secret:', error);
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = (await response.json()) as OpenFoodFactsProduct;
|
||||
|
||||
if (data.status !== 1 || !data.product) {
|
||||
return null;
|
||||
let bluesoftApiKey: string | undefined;
|
||||
if (configDb.bluesoftApiKey) {
|
||||
try {
|
||||
bluesoftApiKey = await decryptSMTPPassword(configDb.bluesoftApiKey);
|
||||
} catch (error) {
|
||||
console.error('Erro ao descriptografar Bluesoft API Key:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const product = data.product;
|
||||
|
||||
// Extrair categoria (primeira categoria disponível)
|
||||
let categoria: string | undefined;
|
||||
if (product.categories_tags && product.categories_tags.length > 0) {
|
||||
// Pegar a primeira categoria e limpar tags
|
||||
const primeiraCategoria = product.categories_tags[0];
|
||||
categoria = primeiraCategoria
|
||||
.replace(/^pt:/, '')
|
||||
.replace(/^en:/, '')
|
||||
.replace(/-/g, ' ')
|
||||
.split(' ')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
} else if (product.categories) {
|
||||
categoria = product.categories.split(',')[0].trim();
|
||||
let productSearchApiKey: string | undefined;
|
||||
if (configDb.productSearchApiKey) {
|
||||
try {
|
||||
productSearchApiKey = await decryptSMTPPassword(configDb.productSearchApiKey);
|
||||
} catch (error) {
|
||||
console.error('Erro ao descriptografar Product-Search API Key:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const info: ProductInfo = {
|
||||
nome: product.product_name_pt || product.product_name || undefined,
|
||||
descricao: product.generic_name_pt || product.generic_name || undefined,
|
||||
categoria,
|
||||
imagemUrl:
|
||||
product.image_front_url ||
|
||||
product.image_url ||
|
||||
product.image_front_small_url ||
|
||||
undefined,
|
||||
marca: product.brands || undefined,
|
||||
quantidade: product.quantity || undefined,
|
||||
embalagem: product.packaging || undefined
|
||||
config = {
|
||||
gs1BrasilClientId: configDb.gs1BrasilClientId,
|
||||
gs1BrasilClientSecret,
|
||||
gs1BrasilTokenUrl: configDb.gs1BrasilTokenUrl,
|
||||
gs1BrasilApiUrl: configDb.gs1BrasilApiUrl,
|
||||
gs1BrasilAtivo: configDb.gs1BrasilAtivo,
|
||||
bluesoftApiKey,
|
||||
bluesoftApiUrl: configDb.bluesoftApiUrl,
|
||||
bluesoftAtivo: configDb.bluesoftAtivo,
|
||||
productSearchApiKey,
|
||||
productSearchApiUrl: configDb.productSearchApiUrl,
|
||||
productSearchAtivo: configDb.productSearchAtivo
|
||||
};
|
||||
|
||||
// Retornar apenas se tiver pelo menos nome ou descrição
|
||||
if (info.nome || info.descricao) {
|
||||
return info;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
// Log do erro mas não falhar a operação
|
||||
console.error('Erro ao buscar informações do produto:', error);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Tentar buscar em todas as fontes em paralelo
|
||||
// Usamos Promise.allSettled para não falhar se uma fonte falhar
|
||||
const resultados = await Promise.allSettled([
|
||||
buscarOpenFoodFacts(codigoBarras), // 0
|
||||
buscarBuscaUnificada(codigoBarras), // 1 - Gratuita, sem credenciais
|
||||
buscarGS1Brasil(codigoBarras, config), // 2
|
||||
buscarBluesoftCosmo(codigoBarras, config), // 3
|
||||
buscarProductSearch(codigoBarras, config) // 4
|
||||
]);
|
||||
|
||||
// Processar resultados e retornar o primeiro que tiver dados válidos
|
||||
// Prioridade: GS1 Brasil > Bluesoft > Product-Search > Busca Unificada > Open Food Facts
|
||||
const ordemPrioridade = [2, 3, 4, 1, 0]; // Índices das fontes por prioridade
|
||||
|
||||
for (const indice of ordemPrioridade) {
|
||||
const resultado = resultados[indice];
|
||||
if (resultado.status === 'fulfilled' && resultado.value) {
|
||||
return resultado.value;
|
||||
}
|
||||
}
|
||||
|
||||
// Se nenhuma fonte retornou dados, retornar null
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Busca na API de Produtos por Código de Barras (Busca Unificada)
|
||||
* Gratuita, sem necessidade de credenciais
|
||||
* Site: https://api-produtos.seunegocionanuvem.com.br
|
||||
*/
|
||||
async function buscarBuscaUnificada(codigoBarras: string): Promise<ProductInfo | null> {
|
||||
try {
|
||||
const apiUrl = 'https://api-produtos.seunegocionanuvem.com.br/api/produtos';
|
||||
const response = await fetch(`${apiUrl}?ean=${codigoBarras}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'SGSE-App/1.0 (Almoxarifado)'
|
||||
},
|
||||
signal: AbortSignal.timeout(5000)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = (await response.json()) as BuscaUnificadaProduct;
|
||||
|
||||
if (!data.produto || (!data.produto.nome && !data.produto.descricao)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const produto = data.produto;
|
||||
|
||||
const info: ProductInfo = {
|
||||
nome: produto.nome || undefined,
|
||||
descricao: produto.descricao || undefined,
|
||||
categoria: produto.categoria || undefined,
|
||||
imagemUrl: produto.imagemUrl || produto.imagem || undefined,
|
||||
marca: produto.marca || undefined,
|
||||
fonte: 'Busca Unificada'
|
||||
};
|
||||
|
||||
if (info.nome || info.descricao) {
|
||||
return info;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar Busca Unificada:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca na Open Food Facts (gratuita, sem autenticação)
|
||||
*/
|
||||
async function buscarOpenFoodFacts(codigoBarras: string): Promise<ProductInfo | null> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://world.openfoodfacts.org/api/v0/product/${codigoBarras}.json`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'User-Agent': 'SGSE-App/1.0 (Almoxarifado)'
|
||||
},
|
||||
signal: AbortSignal.timeout(5000)
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = (await response.json()) as OpenFoodFactsProduct;
|
||||
|
||||
if (data.status !== 1 || !data.product) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const product = data.product;
|
||||
|
||||
// Extrair categoria
|
||||
let categoria: string | undefined;
|
||||
if (product.categories_tags && product.categories_tags.length > 0) {
|
||||
const primeiraCategoria = product.categories_tags[0];
|
||||
categoria = primeiraCategoria
|
||||
.replace(/^pt:/, '')
|
||||
.replace(/^en:/, '')
|
||||
.replace(/-/g, ' ')
|
||||
.split(' ')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
} else if (product.categories) {
|
||||
categoria = product.categories.split(',')[0].trim();
|
||||
}
|
||||
|
||||
const info: ProductInfo = {
|
||||
nome: product.product_name_pt || product.product_name || undefined,
|
||||
descricao: product.generic_name_pt || product.generic_name || undefined,
|
||||
categoria,
|
||||
imagemUrl:
|
||||
product.image_front_url || product.image_url || product.image_front_small_url || undefined,
|
||||
marca: product.brands || undefined,
|
||||
quantidade: product.quantity || undefined,
|
||||
embalagem: product.packaging || undefined,
|
||||
fonte: 'Open Food Facts'
|
||||
};
|
||||
|
||||
if (info.nome || info.descricao) {
|
||||
return info;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar Open Food Facts:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém token de autenticação da GS1 Brasil
|
||||
*/
|
||||
async function obterTokenGS1Brasil(config: ConfigBuscaCodigoBarras): Promise<string | null> {
|
||||
try {
|
||||
// Priorizar configurações do banco, fallback para variáveis de ambiente
|
||||
const clientId = config.gs1BrasilClientId || process.env.GS1_BRASIL_CLIENT_ID;
|
||||
const clientSecret = config.gs1BrasilClientSecret || process.env.GS1_BRASIL_CLIENT_SECRET;
|
||||
const tokenUrl =
|
||||
config.gs1BrasilTokenUrl ||
|
||||
process.env.GS1_BRASIL_TOKEN_URL ||
|
||||
'https://apicnp.gs1br.org/oauth/token';
|
||||
|
||||
if (!clientId || !clientSecret) {
|
||||
console.warn('GS1 Brasil: Credenciais não configuradas');
|
||||
return null;
|
||||
}
|
||||
|
||||
const response = await fetch(tokenUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'client_credentials',
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret
|
||||
}),
|
||||
signal: AbortSignal.timeout(5000)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('GS1 Brasil: Erro ao obter token', response.status);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = (await response.json()) as GS1TokenResponse;
|
||||
return data.access_token || null;
|
||||
} catch (error) {
|
||||
console.error('GS1 Brasil: Erro ao obter token:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca na GS1 Brasil (API Verified by GS1)
|
||||
*/
|
||||
async function buscarGS1Brasil(
|
||||
codigoBarras: string,
|
||||
config: ConfigBuscaCodigoBarras
|
||||
): Promise<ProductInfo | null> {
|
||||
try {
|
||||
if (!config.gs1BrasilAtivo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const token = await obterTokenGS1Brasil(config);
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const apiUrl =
|
||||
config.gs1BrasilApiUrl ||
|
||||
process.env.GS1_BRASIL_API_URL ||
|
||||
'https://apicnp.gs1br.org/api/v1/products';
|
||||
const response = await fetch(`${apiUrl}/${codigoBarras}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'SGSE-App/1.0 (Almoxarifado)'
|
||||
},
|
||||
signal: AbortSignal.timeout(5000)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return null; // Produto não encontrado
|
||||
}
|
||||
console.error('GS1 Brasil: Erro na consulta', response.status);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = (await response.json()) as GS1BrasilProduct;
|
||||
|
||||
if (!data.gtin && !data.description) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const info: ProductInfo = {
|
||||
nome: data.description || undefined,
|
||||
descricao: data.description || undefined,
|
||||
categoria: data.categoryName || data.category || undefined,
|
||||
imagemUrl: data.imageUrl || data.image || undefined,
|
||||
marca: data.brandName || data.brand || undefined,
|
||||
quantidade: data.unidadeMedida || undefined,
|
||||
fonte: 'GS1 Brasil'
|
||||
};
|
||||
|
||||
if (info.nome || info.descricao) {
|
||||
return info;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar GS1 Brasil:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca na Bluesoft Cosmos
|
||||
* Documentação oficial: https://api.cosmos.bluesoft.com.br/api
|
||||
* Endpoint: GET /gtins/{código}.json
|
||||
*/
|
||||
async function buscarBluesoftCosmo(
|
||||
codigoBarras: string,
|
||||
config: ConfigBuscaCodigoBarras
|
||||
): Promise<ProductInfo | null> {
|
||||
try {
|
||||
if (!config.bluesoftAtivo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Priorizar configurações do banco, fallback para variáveis de ambiente
|
||||
const apiKey = config.bluesoftApiKey || process.env.BLUESOFT_API_KEY;
|
||||
|
||||
// Endpoint base da API - pode ser configurado, mas o padrão é o oficial
|
||||
const apiBaseUrl =
|
||||
config.bluesoftApiUrl ||
|
||||
process.env.BLUESOFT_API_URL ||
|
||||
'https://api.cosmos.bluesoft.com.br';
|
||||
|
||||
if (!apiKey) {
|
||||
console.warn('Bluesoft Cosmos: API Key não configurada');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Endpoint correto conforme documentação: /gtins/{codigo}.json
|
||||
const url = `${apiBaseUrl.replace(/\/$/, '')}/gtins/${codigoBarras}.json`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Cosmos-Token': apiKey,
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'SGSE-App/1.0 (Almoxarifado)' // User-Agent é obrigatório
|
||||
},
|
||||
signal: AbortSignal.timeout(10000) // 10 segundos de timeout
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => 'Não foi possível ler resposta');
|
||||
|
||||
if (response.status === 404) {
|
||||
// Produto não encontrado é normal, não é erro
|
||||
return null;
|
||||
}
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
console.error('Bluesoft Cosmos: Erro de autenticação. Verifique a API Key e User-Agent.');
|
||||
console.error('Bluesoft Cosmos: Detalhes do erro', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
body: errorText.substring(0, 500)
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
if (response.status === 429) {
|
||||
console.error('Bluesoft Cosmos: Limite de requisições excedido (HTTP 429)');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.error('Bluesoft Cosmos: Erro na consulta', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
url,
|
||||
body: errorText.substring(0, 500)
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = (await response.json()) as BluesoftProduct;
|
||||
|
||||
// Validar se temos dados mínimos
|
||||
if (!data.gtin && !data.description) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Mapear campos conforme estrutura da API
|
||||
const info: ProductInfo = {
|
||||
nome: data.description || undefined,
|
||||
descricao: data.description || undefined,
|
||||
categoria: data.gpc?.description || data.ncm?.description || undefined,
|
||||
imagemUrl: data.thumbnail || undefined,
|
||||
marca: data.brand?.name || undefined,
|
||||
quantidade: data.net_weight ? `${data.net_weight}g` : data.gross_weight ? `${data.gross_weight}g` : undefined,
|
||||
fonte: 'Bluesoft Cosmos'
|
||||
};
|
||||
|
||||
if (info.nome || info.descricao) {
|
||||
return info;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Bluesoft Cosmos: Erro ao buscar:', error);
|
||||
if (error instanceof Error) {
|
||||
console.error('Bluesoft Cosmos: Detalhes do erro:', error.message);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca no Product-Search.net
|
||||
* Pode funcionar com ou sem API key dependendo do plano
|
||||
*/
|
||||
async function buscarProductSearch(
|
||||
codigoBarras: string,
|
||||
config: ConfigBuscaCodigoBarras
|
||||
): Promise<ProductInfo | null> {
|
||||
try {
|
||||
if (!config.productSearchAtivo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Priorizar configurações do banco, fallback para variáveis de ambiente
|
||||
const apiKey = config.productSearchApiKey || process.env.PRODUCT_SEARCH_API_KEY;
|
||||
const apiUrl =
|
||||
config.productSearchApiUrl ||
|
||||
process.env.PRODUCT_SEARCH_API_URL ||
|
||||
'https://api.product-search.net/v1/products';
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'SGSE-App/1.0 (Almoxarifado)'
|
||||
};
|
||||
|
||||
// Adicionar API key se disponível
|
||||
if (apiKey) {
|
||||
headers['Authorization'] = `Bearer ${apiKey}`;
|
||||
}
|
||||
|
||||
const response = await fetch(`${apiUrl}/${codigoBarras}`, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
signal: AbortSignal.timeout(5000)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return null; // Produto não encontrado
|
||||
}
|
||||
console.error('Product-Search.net: Erro na consulta', response.status);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = (await response.json()) as ProductSearchProduct;
|
||||
|
||||
if (!data.gtin && !data.name && !data.description) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const info: ProductInfo = {
|
||||
nome: data.name || data.description || undefined,
|
||||
descricao: data.description || undefined,
|
||||
categoria: data.category || undefined,
|
||||
imagemUrl: data.imageUrl || data.image || undefined,
|
||||
marca: data.brand || undefined,
|
||||
fonte: 'Product-Search.net'
|
||||
};
|
||||
|
||||
if (info.nome || info.descricao) {
|
||||
return info;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar Product-Search.net:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user