# ENG-1897 Stock Position Progressive Loading Design

## Context

O dashboard de Posição de Estoque consulta atualmente todos os produtos do almoxarifado em uma única requisição. O backend calcula a coleção completa, carrega integrações por Eloquent, recalcula parte dos saldos no `StockIOProductMissingAmountService`, monta os totalizadores em memória e serializa `products` e `critical_products`. O frontend mantém toda a coleção, filtra e ordena localmente e renderiza todas as linhas no DOM.

Com uma coleção grande, o custo não fica restrito à consulta HTTP: o parse do JSON, a ordenação, a reconciliação do React e a criação das linhas ocupam a thread principal. Isso explica o atraso entre o clique no cabeçalho e o início do loading.

O padrão de navegação do sistema é carregamento progressivo ao rolar a página. Os totalizadores devem sempre representar todo o estoque do almoxarifado, independentemente dos filtros aplicados à listagem.

## Goals

- Reduzir o tempo da primeira resposta e o tamanho do payload.
- Renderizar apenas lotes pequenos de produtos no frontend.
- Fazer filtros e ordenação sobre o conjunto global, não somente sobre os produtos já carregados.
- Preservar os cálculos atuais de estoque físico, entradas pendentes, reservas e disponibilidade.
- Manter os totalizadores globais corretos e independentes dos filtros.
- Seguir o padrão de carregamento progressivo do sistema.
- Exibir feedback imediatamente ao alterar ordenação ou filtros.

## Non-Goals

- Criar uma visão materializada ou uma tabela de saldo nesta primeira etapa.
- Alterar a definição funcional dos totalizadores.
- Alterar as regras de equivalência entre produtos integrados.
- Virtualizar a lista inicialmente; isso poderá ser avaliado se usuários carregarem um volume muito grande de páginas na mesma sessão.
- Adicionar índices sem validar o plano da consulta final.

## Recommended Approach

Mover paginação, filtros e ordenação para o backend e retornar os produtos por cursor, inicialmente em lotes de 50. A consulta deve partir de uma base agregada única de disponibilidade, reutilizada para obter:

1. Os totalizadores globais, antes de qualquer filtro de interface.
2. A página filtrada e ordenada de produtos.

A ordenação deve usar `product_id` como último critério de desempate para garantir um cursor estável. A resposta não deve mais carregar a coleção redundante `critical_products`; produtos críticos continuam identificáveis pelo campo `available_stock_amount` e pelo filtro de status.

Esta abordagem elimina o payload completo e reduz drasticamente o DOM sem introduzir a consistência eventual de uma visão materializada.

## Design

### API contract

O endpoint `GET /api/v1/report/warehouse/stock-position` aceitará:

- `cursor`: cursor opaco da próxima página.
- `per_page`: quantidade por lote, com padrão 50 e limite máximo validado.
- `sort_by`: `description`, `stock_amount`, `input_stock_amount`, `reserved_request_amount` ou `available_stock_amount`.
- `sort_direction`: `asc` ou `desc`.
- `product_field`: `description`, `integration_derivation`, `integration_product`, `integration_barcode` ou `id`.
- `product_query`: texto de busca.
- `statuses[]`: `critical`, `reserved` e/ou `available`.

Formato proposto:

```json
{
  "data": {
    "summary": {
      "products_with_physical_stock": 0,
      "products_with_reserved_stock": 0,
      "products_with_positive_availability": 0,
      "products_without_availability": 0
    },
    "products": []
  },
  "links": {
    "next": null
  },
  "meta": {
    "per_page": 50
  }
}
```

Os filtros afetam somente `data.products`. `data.summary` é calculado sobre toda a base de disponibilidade do almoxarifado.

### Availability query

A consulta deverá consolidar em SQL, por produto:

- estoque físico armazenado em endereços do almoxarifado;
- quantidade identificada ainda pendente em entradas;
- quantidade não identificada ainda pendente em entradas;
- quantidade reservada por requisições e saídas sincronizadas;
- equivalência dos produtos conforme código, derivação e empresa da integração;
- disponibilidade como `max(stock_amount + input_stock_amount - reserved_request_amount, 0)`.

Uma CTE ou subconsulta base deve evitar o fluxo atual de localizar produtos com uma agregação e recalcular os mesmos saldos posteriormente. A mesma semântica dos testes existentes deve ser mantida, especialmente para entradas parcialmente identificadas, separações parciais e produtos equivalentes.

