Pricing Agent

CSuite Pricing Agent - Documentação Completa

📋 Índice

  1. Visão Geral
  2. Arquitetura
  3. Integração com CSuite Pricing
  4. Cálculo Interno do Preço
  5. Fatores Calculados Internamente - Explicação para Leigos
  6. API e Payloads
  7. Como Usar a Resposta do Agent
  8. Módulos e Responsabilidades
  9. Fluxo de Decisão
  10. Políticas e Validações
  11. Testes
  12. Exemplos de Uso
  13. Troubleshooting

Visão Geral

O CSuite Pricing Agent é um agente autônomo de precificação que integra com o schema csuite_pricing para tomar decisões de preço baseadas em políticas governamentais, respeitando:

Características Principais


Arquitetura

Estrutura de Módulos

agents/pricing/
├── __init__.py          # Configuração do agente
├── main.py              # FastAPI entrypoint
├── decision.py          # Lógica principal de decisão
├── context.py           # Construção de contexto enriquecido
├── engine.py            # Integração com motor SQL
├── anchor.py            # Lógica de Cliente Âncora
├── repository.py        # Acesso a dados do schema
├── database.py          # Gerenciamento de sessões DB
├── policies.py          # Validação de políticas
├── audit.py             # Registro de auditoria
└── execution.py         # Execução de ações

Fluxo de Dados

Payload  Context Builder  Decision Engine  Policy Validation  Audit  Execution

Implementação (observações do código)


Integração com CSuite Pricing

O Pricing Agent está totalmente integrado com o schema csuite_pricing, utilizando:

Tabelas Principais

Stored Procedures


Cálculo Interno do Preço

O cálculo do preço final é realizado pela stored procedure sp_price_compute_v8, que considera múltiplos fatores para determinar o desconto e o preço final. Esta seção detalha todos os fatores considerados.

Parâmetros de Entrada

A stored procedure recebe os seguintes parâmetros:

Parâmetro Tipo Descrição Origem
p_brand_id BIGINT ID da marca Payload
p_customer_id BIGINT ID do cliente (opcional) Payload
p_sku_id BIGINT ID do SKU Payload
p_PT DECIMAL Preço de Tela (Price Screen) csuite_pricing.price_screen
p_PISO DECIMAL Preço Piso (Price Floor) csuite_pricing.price_floor
p_machine_curve CHAR(1) Curva ABC do produto (A-E) Payload
p_sku_qty INT Quantidade do SKU no pedido Payload
p_order_value DECIMAL Valor total do pedido Payload

Fatores Calculados Internamente

A stored procedure calcula os seguintes fatores em sequência:


📚 Fatores Calculados Internamente - Explicação para Leigos

Esta seção explica, de forma simples e sem jargões técnicos, como o sistema calcula o preço final de um produto. Imagine que você está negociando um preço com um cliente - o sistema faz isso automaticamente, considerando vários fatores importantes.

🎯 Como Funciona o Cálculo de Preço

O sistema funciona como um vendedor experiente que analisa várias informações antes de oferecer um desconto. O cálculo segue uma ordem de prioridade:

Ordem de Prioridade:
1. Cliente Âncora? → Se sim, usa preço âncora (EXIT)
2. Produto em Promoção? → Se sim, usa preço de promoção (EXIT)
3. Cálculo Normal → Analisa vários fatores para calcular desconto

Fatores do Cálculo Normal:
1. Quem é o cliente? (Contexto de Mercado)
2. Quanto ele compra? (Volume de Compras)
3. Qual o nível dele? (Tier - Nível de Cliente)
4. Qual a importância da marca? (Papel da Marca)
5. Qual a importância do produto? (Curva ABC)
6. Qual o nível de estoque? (Stock Level)
7. Qual o valor do pedido? (Order Value Factor)
8. Quanto ele pagou antes? (Last Paid Price) 🆕
9. Qual o desconto permitido? (Cálculo Final)

Vamos entender cada um desses fatores:


1. 📍 Contexto de Mercado (Market Context)

O que é?
O sistema verifica se o cliente é do mercado "street" (varejo direto) ou "non_street" (atacado/distribuição).

Por que importa?
- Mercado Street (Varejo): Clientes que vendem diretamente ao consumidor final. Para proteger margens, o desconto máximo é limitado a 12%, mesmo que o cliente seja muito importante.
- Mercado Non-Street (Atacado): Clientes que compram em grandes volumes para revender. Podem receber descontos maiores conforme seu volume de compras.

Exemplo prático:
- Cliente A: Loja de rua que vende diretamente ao consumidor → Street → Desconto máximo: 12%
- Cliente B: Distribuidor que compra grandes volumes → Non-Street → Pode receber descontos maiores

Como funciona no sistema:
O sistema consulta uma tabela que classifica cada cliente. Se não encontrar informação, assume "non_street" por padrão.


2. 💰 Volume de Compras (Volume 12 Meses)

O que é?
A soma de tudo que o cliente comprou nos últimos 12 meses.

Por que importa?
Clientes que compram mais merecem melhores condições. É como um programa de fidelidade: quanto mais você compra, mais benefícios recebe.

Exemplo prático:
- Cliente A: Comprou R$ 50.000 nos últimos 12 meses
- Cliente B: Comprou R$ 500.000 nos últimos 12 meses
- Cliente C: Comprou R$ 2.000.000 nos últimos 12 meses

O Cliente C, que compra muito mais, terá acesso a descontos melhores.

Como funciona no sistema:
O sistema soma todas as compras do cliente dos últimos 12 meses. Se o cliente é novo ou não tem histórico, assume R$ 0,00.


3. 🏆 Tier (Nível do Cliente)

O que é?
Uma classificação do cliente baseada no volume de compras. É como categorias de um programa de fidelidade: Bronze, Prata, Ouro, Platina.

Por que importa?
Cada nível (Tier) tem regras diferentes de desconto. Quanto maior o tier, melhores as condições.

Exemplo prático:
- Tier V1: Clientes que compram até R$ 100.000/ano → Desconto base: 5%
- Tier V2: Clientes que compram entre R$ 100.000 e R$ 500.000/ano → Desconto base: 10%
- Tier V3: Clientes que compram entre R$ 500.000 e R$ 1.000.000/ano → Desconto base: 15%
- Tier V4: Clientes que compram mais de R$ 1.000.000/ano → Desconto base: 20%

Como funciona no sistema:
O sistema compara o volume de compras do cliente com faixas pré-definidas e determina qual tier ele pertence. Se não encontrar, assume Tier V1 (menor desconto).


4. 🎯 Papel da Marca (Brand Role)

O que é?
A importância estratégica da marca para a empresa. Algumas marcas são mais importantes que outras.

Por que importa?
Marcas mais importantes podem ter políticas de desconto diferentes. É como dar mais atenção a um produto "carro-chefe" da empresa.

Exemplo prático:
- Marca Principal (Primary Target): Marca estratégica, queremos vender mais → Descontos maiores
- Marca Secundária (Secondary Target): Marca importante, mas não prioritária → Descontos menores

Como funciona no sistema:
O sistema consulta uma tabela que define o papel de cada marca. Se não encontrar, assume "secondary_target" (marca secundária).


5. 📊 Desconto por Tier + Papel da Marca

O que é?
O desconto base que o cliente pode receber, combinando seu Tier com o Papel da Marca.

Por que importa?
Um cliente Tier V3 comprando uma marca principal pode receber um desconto maior do que um cliente Tier V3 comprando uma marca secundária.

Exemplo prático:
- Cliente Tier V2 + Marca Principal → Desconto: 12%
- Cliente Tier V2 + Marca Secundária → Desconto: 8%

Como funciona no sistema:
O sistema consulta uma tabela que combina Tier e Papel da Marca para determinar o desconto base. Se não encontrar, assume 0% (sem desconto).


6. 🛡️ Limite para Mercado Street (Cap Street)

O que é?
Uma proteção para o mercado varejo: mesmo que o cliente seja muito importante, o desconto máximo é limitado a 12%.

Por que importa?
Protege as margens no mercado varejo, onde os preços precisam ser mais estáveis.

Exemplo prático:
- Cliente Street, Tier V4 (que normalmente teria 20% de desconto) → Desconto limitado a 12%
- Cliente Non-Street, Tier V4 → Desconto completo de 20%

Como funciona no sistema:
Se o cliente é "street", o sistema compara o desconto calculado com 12% e usa o menor valor. Se não é "street", usa o desconto completo.


7. 🏷️ Preços Fixos por Cliente (Prioridade Máxima) 🆕

O que é?
Permite que clientes específicos tenham preços fixos acordados para determinados produtos (SKUs). Estes preços têm validade mensal e auto-renovação.

Por que importa?
- Contratos: Garante preços acordados em contratos de longo prazo.
- Fidelização: Oferece condições especiais estáveis para clientes estratégicos.
- Governança: Flag de revisão automática quando o dólar ou custos sobem.

