iluria-sdk
Version:
SDK oficial do Iluria para integração com lojas e criação de frontends customizados
418 lines (368 loc) • 12.7 kB
JavaScript
const GraphQLQueryBuilder = require('./graphql-builder');
const CacheManager = require('./cache');
const { NoOpCache } = require('./cache');
/**
* Promotion module - Gerencia promoções via GraphQL
* Updated: 2025-09-08 - Fixed GraphQL types for PromotionStatus and PromotionOrderBy
*/
class Promotion {
constructor(httpClient, cacheManager = null) {
this.http = httpClient;
// Usa o cache manager fornecido ou cria um padrão
if (cacheManager === false) {
this.cache = new NoOpCache();
} else if (cacheManager) {
this.cache = cacheManager;
} else {
// Mantém compatibilidade: cria cache simples se não fornecido
this.cache = new CacheManager({
maxMemorySize: 50,
ttl: 60000,
storage: false // Apenas memória por padrão
});
}
// TTL padrão para cache de promoções
this.cacheTTL = 300000; // 5 minutos cache para promoções
}
/**
* Lista promoções com filtros e campos customizados
* @param {object} options - Opções de filtro (limit, offset, status, orderBy)
* @param {Array<string>} fields - Campos a buscar para cada promoção
* @returns {object} - Lista de promoções com paginação
*/
async list(options = {}, fields = null) {
// Validação de parâmetros para evitar erros
const safeOptions = {
limit: Math.max(1, Math.min(100, options.limit || 10)), // Entre 1-100
offset: Math.max(0, options.offset || 0),
status: options.status || 'ACTIVE',
orderBy: options.orderBy || 'NEWEST'
};
// Campos padrão se não especificados
if (!fields) {
fields = [
'id',
'name',
'type',
'subType',
'discountType',
'discountValue',
'minRequirementType',
'minValue',
'minQuantity',
'targetConfig',
'targetProduct',
'triggerType',
'scope',
'description',
'startDate',
'defineStartDate',
'defineEndDate',
'endDate',
'active',
'combinePromos',
'firstPurchase',
'createdAt',
'updatedAt'
];
}
// Chave de cache baseada nos parâmetros
const cacheKey = `promotions_list_${JSON.stringify(safeOptions)}_${JSON.stringify(fields)}`;
try {
// Verifica cache primeiro
const cached = await this.cache.get(cacheKey);
if (cached) {
return cached;
}
// Monta query GraphQL
const query = `
query ($promotionsLimit: Int, $promotionsOffset: Int, $promotionsStatus: PromotionStatus, $promotionsOrderBy: PromotionOrderBy) {
promotions(limit: $promotionsLimit, offset: $promotionsOffset, status: $promotionsStatus, orderBy: $promotionsOrderBy) {
items {
${fields.join('\n ')}
}
totalCount
hasMore
offset
limit
}
}
`;
const variables = {
promotionsLimit: safeOptions.limit,
promotionsOffset: safeOptions.offset,
promotionsStatus: safeOptions.status,
promotionsOrderBy: safeOptions.orderBy
};
// Log para debug - tipos atualizados para PromotionStatus e PromotionOrderBy
console.log('🔍 GraphQL Query (updated types):', query);
console.log('🔍 GraphQL Variables:', JSON.stringify(variables, null, 2));
const data = await this.http.graphql(query, variables);
// Processa e valida dados retornados
let result = [];
if (data && data.promotions) {
if (data.promotions.items) {
// Formato com paginação
result = {
items: this.processPromotions(data.promotions.items),
totalCount: data.promotions.totalCount || 0,
hasMore: data.promotions.hasMore || false,
offset: data.promotions.offset || 0,
limit: data.promotions.limit || safeOptions.limit
};
} else if (Array.isArray(data.promotions)) {
// Fallback para array direto
result = this.processPromotions(data.promotions);
} else {
result = [];
}
}
// Salva no cache
await this.cache.set(cacheKey, result, this.cacheTTL);
return result;
} catch (error) {
console.error('Erro ao buscar promoções:', error);
// Retorna estrutura padrão em caso de erro
if (options.limit !== undefined) {
return {
items: [],
totalCount: 0,
hasMore: false,
offset: safeOptions.offset,
limit: safeOptions.limit,
error: 'Erro ao carregar promoções'
};
}
return [];
}
}
/**
* Busca uma promoção específica por ID
* @param {string} id - ID da promoção
* @param {Array<string>} fields - Campos a buscar
* @returns {object} - Dados da promoção
*/
async get(id, fields = null) {
// Validação do ID
if (!id || typeof id !== 'string') {
throw new Error('ID da promoção é obrigatório e deve ser uma string');
}
// Campos padrão se não especificados
if (!fields) {
fields = [
'id',
'name',
'type',
'subType',
'discountType',
'discountValue',
'minRequirementType',
'minValue',
'minQuantity',
'targetConfig',
'targetProduct',
'triggerType',
'scope',
'description',
'startDate',
'defineStartDate',
'defineEndDate',
'endDate',
'active',
'combinePromos',
'firstPurchase',
'createdAt',
'updatedAt'
];
}
// Chave de cache
const cacheKey = `promotion_${id}_${JSON.stringify(fields)}`;
try {
// Verifica cache primeiro
const cached = await this.cache.get(cacheKey);
if (cached) {
return cached;
}
// Monta query GraphQL
const query = `
query Promotion($id: ID!) {
promotion(id: $id) {
${fields.join('\n ')}
}
}
`;
const variables = { id };
const data = await this.http.graphql(query, variables);
// Processa dados retornados
let result = null;
if (data && data.promotion) {
result = this.processPromotion(data.promotion);
}
// Salva no cache se encontrou a promoção
if (result) {
await this.cache.set(cacheKey, result, this.cacheTTL);
}
return result;
} catch (error) {
console.error(`Erro ao buscar promoção ${id}:`, error);
return null;
}
}
/**
* Busca promoções ativas aplicáveis para um produto específico
* @param {string} productId - ID do produto
* @param {object} options - Opções adicionais
* @returns {Array} - Lista de promoções aplicáveis
*/
async getForProduct(productId, options = {}) {
if (!productId) {
throw new Error('ID do produto é obrigatório');
}
const safeOptions = {
limit: options.limit || 10,
offset: options.offset || 0
};
try {
const query = `
query PromotionsForProduct($productId: ID!, $limit: Int, $offset: Int) {
promotionsForProduct(productId: $productId, limit: $limit, offset: $offset) {
id
name
type
discountType
discountValue
description
startDate
endDate
active
}
}
`;
const variables = {
productId,
limit: safeOptions.limit,
offset: safeOptions.offset
};
const data = await this.http.graphql(query, variables);
if (data && data.promotionsForProduct) {
return this.processPromotions(data.promotionsForProduct);
}
return [];
} catch (error) {
console.error(`Erro ao buscar promoções para produto ${productId}:`, error);
return [];
}
}
/**
* Processa uma lista de promoções para garantir dados válidos
* @param {Array} promotions - Lista de promoções
* @returns {Array} - Lista processada
*/
processPromotions(promotions) {
if (!Array.isArray(promotions)) {
return [];
}
return promotions.map(promotion => this.processPromotion(promotion));
}
/**
* Processa uma promoção individual para garantir dados válidos
* @param {object} promotion - Dados da promoção
* @returns {object} - Promoção processada
*/
processPromotion(promotion) {
if (!promotion || typeof promotion !== 'object') {
return null;
}
// Garantir que campos obrigatórios existam e tratamento de valores nulos
const processed = {
id: promotion.id || null,
name: promotion.name || 'Promoção sem nome',
type: promotion.type || null,
subType: promotion.subType || null,
discountType: promotion.discountType || null,
discountValue: this.safeNumericValue(promotion.discountValue, 0),
minRequirementType: promotion.minRequirementType || null,
minValue: this.safeNumericValue(promotion.minValue, 0),
minQuantity: this.safeNumericValue(promotion.minQuantity, 0),
targetConfig: promotion.targetConfig || null,
targetProduct: promotion.targetProduct || null,
triggerType: promotion.triggerType || null,
scope: promotion.scope || null,
description: promotion.description || '',
startDate: promotion.startDate || null,
defineStartDate: promotion.defineStartDate || false,
defineEndDate: promotion.defineEndDate || false,
endDate: promotion.endDate || null,
active: promotion.active !== undefined ? promotion.active : true,
combinePromos: promotion.combinePromos !== undefined ? promotion.combinePromos : false,
firstPurchase: promotion.firstPurchase !== undefined ? promotion.firstPurchase : false,
createdAt: promotion.createdAt || null,
updatedAt: promotion.updatedAt || null
};
// Adiciona métodos auxiliares
processed.isActive = () => {
if (!processed.active) return false;
const now = new Date();
// Verifica data de início se definida
if (processed.defineStartDate && processed.startDate) {
const startDate = new Date(processed.startDate);
if (now < startDate) return false;
}
// Verifica data de fim se definida
if (processed.defineEndDate && processed.endDate) {
const endDate = new Date(processed.endDate);
if (now > endDate) return false;
}
return true;
};
processed.getDiscountText = () => {
if (!processed.discountType || processed.discountValue === 0) {
return 'Sem desconto';
}
switch (processed.discountType) {
case 'PERCENTAGE':
return `${processed.discountValue}% OFF`;
case 'FIXED_AMOUNT':
return `R$ ${this.formatPrice(processed.discountValue)} OFF`;
case 'FREE_SHIPPING':
return 'Frete Grátis';
default:
return 'Desconto especial';
}
};
return processed;
}
/**
* Converte valor para número seguro, tratando null/undefined
* @param {any} value - Valor a converter
* @param {number} defaultValue - Valor padrão se conversão falhar
* @returns {number} - Valor numérico seguro
*/
safeNumericValue(value, defaultValue = 0) {
if (value === null || value === undefined) {
return defaultValue;
}
const num = parseFloat(value);
return isNaN(num) ? defaultValue : num;
}
/**
* Formata preço para exibição
* @param {number} price - Preço a formatar
* @returns {string} - Preço formatado
*/
formatPrice(price) {
if (!price || isNaN(price)) return '0,00';
return price.toFixed(2).replace('.', ',');
}
/**
* Limpa cache de promoções
*/
async clearCache() {
try {
await this.cache.clear();
} catch (error) {
console.error('Erro ao limpar cache de promoções:', error);
}
}
}
module.exports = Promotion;