Os totalizadores serão calculados sobre essa base sem filtros. Em seguida, os filtros de interface serão aplicados à consulta da página.

### Stable cursor ordering

Cada ordenação terminará com critérios estáveis:

```text
<campo selecionado> <direção>, description ASC, product_id ASC
```

Quando `description` for o campo principal, `product_id` continua sendo o desempate final. Todos os campos utilizados pelo cursor precisam estar presentes no `SELECT`.

### Frontend flow

- Na carga inicial, o frontend solicita a primeira página e substitui a lista.
- Um `IntersectionObserver` no final do grid solicita `links.next` e acrescenta o próximo lote.
- Durante o carregamento de uma nova página, o spinner aparece no final da lista sem bloquear os produtos já exibidos.
- Alterar filtro ou ordenação cancela ou invalida a requisição anterior, limpa o cursor e solicita novamente a primeira página.
- Durante filtro ou ordenação, o overlay permanece sobre o grid e a camada de interação bloqueia também o botão de filtros.
- As respostas recebem um identificador de geração local. Respostas de uma geração anterior não podem ser anexadas após a troca de filtros ou ordenação.
- Os totalizadores retornados continuam sendo exibidos sem depender da página ou dos filtros.
- A ordenação local de `products` será removida; anexar páginas já ordenadas pelo servidor preservará a ordem global.

### Query profiling and indexes

A consulta final deve ser executada com `EXPLAIN (ANALYZE, BUFFERS)` em uma base representativa. Índices candidatos incluem combinações usadas nos filtros e agrupamentos de `items`, `stock_ios`, `stock_io_products`, `picked_items`, `request_items` e `integrations`, mas somente o plano real determinará quais serão criados.

Índices de expressão sobre campos JSON de integração só devem ser adicionados se o plano mostrar varredura relevante e se a equivalência ou busca por código depender deles.

### Compatibility and rollout

O frontend e o backend precisam ser publicados de forma compatível. Durante a transição, o frontend poderá aceitar ausência de `links.next` como fim da listagem. A remoção de `critical_products` deve ocorrer somente após confirmar que nenhum outro consumidor utiliza esse campo.

## Error Handling

- Valores inválidos de ordenação, direção, quantidade por página, campo de busca ou status retornam erro de validação.
- Falha na primeira página mantém o estado de erro atual e permite tentar novamente.
- Falha ao carregar a próxima página mantém os produtos existentes e oferece nova tentativa no final da lista.
- Requisições antigas são ignoradas quando filtro ou ordenação mudam.
- Um cursor incompatível com os parâmetros atuais não deve misturar resultados; o frontend sempre reinicia o cursor ao mudar parâmetros.

## Testing

### Backend

- Totalizadores consideram todos os produtos mesmo quando a página está filtrada.
- Primeira página e páginas seguintes não repetem nem omitem produtos.
- Ordenação global funciona para cada coluna, incluindo empates.
- Combinações de filtros funcionam com cursor.
- `per_page` possui padrão e limite.
- Regras atuais de disponibilidade continuam cobertas.
- A resposta não duplica produtos em `critical_products` após a remoção compatível.
- Teste de consulta garante quantidade limitada de queries e ausência de N+1.

### Frontend

- A primeira página substitui a lista e a próxima página acrescenta resultados.
- O observador não dispara duas cargas simultâneas.
- Mudança de ordenação ou filtro reinicia a lista e o cursor.
- Respostas obsoletas são descartadas.
- Loading inicial, loading de ordenação e loading de próxima página têm comportamentos distintos.
- Totalizadores não são recalculados com base nos produtos carregados.
- O filtro não altera visualmente os totalizadores globais.

### Performance

- Comparar tempo da primeira resposta, tamanho do JSON e memória do frontend antes e depois.
- Verificar o tempo de resposta ao clique no cabeçalho com uma base representativa.
- Registrar o plano SQL antes e depois dos índices aprovados.

## Open Questions

- Qual volume de dados deve ser usado como referência para o teste de desempenho em homologação?
- Há consumidores externos de `critical_products` além do frontend atual?
- Após a consulta final, será necessário cache curto dos totalizadores ou a execução dinâmica atende ao objetivo?