Como funciona:
1. O sistema busca primeiro se existe um preço fixo ativo para o cliente e produto solicitado.
2. Se existir, este preço é usado imediatamente, ignorando todas as outras regras (Tier, Curva, Estoque, etc).
3. O preço fixo ainda é validado para garantir que não está abaixo do piso técnico (Price Floor).


8. 🎁 Promoções (Nova Camada)

O que é?
Alguns produtos entram em promoção esporadicamente. Quando um produto está em promoção, o sistema usa o preço de promoção como Preço Candidato, ignorando todo o cálculo normal.

Por que importa?
Promoções são usadas para escoar estoque, atrair clientes ou promover produtos específicos. Quando uma promoção está ativa, ela tem prioridade sobre o cálculo normal de preço.

Exemplo prático:
- Produto A: Preço normal calculado: R$ 3.000,00
- Produto A em promoção: Preço de promoção: R$ 2.500,00
- Resultado: Preço Candidato = R$ 2.500,00 (preço de promoção)

Como funciona no sistema:
1. O sistema verifica se o produto tem uma promoção ativa (dentro do período de validade)
2. Se houver promoção, usa o preço de promoção como Preço Candidato
3. O preço de promoção é validado para respeitar limites (PT e Piso)
4. Se o preço de promoção estiver abaixo do piso, é ajustado para o piso
5. Se o preço de promoção estiver acima do PT, é ajustado para o PT

Tabela de Promoções:
As promoções são armazenadas na tabela pricing_promotions (esporádicas) e pricing_active_promotions (automáticas por segmento).

Promoções Automáticas por Segmento:
- Segmentos: Máquinas, Rolamentos, Peças, Autopeças, Motopeças.
- Lógica: Identifica produtos com estoque alto/muito alto e gera ondas promocionais a cada 2 dias.
- Duração: Geralmente 48 horas para incentivar giro rápido.

Ordem de Prioridade no Sistema:
1. Cliente Âncora (maior prioridade)
2. Promoção Ativa (segunda prioridade)
3. Cálculo Normal (terceira prioridade)
4. Desconto por Quantidade (aplicado após cálculo normal)


8. 📊 Descontos por Quantidade (Nova Camada)

O que é?
Alguns produtos (especialmente da Curva A) têm preços diferentes baseados na quantidade comprada. Quanto mais unidades o cliente compra, menor o preço unitário.

Por que importa?
Descontos por quantidade incentivam compras em maior volume, melhorando o giro de estoque e aumentando o ticket médio das vendas.

Exemplo prático:
- Produto SKU 1980206 (Curva A):
- QTY 1-2: R$ 2.610,00 (preço base)
- QTY 3-4: R$ 2.500,00 (desconto por volume)
- QTY 5-9: R$ 2.450,00 (desconto maior)
- QTY 10+: R$ 2.400,00 (desconto máximo)

Como funciona no sistema:
1. O sistema executa o cálculo base via motor SQL (sp_price_compute_v8) para obter PT, Piso, contexto e o payment_term_discount.
2. Em seguida, verifica se existe uma regra especial (Combo/Bundle, desconto por quantidade por SKU ou por família).
3. Se existir uma dessas regras, ela sobrepõe o desconto normal do motor:
- A base de cálculo passa a ser o PT.
- Aplica-se o desconto do combo/quantidade (ou preço fixo).
- Aplica-se apenas o desconto de prazo de pagamento (payment_term_discount).
- Todos os outros descontos (Tier, Brand Role, Curve, Stock, Order Value) são ignorados.
4. O preço final é validado para respeitar limites (PT e Piso), e em seguida pode ser ajustado por LPP/lançamento/proteção regional.

Tabela de Descontos por Quantidade:
As regras são armazenadas na tabela pricing_quantity_discounts com:
- SKU do produto
- Faixa de quantidade (min_quantity, max_quantity)
- Preço fixo OU desconto percentual
- Descrição da regra
- Prioridade (se houver múltiplas regras)

Campos importantes (coluna product_family):
- product_family: identifica uma família de produtos (ex.: códigos de família como B9000). Quando preenchido, a regra aplica-se à família inteira — o agente tentará agregar quantidades do pedido/cart para calcular se a faixa de família é atendida. Se product_family for NULL a regra aplica-se apenas ao sku_id.
- min_quantity / max_quantity: faixas inclusivas usadas para determinar aplicação. max_quantity pode ser NULL para representar 'infinito' (ex.: 10+).
- price: preço fixo por unidade (se fornecido) — substitui cálculo normal.
- discount_pct: desconto percentual sobre o PT (se fornecido) — aplicado quando não há price.
- is_active: flag que habilita/desabilita a regra em tempo de execução.
- priority: ordem de preferência quando múltiplas regras se sobrepõem (maior valor = maior prioridade).

Observação de comportamento:
- Regras por sku_id têm precedência sobre regras por product_family quando ambas aplicam; se não houver regra por SKU, o agente tenta a regra por família agregando quantidades do order_items ou do cart/query.
- Exemplo visto no sistema: algumas linhas têm product_family = 'B9000' (aplicam por família) enquanto outras possuem product_family = NULL (aplicam por SKU). Veja a lista de faixas (1-2, 3-4, 5-9, 10+) usada no front-end.

Exemplos de consultas úteis

-- Listar regras ativas de desconto por SKU/família
SELECT
  id,
  sku_id,
  product_family,
  min_quantity,
  max_quantity,
  price,
  discount_pct,
  description,
  is_active,
  priority,
  created_by,
  created_at,
  updated_at
FROM csuite_pricing.pricing_quantity_discounts
WHERE is_active = 1
ORDER BY
  COALESCE(sku_id, 999999999),
  COALESCE(product_family, ''),
  priority DESC,
  min_quantity;

-- Buscar regras aplicáveis para um SKU específico (ex.: sku_id = 1980206)
SELECT *
FROM csuite_pricing.pricing_quantity_discounts qd
WHERE qd.is_active = 1
  AND (
    qd.sku_id = 1980206
    OR (qd.product_family IS NOT NULL AND qd.product_family = (
      SELECT product_family FROM csuite_pricing.product_classification pc WHERE pc.sku_id = 1980206 LIMIT 1
    ))
  )
ORDER BY qd.priority DESC, qd.min_quantity;

-- Agregar quantidade total por família a partir de um payload/order (exemplo usando uma tabela temporária 'order_items')
SELECT
  qd.product_family,
  SUM(oi.quantity) AS total_family_qty
FROM order_items oi
JOIN csuite_pricing.product_classification pc ON pc.sku_id = oi.sku_id
JOIN csuite_pricing.pricing_quantity_discounts qd ON qd.product_family = pc.product_family
WHERE oi.order_id = 12345
GROUP BY qd.product_family;

Exemplo em Python (usando funções do repositório)

from agents.pricing import repository

def find_applicable_quantity_discount(sku_id: int, quantity: int, brand_id: int | None = None):
  """Retorna a regra de desconto aplicável (por SKU ou por família).

  - Tenta primeiro regra por SKU (`get_quantity_discount`).
  - Se não houver, tenta regra por família (`get_family_quantity_discount`) usando `get_product_family`.
  """
  # 1) Regra específica por SKU
  sku_rule = repository.get_quantity_discount(sku_id=sku_id, quantity=quantity, brand_id=brand_id)
  if sku_rule:
    return sku_rule

  # 2) Fallback: regra por família
  product_family = repository.get_product_family(sku_id)
  if not product_family:
    return None

  # Neste exemplo assumimos `quantity` como total da família; em produção agregue do payload/cart
  family_rule = repository.get_family_quantity_discount(product_family, quantity)
  return family_rule

# Uso de exemplo
if __name__ == "__main__":
  rule = find_applicable_quantity_discount(1980206, 5)
  if rule:
    print("Regra aplicável:", rule)
  else:
    print("Nenhuma regra de desconto por quantidade encontrada")

Quando é aplicado:
- Aplicado após o motor SQL, mas sobrepõe os descontos do motor (base = PT)
- Aplicado antes das validações finais de limites (PT e Piso)
- Se o preço após desconto por quantidade estiver abaixo do piso, é ajustado para o piso
- Se o preço após desconto por quantidade estiver acima do PT, é ajustado para o PT

Exemplo de fluxo:
1. Cálculo normal: Preço = R$ 2.800,00
2. Cliente compra QTY 5
3. Sistema encontra regra: QTY 5-9 = R$ 2.450,00
4. Preço Final = R$ 2.450,00 (desconto por quantidade aplicado)


9. ⭐ Fator da Curva ABC (Curve Factor)

O que é?
Um multiplicador baseado na importância do produto. Produtos são classificados em curvas A, B, C, D ou E.

Por que importa?
Produtos mais importantes (curva A) podem ter descontos ajustados de forma diferente de produtos menos importantes (curva E).

Como funciona a Curva ABC:
- Curva A: Produtos mais vendidos (top 80% das vendas) → Fator: 1.2 (aumenta desconto em 20%)
- Curva B: Produtos médios (próximos 15%) → Fator: 1.0 (sem ajuste)
- Curva C: Produtos menos vendidos (últimos 5%) → Fator: 0.8 (reduz desconto em 20%)
- Curva D: Produtos sem vendas, mas com estoque → Fator: 0.5 (reduz desconto em 50%)
- Curva E: Produtos sem vendas e sem estoque → Fator: 0.3 (reduz desconto em 70%)

Exemplo prático:
- Desconto base: 15%
- Produto Curva A (fator 1.2) → Desconto final: 15% × 1.2 = 18%
- Produto Curva C (fator 0.8) → Desconto final: 15% × 0.8 = 12%

Como funciona no sistema:
O sistema consulta uma tabela que define o fator para cada curva. Se não encontrar, assume fator 1.0 (sem ajuste).


10. 📦 Fator do Nível de Estoque (Stock Level Factor)

O que é?
Um multiplicador baseado na situação de estoque do produto. Produtos são classificados em níveis: baixo, normal ou alto.

Por que importa?
- Estoque Alto: Queremos escoar estoque parado → Aumenta desconto
- Estoque Normal: Situação equilibrada → Sem ajuste
- Estoque Baixo: Queremos preservar estoque → Reduz desconto

Como funciona:
- Estoque Alto (high): Fator: 1.2 (aumenta desconto em 20% para escoar)
- Estoque Normal (normal): Fator: 1.0 (sem ajuste)
- Estoque Baixo (low): Fator: 0.8 (reduz desconto em 20% para preservar)

Exemplo prático:
- Desconto após curva: 18%
- Produto com Estoque Alto (fator 1.2) → Desconto final: 18% × 1.2 = 21.6%
- Produto com Estoque Baixo (fator 0.8) → Desconto final: 18% × 0.8 = 14.4%

Como funciona no sistema:
O sistema consulta uma tabela que define o fator para cada nível de estoque. Se não encontrar, assume fator 1.0 (sem ajuste).


11. 💰 Fator do Valor do Pedido (Order Value Factor) 🆕

O que é?
Um multiplicador baseado no valor total do pedido. Pedidos maiores podem receber descontos adicionais.

Por que importa?
- Pedidos de alto valor: Incentiva pedidos maiores oferecendo descontos extras
- Fidelização: Clientes que fazem pedidos grandes são recompensados
- Ticket médio: Aumenta o valor médio dos pedidos

Como funciona:
- Pedido ≥ R$ 20.000: Fator: 1.2 (aumenta desconto em +20%)
- Pedido ≥ R$ 10.000: Fator: 1.1 (aumenta desconto em +10%)
- Pedido ≥ R$ 5.000: Fator: 1.05 (aumenta desconto em +5%)
- Pedido < R$ 5.000: Fator: 1.0 (sem bônus)

Exemplo prático:
- Desconto base após curva e estoque: 8.40%
- Pedido de R$ 32.640,00 (fator 1.2) → Desconto efetivo: 8.40% × 1.2 = 10.08%
- Pedido de R$ 2.000,00 (fator 1.0) → Desconto efetivo: 8.40% × 1.0 = 8.40%

Como funciona no sistema:
O sistema consulta a tabela pricing_order_value_discounts que define faixas de valor e seus respectivos fatores. Se não encontrar, assume fator 1.0 (sem bônus).

Tabela de Descontos por Valor do Pedido:
As regras são armazenadas na tabela pricing_order_value_discounts com:
- Valor mínimo do pedido (min_order_value)
- Valor máximo do pedido (max_order_value) - pode ser NULL para "sem limite"
- Fator multiplicador (factor)
- Descrição da faixa
- Flag de ativo (is_active)


12. 💳 Desconto por Forma de Pagamento (Payment Term Discount) 🆕

O que é?
Um desconto adicional aplicado para produtos do segmento MACHINES quando o pagamento é feito em menos de 5 parcelas.

Por que importa?
- Melhora de caixa: Incentiva pagamentos à vista ou em poucas parcelas
- Segmento específico: Aplicado apenas para máquinas (produtos de alto valor)
- Flexibilidade: Cliente escolhe entre desconto ou mais parcelas

Como funciona:
- Apenas MACHINES: Desconto aplicado apenas para produtos do segmento MACHINES (segment_id = 1)
- Parcelas < 5: Desconto apenas para pagamentos em 0, 1, 2, 3 ou 4 parcelas
- Aplicado após cálculo normal: O desconto é aplicado sobre o preço já com desconto normal

Tabela de Descontos:
| Parcelas | Desconto |
|----------|----------|
| À vista (0) | 5% |
| 1x | 4% |
| 2x | 3% |
| 3x | 2% |
| 4x | 1% |
| 5x ou mais | 0% (sem desconto) |

Exemplo prático:
- Produto MACHINES: PT = R$ 3.264,00
- Desconto normal calculado: 8.4% → Preço após desconto normal: R$ 2.989,82
- Pagamento em 2x → Desconto adicional: 3%
- Preço após desconto de pagamento: R$ 2.989,82 × (1 - 0.03) = R$ 2.900,13
- Piso: R$ 2.549,18 → Preço Final: R$ 2.900,13

Como funciona no sistema:
1. O sistema verifica o segment_id do produto na tabela product_classification
2. Se segment_id = 1 (MACHINES) e installments < 5:
- Busca o desconto na tabela pricing_payment_term_discounts
- Aplica o desconto sobre o preço após desconto normal
3. Se não for MACHINES ou parcelas ≥ 5 → sem desconto adicional

Tabela de Configuração (pricing_payment_term_discounts):
- segment_id: ID do segmento (1 = MACHINES)
- installments: Número de parcelas (0 = à vista, 1 = 1x, etc.)
- discount_pct: Desconto percentual (0.050000 = 5%)
- is_active: Flag de ativo
- description: Descrição da regra

Ordem de Aplicação:
1. Cálculo normal (Tier, Brand Role, Curve, Stock Level, Order Value)
2. Desconto por Forma de Pagamento (se aplicável)
3. Validação do Piso
4. Preço Final


13. 📋 Último Preço Pago (Last Paid Price) 🆕

O que é?
O sistema verifica quanto o cliente pagou na última compra daquele produto e limita aumentos de preço para evitar surpresas negativas.

Por que importa?
- Consistência: Cliente espera pagar preço similar ao anterior
- Confiança: Evita "susto" com aumentos repentinos
- Fidelização: Demonstra que a empresa valoriza o histórico
- Proteção contra promoções: Não usa preço de promoção como referência

Como funciona:
- Clientes Tier V4 (top): Máximo +3% de aumento
- Clientes Tier V3: Máximo +4% de aumento
- Demais clientes: Máximo +5% de aumento
- Preço de promoção: Ignorado (usa preço médio)

Exemplo prático:
| Cenário | Último Preço | Preço Calculado | Resultado |
|---------|--------------|-----------------|-----------|
| Normal | R$ 2.940 | R$ 2.900 | R$ 2.900 ✅ (desconto) |
| Normal | R$ 2.940 | R$ 3.000 | R$ 3.000 ✅ (+2%, dentro do limite) |
| Normal | R$ 2.940 | R$ 3.200 | R$ 3.087 ⚠️ (limitado a +5%) |
| Promoção | R$ 2.200 | R$ 2.900 | R$ 2.900 ✅ (promoção ignorada) |
| Primeira compra | - | R$ 2.900 | R$ 2.900 ✅ (sem referência) |

Como funciona no sistema:
1. O sistema busca histórico de compras do cliente para o SKU
2. Verifica se o último preço era válido (não era promoção)
- Se last_price >= piso × 0.90 → preço válido, usar como referência
- Se last_price < piso × 0.90 → provavelmente promoção, usar preço médio
3. Calcula o máximo permitido: reference_price × (1 + max_increase_pct)
4. Se preço calculado > máximo permitido → limita ao máximo

Tabela de Regras (pricing_last_price_rules):
| Tier | Aumento Máximo | Histórico |
|------|----------------|-----------|
| V4 | +3% | 24 meses |
| V3 | +4% | 18 meses |
| Outros | +5% | 12 meses |


14. 🎁 Desconto Final

O que é?
O desconto que será realmente aplicado, após considerar todos os fatores.

Como é calculado:

Desconto Final = Desconto Base × Fator da Curva ABC × Fator do Stock Level × Fator do Order Value

Proteções de Segurança:
- Desconto nunca pode ser negativo (mínimo: 0%)
- Desconto nunca pode ser maior que 95% (máximo: 95%)

Exemplo prático:
- Desconto base: 15%
- Fator da curva: 1.2
- Fator do stock level: 1.2 (estoque alto)
- Fator do order value: 1.2 (pedido ≥ R$ 20k)
- Desconto final: 15% × 1.2 × 1.2 × 1.2 = 25.92% (limitado a 95% máx)

Outro exemplo:
- Desconto base: 8.4%
- Fator da curva: 1.0
- Fator do stock level: 1.0 (estoque normal)
- Fator do order value: 1.2 (pedido ≥ R$ 20k)
- Desconto final: 8.4% × 1.0 × 1.0 × 1.2 = 10.08%


15. 💵 Preço Candidato

O que é?
O preço calculado após aplicar o desconto ao Preço de Tela (PT).

Como é calculado:

Preço Candidato = Preço de Tela × (1 - Desconto Final)

Exemplo prático:
- Preço de Tela (PT): R$ 100,00
- Desconto Final: 18%
- Preço Candidato: R$ 100,00 × (1 - 0.18) = R$ 100,00 × 0.82 = R$ 82,00


16. 🚧 Respeitar o Piso (Price Floor)

O que é?
O preço mínimo que pode ser cobrado. É como um "chão" que o preço não pode ultrapassar para baixo.

Por que importa?
Garante que nunca vendamos abaixo do custo ou de um preço mínimo definido pela empresa.

Como funciona:
- Se o Preço Candidato for maior que o Piso → Usa o Preço Candidato ✅
- Se o Preço Candidato for menor que o Piso → Usa o Piso (proteção) 🛡️

Exemplo prático:
- Preço Candidato: R$ 82,00
- Piso Técnico: R$ 80,00
- Preço Final: R$ 82,00 (maior que o piso, então usa o candidato)

Outro exemplo:
- Preço Candidato: R$ 75,00
- Piso Técnico: R$ 80,00
- Preço Final: R$ 80,00 (menor que o piso, então usa o piso para proteger)


📐 Resumo do Cálculo Completo

Vamos ver um exemplo completo do início ao fim:

Cenário:
- Preço de Tela (PT): R$ 3.264,00
- Piso Técnico: R$ 2.549,18
- Cliente: Distribuidor (Non-Street), comprou R$ 97.998 nos últimos 12 meses
- Tier: V2 (baseado no volume)
- Marca: Secundária (Secondary Target)
- Produto: Curva A (produto importante)
- Estoque: Normal
- Valor do Pedido: R$ 32.640,00 (10 unidades)

Passo a passo:

  1. Contexto de Mercado: Non-Street ✅
  2. Volume 12 Meses: R$ 97.998
  3. Tier: V2
  4. Papel da Marca: Secondary Target
  5. Desconto Base (Tier V2 + Secondary): 8.4%
  6. Limite Street: Não aplica (é Non-Street) → Desconto permitido: 8.4%
  7. Fator da Curva A: 1.0
  8. Fator do Stock Level (Normal): 1.0
  9. Fator do Order Value (≥ R$ 20k): 1.2 🆕 (bônus +20%)
  10. Desconto Final: 8.4% × 1.0 × 1.0 × 1.2 = 10.08% (efetivo: 8.4%)
  11. Preço Após Desconto Normal: R$ 3.264,00 × (1 - 0.084) = R$ 2.989,82
  12. Desconto por Pagamento (2x, MACHINES): 3% 🆕 → Preço após desconto de pagamento: R$ 2.989,82 × (1 - 0.03) = R$ 2.900,13
  13. Respeitar Piso: R$ 2.900,13 > R$ 2.549,18 → Preço Final: R$ 2.900,13

Resultado: O cliente receberá o produto por R$ 2.900,13, com 8.4% de desconto normal + 3% de desconto por pagamento em 2x (totalizando 11.15% de desconto efetivo). O Order Value Factor de 1.2 ampliou o limite de desconto permitido, e o desconto por forma de pagamento foi aplicado adicionalmente.


🎓 Conceitos Importantes

Preço de Tela (PT):
O preço de referência máximo. É como o "preço de prateleira" antes de qualquer desconto.

Piso Técnico:
O preço mínimo que pode ser cobrado. É uma proteção para não vender abaixo de um valor definido.

Tier (Nível):
Uma classificação do cliente baseada no volume de compras. Quanto maior o tier, melhores as condições.

Curva ABC:
Uma classificação dos produtos baseada na importância (vendas). Produtos A são os mais importantes.

Desconto Final:
O desconto que será realmente aplicado, após considerar todos os fatores e proteções.


❓ Perguntas Frequentes

P: Por que o desconto é limitado a 95%?
R: Para proteger a empresa de erros ou configurações incorretas. Um desconto de 100% significaria vender de graça.

P: O que acontece se o Preço Candidato for menor que o Piso?
R: O sistema usa o Piso como preço final. Isso garante que nunca vendamos abaixo do mínimo definido.

P: Por que clientes Street têm desconto limitado a 12%?
R: Para proteger as margens no mercado varejo, onde os preços precisam ser mais estáveis e competitivos.

P: Como o sistema sabe qual curva ABC usar?
R: O sistema consulta uma tabela que classifica cada produto automaticamente baseado nas vendas dos últimos 12 meses.

P: Como o sistema sabe qual stock level usar?
R: O sistema consulta a tabela product_classification que calcula automaticamente o nível de estoque baseado em dias de estoque e vendas. Se não encontrar, assume 'normal' (sem ajuste).

P: O que acontece se não encontrar informações do cliente?
R: O sistema usa valores padrão (fallback) que são conservadores, garantindo que nunca dê descontos maiores do que deveria.

P: Como funciona o Order Value Factor? 🆕
R: O sistema verifica o valor total do pedido e aplica um bônus no desconto. Pedidos maiores (≥ R$ 20.000) recebem até +20% de bônus no desconto. Por exemplo, se o desconto base é 8.4% e o pedido vale R$ 32.640, o limite de desconto sobe para 10.08% (8.4% × 1.2).

P: O Order Value Factor é obrigatório?
R: Não. Se não houver configuração na tabela pricing_order_value_discounts ou o pedido for pequeno, o fator assume valor 1.0 (sem bônus).

P: Como funciona o Last Paid Price (Último Preço Pago)? 🆕
R: O sistema verifica o último preço que o cliente pagou por aquele produto. Se o novo preço calculado for maior que o último preço + 5% (variação máxima configurável), o sistema limita o aumento. Por exemplo, se o cliente pagou R$ 2.940 na última compra, o máximo que ele pode pagar agora é R$ 3.087 (2.940 × 1.05).

P: E se o último preço pago foi em uma promoção?
R: O sistema detecta automaticamente se o preço anterior era de promoção (quando o preço está abaixo de 90% do piso técnico). Nesse caso, o último preço é ignorado e o sistema usa o preço médio como referência.

P: O cliente sempre vai pagar o mesmo preço ou menos?
R: Não necessariamente. O sistema permite aumentos de até 5% (configurável por tier). Clientes V4 (top tier) têm limite de +3%, V3 têm +4%, e demais têm +5%. Reduções de preço não têm limite.


🔍 Fatores Calculados Internamente (Versão Técnica)

A stored procedure calcula os seguintes fatores em sequência:

1. Market Context (Contexto de Mercado)

Tabela: csuite_pricing.customer_market_context

SELECT COALESCE((
    SELECT market_context
    FROM csuite_pricing.customer_market_context
    WHERE customer_id = p_customer_id AND is_active = 1
    LIMIT 1
), 'non_street')
INTO v_market_context;

2. Volume 12 Meses

Tabela: csuite_pricing.vw_customer_volume_12m

SELECT COALESCE((
    SELECT volume_12m
    FROM csuite_pricing.vw_customer_volume_12m
    WHERE customer_id = p_customer_id
    LIMIT 1
), 0.00)
INTO v_volume_12m;

3. Tier Code (Nível de Volume)

Tabela: csuite_pricing.pricing_volume_tiers

SELECT COALESCE((
    SELECT tier_code
    FROM csuite_pricing.pricing_volume_tiers
    WHERE is_active = 1
      AND v_volume_12m >= min_volume_12m
      AND (max_volume_12m IS NULL OR v_volume_12m < max_volume_12m)
    LIMIT 1
), 'V1')
INTO v_tier_code;

4. Brand Role (Papel da Marca)

Tabela: csuite_pricing.brands

SELECT COALESCE((
    SELECT brand_role
    FROM csuite_pricing.brands
    WHERE brand_id = p_brand_id AND is_active = 1
    LIMIT 1
), 'secondary_target')
INTO v_brand_role;

5. Desconto por Tier + Brand Role

Tabela: csuite_pricing.pricing_volume_tiers_by_brand_role

SELECT COALESCE((
    SELECT discount_max
    FROM csuite_pricing.pricing_volume_tiers_by_brand_role
    WHERE is_active = 1
      AND brand_role = v_brand_role
      AND tier_code  = v_tier_code
    LIMIT 1
), 0.000000)
INTO v_discount_role;

6. Cap Street (Limite para Mercado Street)

IF v_market_context = 'street' THEN
    SET v_discount_allowed = LEAST(v_discount_role, 0.120000);
ELSE
    SET v_discount_allowed = v_discount_role;
END IF;

7. Curve Factor (Fator da Curva ABC)

Tabela: csuite_pricing.pricing_curve_factors

SELECT COALESCE((
    SELECT factor
    FROM csuite_pricing.pricing_curve_factors
    WHERE machine_curve = p_machine_curve
      AND is_active = 1
    LIMIT 1
), 1.000000)
INTO v_curve_factor;

8. Stock Level Factor (Fator do Nível de Estoque)

Tabela: csuite_pricing.pricing_stock_level_factors

SELECT COALESCE((
    SELECT factor
    FROM csuite_pricing.pricing_stock_level_factors
    WHERE stock_level = CAST(p_stock_level AS CHAR CHARACTER SET utf8mb4) COLLATE utf8mb4_unicode_ci
      AND is_active = 1
    LIMIT 1
), 1.000000)
INTO v_stock_level_factor;

9. Order Value Factor (Fator do Valor do Pedido) 🆕

Tabela: csuite_pricing.pricing_order_value_discounts

Faixas padrão:
| Valor do Pedido | Fator | Bônus |
|-----------------|-------|-------|
| ≥ R$ 20.000 | 1.20 | +20% |
| ≥ R$ 10.000 | 1.10 | +10% |
| ≥ R$ 5.000 | 1.05 | +5% |
| < R$ 5.000 | 1.00 | 0% |

SELECT COALESCE((
    SELECT factor
    FROM csuite_pricing.pricing_order_value_discounts
    WHERE p_order_value >= min_order_value
      AND (max_order_value IS NULL OR p_order_value <= max_order_value)
      AND is_active = 1
    LIMIT 1
), 1.000000)
INTO v_order_value_factor;

10. Desconto Final

SET v_discount_final = v_discount_allowed * v_curve_factor * v_stock_level_factor * v_order_value_factor;

-- Clamp segurança
IF v_discount_final < 0 THEN SET v_discount_final = 0; END IF;
IF v_discount_final > 0.95 THEN SET v_discount_final = 0.95; END IF;

11. Preço Candidato (Cálculo Normal)

SET v_price_candidate = p_PT * (1 - v_discount_final);

Nota: Este é o preço calculado pelo motor SQL. Se houver desconto por quantidade configurado, ele será aplicado após este passo, no código Python.

12. Desconto por Quantidade (Aplicado no Python)

Nota: Este passo é executado no código Python (decision.py), não na stored procedure.

13. Respeitar Piso (Price Floor)

IF v_price_candidate < p_PISO THEN
    SET o_final_price = p_PISO;
    SET o_status = 'FLOOR';
ELSE
    SET o_final_price = v_price_candidate;
    SET o_status = 'OK';
END IF;

Fórmula Final

O cálculo completo pode ser resumido na seguinte fórmula:

// 1. Cálculo Normal (via sp_price_compute_v8)
Desconto_Role = f(tier_code, brand_role)  // Tabela pricing_volume_tiers_by_brand_role
Desconto_Allowed = IF market_context = 'street' 
                    THEN MIN(Desconto_Role, 12%)
                    ELSE Desconto_Role
Curve_Factor = f(machine_curve)  // Tabela pricing_curve_factors
Stock_Level_Factor = f(stock_level)  // Tabela pricing_stock_level_factors
Order_Value_Factor = f(order_value)  // Tabela pricing_order_value_discounts 🆕
Desconto_Final = Desconto_Allowed * Curve_Factor * Stock_Level_Factor * Order_Value_Factor  // Limitado entre 0% e 95%
Preço_Candidato_Normal = PT * (1 - Desconto_Final)

// 2. Desconto por Quantidade (se aplicável)
IF existe_regra_quantidade(sku_id, quantity):
    Preço_Candidato = f(quantity)  // Preço fixo ou desconto percentual da tabela pricing_quantity_discounts
ELSE:
    Preço_Candidato = Preço_Candidato_Normal

// 3. Validação de Limites
Preço_Final = MAX(MIN(Preço_Candidato, PT), PISO)

Nota: Descontos por quantidade são aplicados após o cálculo normal, mas antes da validação de limites.

Variáveis de Saída

A stored procedure retorna as seguintes variáveis:

Variável Tipo Descrição
o_status VARCHAR(16) 'OK' ou 'FLOOR' ou 'ERROR'
o_reason VARCHAR(160) Descrição do cálculo (tier, context, role, curve)
o_final_price DECIMAL(18,6) Preço final calculado
o_discount_allowed DECIMAL(9,6) Desconto final aplicado (já com curva)
o_tier_code VARCHAR(8) Tier do cliente (V1, V2, etc.)
o_market_context VARCHAR(16) Contexto de mercado (street/non_street)
o_volume_12m DECIMAL(18,2) Volume 12 meses do cliente
o_brand_role VARCHAR(32) Papel da marca
o_curve_factor DECIMAL(9,6) Fator da curva ABC aplicado
o_stock_level_factor DECIMAL(9,6) Fator do nível de estoque aplicado
o_order_value_factor DECIMAL(9,6) Fator do valor do pedido aplicado 🆕

Tabelas Consultadas

A stored procedure consulta as seguintes tabelas do schema csuite_pricing:

  1. customer_market_context - Contexto de mercado do cliente
  2. vw_customer_volume_12m - Volume 12 meses (view)
  3. pricing_volume_tiers - Definição de tiers por volume
  4. brands - Informações da marca (brand_role)
  5. pricing_volume_tiers_by_brand_role - Descontos por tier + brand role
  6. pricing_curve_factors - Fatores por curva ABC
  7. pricing_stock_level_factors - Fatores por nível de estoque
  8. pricing_order_value_discounts - Fatores por valor do pedido 🆕
  9. pricing_promotions - Promoções esporádicas (consultada antes do cálculo normal)
  10. pricing_quantity_discounts - Descontos por quantidade (consultada após cálculo normal)
  11. pricing_payment_term_discounts - Descontos por forma de pagamento (MACHINES, após cálculo normal) 🆕

Exemplo de Cálculo

Cenário:
- PT = R$ 3.264,00
- PISO = R$ 2.549,18
- Cliente: volume_12m = 97.998, market_context = 'non_street'
- Tier: V2 (baseado no volume)
- Brand Role: 'secondary_target'
- Machine Curve: 'A'
- Stock Level: 'normal'
- Order Value: R$ 32.640,00 (10 unidades)
- Desconto V2 + secondary_target: 8.4%
- Curve Factor A: 1.0
- Stock Level Factor (normal): 1.0
- Order Value Factor (≥ R$ 20k): 1.2
- Installments: 2x (MACHINES) → Payment Term Discount: 3% 🆕

Cálculo:
1. v_discount_role = 0.084 (8.4%)
2. v_discount_allowed = 0.084 (não é street, sem cap)
3. v_curve_factor = 1.0
4. v_stock_level_factor = 1.0
5. v_order_value_factor = 1.2 (pedido ≥ R$ 20k → bônus +20%)
6. v_discount_final = 0.084 * 1.0 * 1.0 * 1.2 = 0.1008 (10.08%)
7. v_price_candidate = 3264 * (1 - 0.1008) = R$ 2.935,39 (após desconto normal)
8. v_payment_term_discount = 0.03 (3% para 2x em MACHINES) 🆕
9. v_price_after_payment = 2935.39 * (1 - 0.03) = R$ 2.847,33 (após desconto de pagamento) 🆕
10. o_final_price = MAX(2847.33, 2549.18) = R$ 2.847,33

*Nota: O order_value_factor aumenta o limite de desconto permitido, mas o preço final ainda respeita os limites do corredor. O desconto por forma de pagamento é aplicado APÓS o cálculo normal, apenas para produtos MACHINES com parcelas < 5.


API e Payloads

Endpoint Principal

POST /run

Payload (Formato CSuite Pricing)

{
  "org_id": 1,
  "brand_id": 1,
  "customer_id": 123,
  "sku_id": 456,
  "sku_qty": 5,
  "order_value": 50000.00,
  "payment_term": "standard",    // standard | short | extended
  "installments": null,           // número de parcelas (0=à vista, 1=1x, 2=2x, etc.) - apenas para MACHINES 🆕
  "stock_level": "normal",        // normal | high
  "machine_curve": "A"            // A | B | C
}

Payload (Formato Legado - Compatibilidade)

{
  "org_id": 0,
  "sku": {
    "id": "SKU-123",
    "cost": 8000,
    "margin": 20
  },
  "inventory": {
    "days_on_hand": 45,
    "turn_rate": 0.5
  },
  "market": {
    "elasticity": -1.2,
    "competitor_price": 10000
  },
  "policy": {
    "floor_margin": 18,
    "max_discount": 5
  }
}

Resposta (Sucesso)

{
  "decision": {
    "decision_type": "PRICING.COMPUTED",
    "confidence": 0.9,
    "final_price": 9500.00,
    "discount_allowed": 0.05,
    "applied_mode": "CORRIDOR_PRICE",
    "elasticity": "medium",
    "screen_price_pt": 10000.00,
    "floor_price": 8000.00,
    "proposed_actions": [
      {
        "type": "UPDATE_PRICE",
        "new_price": 9500.00,
        "discount_pct": 5.0,
        "elasticity": "medium"
      }
    ]
  },
  "context": {
    "org_id": 1,
    "brand_id": 1,
    "customer_id": 123,
    "sku_id": 456,
    "is_anchor_customer": false,
    "price_screen_pt": 10000.00,
    "price_floor": 8000.00,
    "brand_role": "secondary_target"
  },
  "execution": {
    "status": "EXECUTED",
    "actions": [...]
  },
  "decision_log_id": 2001,
  "calc_id": 1001
}

Resposta (Cliente Âncora)

{
  "decision": {
    "decision_type": "PRICING.ANCHOR",
    "confidence": 1.0,
    "final_price": 9500.00,
    "applied_mode": "ANCHOR_TABLE",
    "elasticity": "low",
    "discount_pct": 0.05,
    "reason": "Preço Âncora derivado da política vigente"
  },
  "context": {
    "is_anchor_customer": true,
    ...
  },
  "calc_id": 1001
}

Resposta (Incidente)

{
  "decision": {
    "decision_type": "PRICING.INCIDENT",
    "confidence": 0.0,
    "reason": "PT_LEQ_PISO",
    "proposed_actions": [
      {
        "type": "BLOCK_PRICE",
        "reason": "PT_LEQ_PISO"
      }
    ]
  },
  "incident_id": 5001
}

Como Usar a Resposta do Agent

A resposta do endpoint /run contém todas as informações necessárias para aplicar o preço calculado e tomar decisões baseadas no resultado. Esta seção explica como interpretar e usar cada parte da resposta.

Estrutura da Resposta

A resposta HTTP tem o seguinte formato:

{
  "status": "success",
  "agent": "CSuite.Pricing.Agent",
  "result": {
    "decision": { ... },
    "context": { ... },
    "execution": { ... },
    "decision_log_id": 2001,
    "run_id": 1001,
    "calc_id": 1001
  }
}

1. Verificar Status da Resposta

Primeiro passo: Verificar se a requisição foi bem-sucedida:

response = requests.post("https://csuite.internut.com.br/pricing/run", json=payload)
data = response.json()

if data["status"] != "success":
    # Tratar erro
    raise Exception(f"Agent retornou status: {data['status']}")

result = data["result"]

2. Interpretar o Tipo de Decisão

Segundo passo: Verificar o decision_type para saber como proceder:

decision = result["decision"]
decision_type = decision["decision_type"]

if decision_type == "PRICING.ANCHOR":
    # Cliente Âncora: usar preço estável
    final_price = decision["final_price"]
    print(f"Preço Âncora: R$ {final_price:.2f}")

elif decision_type == "PRICING.COMPUTED":
    # Preço calculado normalmente
    final_price = decision["final_price"]
    discount = decision.get("discount_allowed", 0) * 100
    print(f"Preço Final: R$ {final_price:.2f} (Desconto: {discount:.2f}%)")

elif decision_type == "PRICING.INCIDENT":
    # Incidente: PT <= Piso - NÃO APLICAR PREÇO
    print(f"ERRO: {decision['reason']}")
    print("Preço não pode ser aplicado. Verificar configuração de PT e Piso.")
    # NÃO atualizar preço no sistema

elif decision_type == "PRICING.BLOCK":
    # Bloqueado por validação de política
    print(f"Preço bloqueado: {decision.get('reason', 'Validação falhou')}")
    # Requer aprovação manual

3. Extrair o Preço Final

Terceiro passo: Obter o preço final para aplicar:

def get_final_price(result):
    """Extrai o preço final da resposta do agent"""
    decision = result["decision"]
    decision_type = decision["decision_type"]

    # Apenas ANCHOR e COMPUTED têm preço válido
    if decision_type in ["PRICING.ANCHOR", "PRICING.COMPUTED"]:
        return decision.get("final_price")

    # INCIDENT e BLOCK não têm preço aplicável
    return None

final_price = get_final_price(result)

if final_price:
    # Aplicar preço no sistema
    update_product_price(sku_id, final_price)
    print(f"Preço atualizado: R$ {final_price:.2f}")
else:
    print("Preço não pode ser aplicado")

4. Verificar Metadados do Cálculo

Quarto passo: Usar informações adicionais para auditoria e análise:

decision = result["decision"]
context = result["context"]

# Informações de precificação
print(f"Preço de Tela (PT): R$ {decision.get('screen_price_pt', 0):.2f}")
print(f"Piso Técnico: R$ {decision.get('floor_price', 0):.2f}")
print(f"Desconto Aplicado: {decision.get('discount_allowed', 0) * 100:.2f}%")
print(f"Modo Aplicado: {decision.get('applied_mode', 'N/A')}")
print(f"Elasticidade: {decision.get('elasticity', 'N/A')}")

# Confiança da decisão
confidence = decision.get("confidence", 0)
if confidence >= 0.8:
    print("✅ Alta confiança - pode aplicar automaticamente")
elif confidence >= 0.5:
    print("⚠️ Confiança média - revisar antes de aplicar")
else:
    print("❌ Baixa confiança - requer aprovação manual")

# IDs para auditoria
print(f"Decision Log ID: {result.get('decision_log_id')}")
print(f"Calculation ID: {result.get('calc_id')}")
print(f"Run ID: {result.get('run_id')}")

5. Executar Ações Propostas

Quinto passo: Executar as ações sugeridas pelo agent:

execution = result["execution"]
actions = execution.get("actions", [])

for action in actions:
    action_type = action["type"]

    if action_type == "UPDATE_PRICE":
        # Atualizar preço no sistema
        new_price = action["new_price"]
        update_product_price(sku_id, new_price)
        print(f"✅ Preço atualizado: R$ {new_price:.2f}")

    elif action_type == "APPLY_ANCHOR_PRICE":
        # Aplicar preço âncora
        price = action["price"]
        apply_anchor_price(sku_id, price)
        print(f"✅ Preço âncora aplicado: R$ {price:.2f}")

    elif action_type == "BLOCK_PRICE":
        # Bloquear preço - não aplicar
        reason = action.get("reason", "Política violada")
        block_price(sku_id, reason)
        print(f"🚫 Preço bloqueado: {reason}")

    elif action_type == "ACTIVATE_PROMOTION":
        # Ativar promoção
        promotion_id = action.get("promotion_id")
        activate_promotion(promotion_id)
        print(f"🎯 Promoção ativada: {promotion_id}")

6. Tratamento de Erros

Sexto passo: Tratar erros e casos especiais:

try:
    response = requests.post(url, json=payload, timeout=30)
    response.raise_for_status()
    data = response.json()

    if data["status"] != "success":
        raise Exception(f"Agent retornou erro: {data.get('detail', 'Erro desconhecido')}")

    result = data["result"]
    decision = result["decision"]

    # Verificar se há incidente
    if decision["decision_type"] == "PRICING.INCIDENT":
        incident_id = result.get("incident_id")
        log_incident(incident_id, decision["reason"])
        notify_administrators(f"Incidente de precificação: {decision['reason']}")
        return None  # Não aplicar preço

    # Verificar confiança
    if decision.get("confidence", 0) < 0.5:
        # Requer aprovação manual
        send_for_approval(result)
        return None

    # Aplicar preço
    return decision["final_price"]

except requests.exceptions.Timeout:
    print("Timeout ao chamar Pricing Agent")
    # Usar preço padrão ou cache

except requests.exceptions.HTTPError as e:
    print(f"Erro HTTP: {e}")
    # Tratar erro específico

except Exception as e:
    print(f"Erro inesperado: {e}")
    # Log e notificação

Exemplo Completo de Integração

import requests
from typing import Optional, Dict, Any

def calculate_price(
    org_id: int,
    brand_id: int,
    customer_id: int,
    sku_id: int,
    sku_qty: int = 1,
    order_value: float = 0,
    machine_curve: str = "B"
) -> Optional[Dict[str, Any]]:
    """
    Calcula preço usando Pricing Agent

    Returns:
        Dict com preço e metadados, ou None se houver erro
    """
    url = "https://csuite.internut.com.br/pricing/run"

    payload = {
        "org_id": org_id,
        "brand_id": brand_id,
        "customer_id": customer_id,
        "sku_id": sku_id,
        "sku_qty": sku_qty,
        "order_value": order_value,
        "machine_curve": machine_curve,
        "payment_term": "standard",
        "stock_level": "normal"
    }

    try:
        response = requests.post(url, json=payload, timeout=30)
        response.raise_for_status()
        data = response.json()

        if data["status"] != "success":
            print(f"Erro: {data.get('detail', 'Erro desconhecido')}")
            return None

        result = data["result"]
        decision = result["decision"]
        decision_type = decision["decision_type"]

        # Verificar tipo de decisão
        if decision_type == "PRICING.INCIDENT":
            print(f"⚠️ Incidente: {decision['reason']}")
            return {
                "status": "incident",
                "reason": decision["reason"],
                "incident_id": result.get("incident_id")
            }

        if decision_type == "PRICING.BLOCK":
            print(f"🚫 Bloqueado: {decision.get('reason', 'Validação falhou')}")
            return {
                "status": "blocked",
                "reason": decision.get("reason"),
                "requires_approval": True
            }

        # Extrair preço final
        final_price = decision.get("final_price")
        if not final_price:
            print("❌ Preço final não disponível")
            return None

        # Retornar resultado completo
        return {
            "status": "success",
            "final_price": final_price,
            "screen_price_pt": decision.get("screen_price_pt"),
            "floor_price": decision.get("floor_price"),
            "discount_pct": decision.get("discount_allowed", 0) * 100,
            "applied_mode": decision.get("applied_mode"),
            "elasticity": decision.get("elasticity"),
            "confidence": decision.get("confidence", 0),
            "decision_type": decision_type,
            "calc_id": result.get("calc_id"),
            "decision_log_id": result.get("decision_log_id"),
            "run_id": result.get("run_id")
        }

    except requests.exceptions.Timeout:
        print("⏱️ Timeout ao chamar Pricing Agent")
        return None

    except requests.exceptions.HTTPError as e:
        print(f"❌ Erro HTTP {e.response.status_code}: {e}")
        return None

    except Exception as e:
        print(f"❌ Erro inesperado: {e}")
        return None


# Uso prático
def apply_pricing_to_order(order_id: int, items: list):
    """Aplica precificação a um pedido completo"""
    for item in items:
        price_result = calculate_price(
            org_id=item["org_id"],
            brand_id=item["brand_id"],
            customer_id=item["customer_id"],
            sku_id=item["sku_id"],
            sku_qty=item["quantity"],
            order_value=item["order_total"],
            machine_curve=item.get("curve", "B")
        )

        if price_result and price_result["status"] == "success":
            # Aplicar preço
            item["final_price"] = price_result["final_price"]
            item["discount_pct"] = price_result["discount_pct"]
            item["calc_id"] = price_result["calc_id"]

            # Atualizar no banco
            update_order_item_price(
                order_id=order_id,
                sku_id=item["sku_id"],
                price=price_result["final_price"],
                calc_id=price_result["calc_id"]
            )
        else:
            # Tratar erro ou incidente
            item["pricing_status"] = price_result.get("status", "error")
            item["pricing_error"] = price_result.get("reason", "Erro desconhecido")

            if price_result and price_result.get("requires_approval"):
                send_for_manual_review(order_id, item)

Campos Importantes da Resposta

Campo Localização Descrição Uso
final_price result.decision.final_price Preço final calculado Aplicar no sistema
screen_price_pt result.decision.screen_price_pt Preço de Tela (referência) Exibir para usuário
floor_price result.decision.floor_price Piso Técnico (mínimo) Validação
discount_allowed result.decision.discount_allowed Desconto aplicado (0-1) Exibir desconto %
applied_mode result.decision.applied_mode Modo aplicado (ANCHOR_TABLE/CORRIDOR_PRICE) Auditoria
elasticity result.decision.elasticity Nível de elasticidade Análise
confidence result.decision.confidence Confiança (0-1) Decisão automática vs manual
decision_type result.decision.decision_type Tipo de decisão Roteamento
calc_id result.calc_id ID do cálculo Auditoria/rastreabilidade
decision_log_id result.decision_log_id ID do log de decisão Policy Engine
run_id result.run_id ID da execução Agent Registry

Boas Práticas

  1. Sempre verificar decision_type antes de aplicar preço
  2. Usar calc_id para rastreabilidade e auditoria
  3. Verificar confidence para decidir se requer aprovação manual
  4. Tratar incidentes adequadamente (não aplicar preço)
  5. Logar todas as decisões usando decision_log_id e run_id
  6. Implementar fallback para casos de timeout ou erro
  7. Validar preço final está dentro do corredor (PT >= preço >= Piso)

Módulos e Responsabilidades

main.py

decision.py

context.py

engine.py

anchor.py

repository.py

database.py

policies.py

audit.py

execution.py


Fluxo de Decisão

O Pricing Agent segue uma ordem de prioridade específica ao calcular o preço:

Ordem de Prioridade

  1. Cliente Âncora (maior prioridade)
  2. Se o cliente é classificado como "âncora", usa preço da tabela pricing_anchor_prices
  3. EXIT - não verifica mais nada

  4. Promoção Ativa (segunda prioridade)

  5. Se o produto está em promoção (tabela pricing_promotions), usa preço de promoção
  6. EXIT - não calcula preço normal

  7. Cálculo Base (motor SQL) (terceira prioridade)

  8. Usa motor SQL (sp_price_compute_v8) para obter PT, Piso, contexto e payment_term_discount
  9. O desconto normal do motor pode ser sobreposto por regras especiais

  10. Combo/Bundle ou Desconto por Quantidade

  11. Se houver regra de combo/quantidade (SKU ou família), recalcula o preço a partir do PT
  12. Aplica o desconto do combo/quantidade e apenas o desconto de prazo de pagamento

  13. Último Preço Pago (LPP)

  14. Pode limitar aumento de preço conforme histórico do cliente

  15. Produto em Lançamento (teto) 🆕

  16. Se SKU está em lançamento ativo, limita preço ao launch_price

  17. Proteção Regional

  18. Ajusta preço conforme regra regional (quando ativa)

  19. Validações finais de limites

  20. Garante que o preço final está entre PT e Piso
  21. Ajusta se necessário

Fluxo Detalhado

Payload
  
Context Builder (enriquece com dados do schema)
  
┌─────────────────────────────────────┐
 1. Cliente Âncora?                 
     SIM: Retorna Preço Âncora     
     NÃO: Continua                  
└─────────────────────────────────────┘
  
┌─────────────────────────────────────┐
 2. Promoção Ativa?                  
     SIM: Retorna Preço Promoção    
     NÃO: Continua                  
└─────────────────────────────────────┘
  
┌─────────────────────────────────────┐
 3. Cálculo Normal (sp_price_compute_v8) 
    - Market Context                 
    - Volume 12m  Tier              
    - Brand Role                     
    - Desconto Base (Tier + Role)    
    - Cap Street (se aplicável)      
    - Curve Factor                   
    - Stock Level Factor             
    - Order Value Factor 🆕          
    - Preço Candidato                
└─────────────────────────────────────┘
  
┌─────────────────────────────────────┐
 4. Desconto por Quantidade?         
     SIM: Ajusta preço              
     NÃO: Mantém preço calculado    
└─────────────────────────────────────┘
  
┌─────────────────────────────────────┐
 5. Validar Limites                  
    - Se < Piso: Ajusta para Piso    
    - Se > PT: Ajusta para PT        
└─────────────────────────────────────┘
  
Preço Final

Fluxo de Decisão (Detalhado)

Fluxo Principal

1. Recebe Payload
   
2. build_context()  Enriquece com dados do schema
   
3. Verifica is_anchor_customer?
   ├─ SIM  get_anchor_price_for_sku()  Preço Âncora (EXIT)
   └─ NÃO  Continua
   
4. Verifica Promoção Ativa?
   ├─ SIM  get_active_promotion()  Preço Promoção (EXIT)
   └─ NÃO  Continua
   
5. compute_price_v8()  Chama stored procedure (cálculo normal)
   
6. Verifica engine_result["status"]
   ├─ "INCIDENT"  log_price_incident()  PRICING.INCIDENT
   └─ "OK"  Continua
   
7. Aplica Desconto por Quantidade?
   ├─ SIM  get_quantity_discount()  Ajusta preço
   └─ NÃO  Mantém preço calculado
   
8. Valida limites (PT e Piso)  Ajusta se necessário
   
9. validate_decision()  Valida contra políticas
   ├─ FALHA  PRICING.BLOCK
   └─ SUCESSO  Continua
   
10. log_price_calculation()  Registra auditoria
   
11. _instrument_decision()  Instrumenta Policy Engine
   
12. execute_actions()  Executa ações propostas
   
13. _register_agent_run()  Registra no Agent Registry
   
14. Retorna resultado completo

Fluxo Cliente Âncora

1. build_context()  is_anchor_customer = True
   
2. get_anchor_price_for_sku()  Consulta pricing_anchor_prices
   
3. Retorna preço estável (não calcula desconto)
   
4. log_price_calculation()  Registra como ANCHOR_TABLE
   
5. execute_actions()  APPLY_ANCHOR_PRICE
   
6. Retorna PRICING.ANCHOR

Fluxo Incidente

1. compute_price_v8()  Retorna status="INCIDENT"
   
2. log_price_incident()  Registra em price_incidents
   
3. Retorna PRICING.INCIDENT (não valida, não calcula)

Políticas e Validações

Regras Absolutas

  1. PT > Piso: Obrigatório. Se PT ≤ Piso → gera PRICING.INCIDENT
  2. Cliente Âncora:
  3. Apenas ANCHOR_TABLE mode
  4. Elasticidade: NONE ou LOW apenas
  5. Não recebe desconto elástico
  6. Marca Principal:
  7. Elasticidade máxima: LOW
  8. Não pode escalar elasticidade
  9. Corredor de Decisão:
  10. Preço final: PT >= final_price >= Piso
  11. Se fora do corredor → PRICING.BLOCK

Níveis de Autonomia


Testes

Estrutura de Testes

tests/agents/pricing/
├── __init__.py
├── conftest.py              # Fixtures compartilhadas
├── test_repository.py       # Testes de repository
├── test_anchor.py           # Testes de Cliente Âncora
├── test_policies.py         # Testes de validação
├── test_context.py          # Testes de contexto
├── test_decision.py         # Testes de decisão
├── test_integration.py      # Testes de integração
└── README.md                # Documentação dos testes

Executar Testes

# Todos os testes
pytest tests/agents/pricing/ -v

# Apenas unitários
pytest tests/agents/pricing/ -v -m unit

# Apenas integração
pytest tests/agents/pricing/ -v -m integration

# Teste específico
pytest tests/agents/pricing/test_decision.py::TestDecision::test_run_decision_anchor_customer -v

Cobertura

Total: 38 testes, todos passando ✅


Exemplos de Uso

Exemplo 1: Cliente Normal

import requests

payload = {
    "org_id": 1,
    "brand_id": 1,
    "customer_id": 123,
    "sku_id": 456,
    "sku_qty": 5,
    "order_value": 50000.00,
    "payment_term": "standard",
    "installments": null,  // número de parcelas (0=à vista, 1=1x, etc.) - apenas para MACHINES
    "stock_level": "normal",
    "machine_curve": "A"
}

response = requests.post("http://localhost:8000/run", json=payload)
result = response.json()

print(f"Preço final: {result['decision']['final_price']}")
print(f"Elasticidade: {result['decision']['elasticity']}")
print(f"Modo: {result['decision']['applied_mode']}")

Exemplo 2: Cliente Âncora

payload = {
    "org_id": 1,
    "brand_id": 1,
    "customer_id": 999,  # Cliente Âncora
    "sku_id": 456
}

response = requests.post("http://localhost:8000/run", json=payload)
result = response.json()

assert result['decision']['decision_type'] == "PRICING.ANCHOR"
assert result['decision']['applied_mode'] == "ANCHOR_TABLE"
print(f"Preço Âncora: {result['decision']['final_price']}")

Exemplo 3: Incidente (PT ≤ Piso)

# Quando PT <= Piso, gera incidente
payload = {
    "org_id": 1,
    "brand_id": 1,
    "sku_id": 789  # SKU com PT <= Piso
}

response = requests.post("http://localhost:8000/run", json=payload)
result = response.json()

assert result['decision']['decision_type'] == "PRICING.INCIDENT"
assert result['decision']['reason'] == "PT_LEQ_PISO"
print(f"Incidente registrado: {result.get('incident_id')}")

Exemplo 4: Uso Direto (Python)

from agents.pricing.decision import run_decision
import asyncio

async def main():
    payload = {
        "org_id": 1,
        "brand_id": 1,
        "customer_id": 123,
        "sku_id": 456,
        "sku_qty": 5,
        "order_value": 50000.00
    }

    result = await run_decision(payload)
    print(result)

asyncio.run(main())

Troubleshooting

Erro: "Access denied for user 'core'"

Causa: Credenciais de banco não configuradas ou incorretas.

Solução: Verificar variáveis de ambiente:
- DB_HOST
- DB_USER
- DB_PASSWORD
- DB_NAME (deve ser o schema csuite_pricing)

Erro: "sp_price_compute_v8 não disponível"

Causa: Stored procedure não existe no banco.

Solução:
- O agente faz fallback automático para v7
- Verificar se pelo menos sp_price_compute_v7 existe
- Criar stored procedure se necessário

Erro: "PRICING.BLOCK" em vez de "PRICING.COMPUTED"

Causa: Validação de políticas falhou.

Possíveis motivos:
- Preço final fora do corredor (PT >= preço >= Piso)
- Cliente Âncora com modo incorreto
- Marca Principal com elasticidade > LOW

Solução: Verificar logs para identificar qual validação falhou.

Erro: "Cliente Âncora não encontrado"

Causa: Tabela pricing_anchor_prices vazia ou SKU não tem preço âncora.

Solução:
- Verificar se preço âncora foi calculado para o SKU
- Executar job de cálculo de preços âncora
- Verificar policy_version usado na consulta

Performance: Consultas lentas

Causa: Falta de índices ou connection pooling inadequado.

Solução:
- Verificar índices nas tabelas do csuite_pricing
- Ajustar tamanho do connection pool
- Usar db parameter para reutilizar sessão


🚀 Produtos em Lançamento (12/01/2026)

O que é?

Sistema para gerenciar produtos em lançamento com preços promocionais temporários. Durante o período de lançamento, o motor de pricing:

  1. Ignora a regra de LPP (Last Paid Price) - Cliente pode pagar o preço de lançamento mesmo que seja maior que +5% do último preço
  2. Usa o preço de lançamento como teto - O preço final nunca excede o launch_price cadastrado
  3. Suporta período de transição - Após o lançamento, há um período onde o LPP ainda é ignorado (ignore_lpp_until)

Por que importa?

Tabela pricing_launch_products

Campo Tipo Descrição
sku_id BIGINT ID do SKU
brand_id BIGINT ID da marca
product_name VARCHAR Nome do produto
product_model VARCHAR Modelo do produto
launch_price DECIMAL Preço de lançamento (teto durante o período)
regular_price DECIMAL Preço regular após lançamento
price_pt DECIMAL PT no momento do cadastro
price_floor DECIMAL Piso no momento do cadastro
launch_start DATE Início do lançamento
launch_end DATE Fim do período de lançamento
ignore_lpp_until DATE Ignorar LPP até esta data (transição)
max_discount_pct DECIMAL Desconto máximo adicional permitido
notes VARCHAR Observações
is_active BOOLEAN Flag de ativo

Status do Lançamento

Status Condição Comportamento
SCHEDULED CURDATE() < launch_start Aguardando início
ACTIVE CURDATE() BETWEEN launch_start AND launch_end LPP ignorado + preço teto = launch_price
TRANSITION CURDATE() > launch_end AND CURDATE() <= ignore_lpp_until LPP ignorado, preço normal
ENDED CURDATE() > ignore_lpp_until Comportamento normal

Resposta da API

Quando um produto está em lançamento, a resposta inclui:

"launch_product": {
    "is_launch": true,
    "status": "ACTIVE",
    "launch_price": 3200.00,
    "regular_price": 3768.00,
    "lpp_ignored": true,
    "launch_price_applied": true,
    "launch_end": "2026-01-31",
    "ignore_lpp_until": "2026-03-12"
}

Interface de Administração

Acessível em /pricing/admin🚀 Produtos em Lançamento:

Exemplo de Fluxo

1. Cadastrar produto em lançamento:
   - SKU: 1981269
   - launch_price: R$ 3.200,00
   - regular_price: R$ 3.768,00
   - launch_end: 2026-01-31
   - ignore_lpp_until: 2026-03-12

2. Cliente solicita cotação durante lançamento:
   - Motor calcula: R$ 3.372,36 (baseado em tier, curva, etc.)
   - Motor detecta: SKU em lançamento ativo
   - Motor limita: R$ 3.200,00 (launch_price)
   - Resposta: final_price = R$ 3.200,00, launch_price_applied = true

3. Após launch_end, até ignore_lpp_until (transição):
   - LPP ainda é ignorado
   - Preço calculado normalmente (sem teto do launch_price)

4. Após ignore_lpp_until:
   - Comportamento normal
   - LPP aplicado normalmente

Referências


Changelog

v1.3.0 (2026-01-12)

v1.2.0 (2026-01-10)

v1.1.0 (2026-01-10)

v1.0.0 (2025-01-03)


Última atualização: 2026-01-12
Versão do Agente: 1.3.0
Status: ✅ Produção

🔊 Text-to-Speech

1.0x
1.0
Pronto para reproduzir