DocumentaçãoDocumentationDocumentaciónOne Conecta
ÍndiceIndexÍndice
Baixar .mdDownload .mdBajar .md
Você está vendo esta documentação online. No topo você também pode baixar o PDF (mesmo conteúdo desta página, no idioma e modo atuais) e o Markdown (Funcional ou Técnica).You are viewing this documentation online. At the top you can also download the PDF (same content as this page, in the current language and mode) and the Markdown (Functional or Technical).Está viendo esta documentación en línea. Arriba también puede bajar el PDF (mismo contenido de esta página, en el idioma y modo actuales) y el Markdown (Funcional o Técnica).
Feature · Contagem de estoqueFeature · Stock countFeature · Conteo de stock

Contagem de estoqueStock countConteo de stock

A ferramenta que o representante de vendas usa dentro de uma visita para registrar quanto produto o varejo ainda tem — duas quantidades por item, na unidade de medida Alta e na Baixa. A lista de produtos vem do cache do catálogo, filtrada por categoria e por família de marca; cada cartão ainda mostra uma grade com as últimas quatro contagens. As quantidades são digitadas na tela e enviadas ao backend pelo Dispatcher, sem nunca serem gravadas no aparelho. The tool a sales rep uses inside a visit to record how much product the retail still has — two quantities per item, in the High unit of measure and in the Low one. The product list comes from the catalog cache, filtered by category and brand family; each card also shows a grid of the last four counts. The quantities are typed on screen and sent to the backend through the Dispatcher, never written to the device. La herramienta que el representante de ventas usa dentro de una visita para registrar cuánto producto le queda al punto de venta — dos cantidades por ítem, en la unidad de medida Alta y en la Baja. La lista de productos viene del caché del catálogo, filtrada por categoría y por familia de marca; cada tarjeta muestra además una grilla con los últimos cuatro conteos. Las cantidades se escriben en la pantalla y se envían al backend por el Dispatcher, sin grabarse nunca en el dispositivo.

PúblicoAudiencePúblico
Representante · QA · Suporte · DevRep · QA · Support · DevRepresentante · QA · Soporte · Dev
Onde ficaWhereDónde
Detalhe da visita → Ferramentas · e o encerramento de visita (BR/ZA)Visit detail → Tools · and visit end (BR/ZA)Detalle de la visita → Herramientas · y el cierre de visita (BR/ZA)
RelacionadoRelatedRelacionado
Visit Detail · Price Check · LocationStockUploadAPI (serviceName)
AtualizadoUpdatedActualizado
04/08/20262026-08-04
Disponível emAvailable inDisponible en BR CL ZA
01

O que é e para que serveWhat it is and what it's forQué es y para qué sirve

A Contagem de estoque é um levantamento de campo: durante a visita, o representante de vendas percorre a lista de produtos e anota, produto por produto, quanto ainda existe em estoque naquele varejo. São duas colunas de entrada por produto, nomeadas pela unidade de medida: Alta (a embalagem maior — caixa, pacote fechado) e Baixa (a unidade menor em que o mesmo produto também é contado). Stock count is a field survey: during the visit, the sales rep walks the product list and records, product by product, how much is still in stock at that retail. There are two input columns per product, named after the unit of measure: High (the larger pack — case, sealed carton) and Low (the smaller unit the same product is also counted in). El Conteo de stock es un levantamiento de campo: durante la visita, el representante de ventas recorre la lista de productos y anota, producto por producto, cuánto queda en stock en ese punto de venta. Hay dos columnas de entrada por producto, nombradas por la unidad de medida: Alta (el envase mayor — caja, paquete cerrado) y Baja (la unidad menor en la que el mismo producto también se cuenta).

Quais produtos aparecem?Which products show up?¿Qué productos aparecen?

Os do catálogo do mercado marcados pelo backend como disponíveis para contagem de estoque. A lista é a mesma para todos os varejos — apesar do texto do estado vazio prometer o contrário, ela não é recortada por varejo (ver Pendências).The ones in the market catalog flagged by the backend as available for stock count. The list is the same for every retail — despite what the empty-state copy promises, it is not sliced per retail (see Pending items).Los del catálogo del mercado marcados por el backend como disponibles para conteo de stock. La lista es la misma para todos los puntos de venta — a pesar de lo que promete el texto del estado vacío, no se recorta por punto de venta (ver Pendientes).

O que se digita?What do you type?¿Qué se escribe?

números inteiros. Cada produto tem dois campos independentes; pode-se preencher um, o outro ou os dois. Campo vazio significa "não contado" e o produto simplesmente não entra no envio.Whole numbers only. Each product has two independent fields; you may fill one, the other or both. An empty field means "not counted" and the product simply doesn't go into the submission.Solo números enteros. Cada producto tiene dos campos independientes; se puede completar uno, el otro o los dos. Un campo vacío significa "no contado" y el producto simplemente no entra en el envío.

O que acontece ao enviar?What happens on submit?¿Qué pasa al enviar?

O app confirma num modal, monta uma linha por produto contado e manda tudo num único envio ao backend. Com sucesso, avisa em verde, apaga as contagens e volta para a visita.The app asks for confirmation in a modal, builds one row per counted product and sends everything in a single submission. On success it shows a green notice, wipes the counts and returns to the visit.La app confirma en un modal, arma una fila por producto contado y manda todo en un único envío al backend. Con éxito avisa en verde, borra los conteos y vuelve a la visita.

Duas coisas diferentes chamadas "estoque"Two different things called "stock"Dos cosas distintas llamadas "stock" Esta tela mede o estoque do varejo, na prateleira. Ela não tem nada a ver com o controle de estoque da van — o saldo que o representante carrega no veículo e que alimenta o pedido pronta-entrega, descrito em Prompt. São dois subsistemas separados, que só compartilham a palavra: a contagem de estoque não lê nem atualiza o saldo da van. A grade de histórico que aparece em cada cartão vem do catálogo de produtos, não do saldo da van. This screen measures the retail's stock, on the shelf. It has nothing to do with van stock control — the balance the rep carries in the vehicle that feeds prompt-delivery orders, described in Prompt. They are two separate subsystems that only share the word: stock count neither reads nor updates the van balance. The history grid on each card comes from the product catalog, not from the van balance. Esta pantalla mide el stock del punto de venta, en la estantería. No tiene nada que ver con el control de stock de la van — el saldo que el representante lleva en el vehículo y que alimenta el pedido de entrega inmediata, descrito en Prompt. Son dos subsistemas separados que solo comparten la palabra: el conteo de stock no lee ni actualiza el saldo de la van. La grilla de historial de cada tarjeta viene del catálogo de productos, no del saldo de la van.

02

Como acessarHow to openCómo acceder

  1. Caminho principal: a grade de ferramentas da visitaMain path: the visit tools gridCamino principal: la grilla de herramientas de la visitaAbra o Detalhe da visita, desça até Ferramentas e toque no tile da contagem de estoque. Esse é o acesso disponível nos três mercados. Não há aba inferior, item de menu lateral nem atalho na Home.Open the Visit detail, scroll to Tools and tap the stock count tile. This is the entry available in all three markets. There is no bottom tab, no side-menu item and no Home shortcut.Abra el Detalle de la visita, baje hasta Herramientas y toque el tile del conteo de stock. Ese es el acceso disponible en los tres mercados. No hay pestaña inferior, ítem de menú lateral ni atajo en el Home.
  2. Segundo caminho: a lista de pendências do encerramento de visitaSecond path: the visit-end pending listSegundo camino: la lista de pendientes del cierre de visitaAo encerrar a visita, o app lista o que ficou pendente. Se a contagem de estoque estiver pendente, o cartão dela é tocável e abre esta mesma tela; ao voltar, a lista de pendências é recalculada. Esse caminho existe apenas no Brasil e na África do Sul — o Chile tem o tile, mas não declara a categoria de pendência.When closing the visit, the app lists what is still pending. If stock count is pending, its card is tappable and opens this same screen; on return, the pending list is recomputed. This path exists only in Brazil and South Africa — Chile has the tile but doesn't declare the pending category.Al cerrar la visita, la app lista lo que quedó pendiente. Si el conteo de stock está pendiente, su tarjeta es tocable y abre esta misma pantalla; al volver, la lista de pendientes se recalcula. Ese camino existe solo en Brasil y Sudáfrica — Chile tiene el tile, pero no declara la categoría de pendiente.
  3. A visita tem de estar iniciadaThe visit must be startedLa visita tiene que estar iniciadaTodo tile da grade passa por uma verificação: se a visita ainda não foi iniciada, aparece um modal oferecendo iniciá-la. Confirmar inicia a visita mas não abre a ferramenta — é preciso tocar no tile de novo. O cartão de pendência do encerramento não passa por essa verificação (a visita já está em curso).Every tile in the grid goes through a check: if the visit hasn't been started, a modal appears offering to start it. Confirming starts the visit but does not open the tool — you have to tap the tile again. The visit-end pending card skips that check (the visit is already underway).Todo tile de la grilla pasa por una verificación: si la visita todavía no fue iniciada, aparece un modal ofreciendo iniciarla. Confirmar inicia la visita pero no abre la herramienta — hay que tocar el tile de nuevo. La tarjeta de pendiente del cierre no pasa por esa verificación (la visita ya está en curso).
  4. A tela abre com a lista prontaThe screen opens with the list readyLa pantalla abre con la lista listaO que a ferramenta recebe é só o identificador do varejo — nem a visita nem o código SAP são passados adiante; a própria tela rebusca a visita no cache, e só no momento do envio. Os produtos vêm do catálogo já sincronizado; puxar a lista para baixo rebusca o catálogo no servidor.All the tool receives is the retail identifier — neither the visit nor the SAP code is passed along; the screen re-fetches the visit from cache, and only at submit time. The products come from the already-synced catalog; pulling the list down re-fetches the catalog from the server.Lo que la herramienta recibe es solo el identificador del punto de venta — ni la visita ni el código SAP se pasan adelante; la propia pantalla vuelve a buscar la visita en el caché, y solo en el momento del envío. Los productos vienen del catálogo ya sincronizado; deslizar la lista hacia abajo vuelve a buscar el catálogo en el servidor.

A chave de configuração se chama stock_history, não stock_countThe config key is called stock_history, not stock_countLa clave de configuración se llama stock_history, no stock_count Quem for procurar o interruptor desta feature no End Market Configuration não vai achar nenhuma chave stock_count na grade de ferramentas: o tile é declarado como stock_history (nome herdado de quando a tela só mostrava o histórico), e é esse item que dispara a navegação para a contagem. A chave stock_count existe, mas em outro lugar — é o nome da categoria de pendência no encerramento de visita. Duas chaves, dois gates distintos, e é comum confundi-los. Vale o mesmo aviso do lado da transação: "LocationStockUploadAPI" existe como serviceNamenão há arquivo build_location_stock_upload_…; o builder real é BuildStockCountDispatcherPayloadUseCase. Anyone looking for this feature's switch in the End Market Configuration will find no stock_count key in the tools grid: the tile is declared as stock_history (a name inherited from when the screen only showed history), and that item is what triggers navigation to the count. The stock_count key does exist, but elsewhere — it is the name of the visit-end pending category. Two keys, two distinct gates, and they are easy to confuse. The same warning applies on the transaction side: "LocationStockUploadAPI" exists only as the serviceName — there is no build_location_stock_upload_… file; the real builder is BuildStockCountDispatcherPayloadUseCase. Quien busque el interruptor de esta feature en el End Market Configuration no va a encontrar ninguna clave stock_count en la grilla de herramientas: el tile se declara como stock_history (nombre heredado de cuando la pantalla solo mostraba el historial), y es ese ítem el que dispara la navegación al conteo. La clave stock_count existe, pero en otro lugar — es el nombre de la categoría de pendiente en el cierre de visita. Dos claves, dos gates distintos, y es común confundirlos. Vale el mismo aviso del lado de la transacción: "LocationStockUploadAPI" existe solo como serviceNameno hay archivo build_location_stock_upload_…; el builder real es BuildStockCountDispatcherPayloadUseCase.

03

Estrutura da telaScreen structureEstructura de la pantalla

Última sincronizaçãoLast syncÚltima sincronización
Faixa no topo com a data e hora em que o catálogo de produtos foi baixado — não é a data da contagem.A strip at the top with the date and time the product catalog was downloaded — not the date of the count.Franja arriba con la fecha y hora en que se bajó el catálogo de productos — no es la fecha del conteo.
CabeçalhoHeaderEncabezado
Ícone da ferramenta e o título Contagem de estoque. Não há cartão do varejo aqui — o nome do varejo fica na tela da visita.The tool icon and the Stock count title. There is no retail card here — the retail name stays on the visit screen.Ícono de la herramienta y el título Conteo de stock. No hay tarjeta del punto de venta aquí — el nombre queda en la pantalla de la visita.
Dois filtros em cascataTwo cascading filtersDos filtros en cascada
Duas listas de seleção: Categoria e Marca. A de marca só fica habilitada depois de escolher uma categoria, e mostra apenas as marcas daquela categoria. Escolher uma categoria nova limpa a marca. Qualquer mudança de filtro volta a lista para os primeiros 20 produtos.Two pickers: Category and Brand. The brand one is enabled only after a category is chosen, and shows only that category's brands. Choosing a new category clears the brand. Any filter change resets the list to the first 20 products.Dos listas de selección: Categoría y Marca. La de marca se habilita solo después de elegir una categoría, y muestra solo las marcas de esa categoría. Elegir una categoría nueva limpia la marca. Cualquier cambio de filtro devuelve la lista a los primeros 20 productos.
Cabeçalho de colunasColumn headerEncabezado de columnas
Faixa única acima da lista: Marca à esquerda (com Histórico de contagem de estoque como legenda) e os rótulos Alta e Baixa alinhados exatamente sobre os dois campos de entrada. Desaparece quando não há produtos.A single strip above the list: Brand on the left (with Stock count history as a caption) and the High and Low labels aligned exactly over the two input fields. It disappears when there are no products.Franja única arriba de la lista: Marca a la izquierda (con Historial de conteo de stock como leyenda) y los rótulos Alta y Baja alineados exactamente sobre los dos campos de entrada. Desaparece cuando no hay productos.
Cartão de produtoProduct cardTarjeta de producto
Um cartão por produto, com borda fina e cantos bem arredondados. Em cima: botão de informação, nome do produto (até 2 linhas) e os dois campos-pílula de quantidade. Embaixo: a grade de histórico.One card per product, with a thin border and strongly rounded corners. On top: info button, product name (up to 2 lines) and the two pill fields for quantity. Below: the history grid.Una tarjeta por producto, con borde fino y esquinas bien redondeadas. Arriba: botón de información, nombre del producto (hasta 2 líneas) y los dos campos-píldora de cantidad. Abajo: la grilla de historial.
Grade de histórico (4 contagens)History grid (4 counts)Grilla de historial (4 conteos)
Mini-tabela dentro do cartão: as datas viram colunas (pílulas azuis no topo, as 4 contagens mais recentes primeiro) e Alta / Baixa são rótulos de linha à esquerda, escritos uma única vez. Célula sem dado mostra -. Quando há menos de 4 contagens, as colunas restantes ficam com -.A mini table inside the card: dates become columns (blue pills on top, the 4 most recent counts first) and High / Low are row labels on the left, written once. A cell with no data shows -. When there are fewer than 4 counts, the remaining columns hold -.Mini-tabla dentro de la tarjeta: las fechas se vuelven columnas (píldoras azules arriba, los 4 conteos más recientes primero) y Alta / Baja son rótulos de fila a la izquierda, escritos una sola vez. Una celda sin dato muestra -. Cuando hay menos de 4 conteos, las columnas restantes quedan con -.
Botão de informação do produtoProduct info buttonBotón de información del producto
O ícone à esquerda do nome abre um modal com o nome e a foto do produto. É o mesmo componente usado na vitrine de pedido e na verificação de preço.The icon left of the name opens a modal with the product's name and photo. It's the same component used in the order showcase and in price check.El ícono a la izquierda del nombre abre un modal con el nombre y la foto del producto. Es el mismo componente usado en la vitrina de pedido y en la verificación de precio.
Carregamento por rolagemScroll loadingCarga por desplazamiento
A lista começa com 20 produtos e cresce de 20 em 20 conforme se rola, com uma pausa curta antes de cada leva. Enquanto houver produtos por carregar, uma rodinha fica embaixo da última linha.The list starts with 20 products and grows 20 at a time as you scroll, with a short pause before each batch. While products remain to load, a spinner sits under the last row.La lista empieza con 20 productos y crece de 20 en 20 al desplazar, con una pausa corta antes de cada lote. Mientras queden productos por cargar, una rueda queda debajo de la última fila.
Contador "X de Y""X of Y" counterContador "X de Y"
Abaixo da lista: quantos produtos já estão na tela do total filtrado.Below the list: how many products are on screen out of the filtered total.Debajo de la lista: cuántos productos ya están en pantalla del total filtrado.
Barra de resumoSummary barBarra de resumen
Barra fixa no rodapé que só aparece depois da primeira quantidade digitada. Fechada, mostra três números — preenchidos, soma da Alta, soma da Baixa — e o botão de enviar.A bar pinned to the bottom that only appears after the first quantity is typed. Collapsed, it shows three numbers — filled, High total, Low total — and the submit button.Barra fija al pie que solo aparece después de la primera cantidad escrita. Cerrada, muestra tres números — llenados, suma de Alta, suma de Baja — y el botón de enviar.
Painel expandido do resumoExpanded summary panelPanel expandido del resumen
Arrastar a barra para cima abre a conferência: os produtos contados agrupados por categoria, com subtotal de Alta e de Baixa por categoria, cinco números no rodapé (total de produtos, preenchidos, vazios, soma da Alta, soma da Baixa) e o botão Limpar tudo. Produto sem contagem não aparece aqui; coluna não preenchida aparece com um marcador de vazio.Dragging the bar up opens the review: counted products grouped by category, with a High and a Low subtotal per category, five numbers in the footer (total products, filled, empty, High total, Low total) and the Clear all button. A product with no count isn't listed here; an unfilled column shows an empty marker.Arrastrar la barra hacia arriba abre la revisión: los productos contados agrupados por categoría, con subtotal de Alta y de Baja por categoría, cinco números al pie (total de productos, llenados, vacíos, suma de Alta, suma de Baja) y el botón Limpiar todo. Un producto sin conteo no aparece aquí; una columna no completada muestra un marcador de vacío.
Puxar para atualizarPull to refreshDeslizar para actualizar
Rebusca o catálogo no servidor. As quantidades já digitadas e os filtros ativos são preservados; a lista volta a mostrar os primeiros 20 produtos.Re-fetches the catalog from the server. Quantities already typed and the active filters are preserved; the list goes back to showing the first 20 products.Vuelve a buscar el catálogo en el servidor. Las cantidades ya escritas y los filtros activos se preservan; la lista vuelve a mostrar los primeros 20 productos.

A tela não tem busca, ordenação nem abas. A lista sai na ordem em que o backend entrega o catálogo. Comparada com a feature irmã — a verificação de preço, que tem a mesma anatomia de lista, dois campos por linha e barra de resumo —, a contagem de estoque ganha os dois filtros e a grade de histórico, e perde o cartão único que envolvia a tabela: aqui cada produto é um cartão próprio.The screen has no search, no sorting and no tabs. The list comes out in the order the backend delivers the catalog. Compared with its sibling feature — price check, which has the same list anatomy, two fields per row and a summary bar — stock count gains the two filters and the history grid, and loses the single card that wrapped the table: here each product is its own card.La pantalla no tiene búsqueda, orden ni pestañas. La lista sale en el orden en que el backend entrega el catálogo. Comparada con la feature hermana — la verificación de precio, que tiene la misma anatomía de lista, dos campos por fila y barra de resumen —, el conteo de stock gana los dos filtros y la grilla de historial, y pierde la tarjeta única que envolvía la tabla: aquí cada producto es una tarjeta propia.

04

Estados da telaScreen statesEstados de la pantalla

A contagem de estoque não tem "status" de negócio — nada é aprovado, rejeitado ou fica pendente dentro da tela. O que existe são os estados da própria tela (e, fora dela, a pendência do encerramento de visita):Stock count has no business "status" — nothing gets approved, rejected or left pending inside the screen. What exists are the screen's own states (and, outside it, the visit-end pending):El conteo de stock no tiene "estado" de negocio — nada se aprueba, se rechaza ni queda pendiente dentro de la pantalla. Lo que existe son los estados de la propia pantalla (y, fuera de ella, el pendiente del cierre de visita):

CarregandoLoadingCargando
Rodinha centralizada com o logo do app, cobrindo a tela inteira, enquanto o catálogo é lido.A centered spinner with the app logo, covering the whole screen, while the catalog is read.Rueda centrada con el logo de la app, cubriendo la pantalla entera, mientras se lee el catálogo.
ErroErrorError
Tela de falha com botão de tentar de novo, que recarrega a ferramenta do zero. Acontece quando o catálogo não pode ser lido nem do cache nem do servidor.A failure screen with a retry button that reloads the tool from scratch. It happens when the catalog can be read neither from cache nor from the server.Pantalla de fallo con botón de reintentar, que recarga la herramienta desde cero. Ocurre cuando el catálogo no puede leerse ni del caché ni del servidor.
Lista vaziaEmpty listLista vacía
Quando o filtro (ou o catálogo) não devolve nenhum produto, aparece o cartão de estado vazio com ícone e a mensagem "nenhum produto com histórico de vendas para este varejo". O cabeçalho de colunas desaparece junto. Vale notar que a mensagem descreve um filtro que o código não aplica (ver Pendências).When the filter (or the catalog) returns no product, the empty-state card appears with an icon and the message "no products with sales history for this retail". The column header disappears with it. Note that the message describes a filter the code doesn't apply (see Pending items).Cuando el filtro (o el catálogo) no devuelve ningún producto, aparece la tarjeta de estado vacío con ícono y el mensaje "sin productos con historial de ventas para este punto de venta". El encabezado de columnas desaparece junto. Cabe notar que el mensaje describe un filtro que el código no aplica (ver Pendientes).
Lista sem nada preenchidoList with nothing filledLista sin nada completado
Estado inicial: todos os campos vazios e sem barra de resumo. Não há como enviar nesse estado.Initial state: all fields empty and no summary bar. There is no way to submit in this state.Estado inicial: todos los campos vacíos y sin barra de resumen. No hay manera de enviar en ese estado.
EnviandoSubmittingEnviando
O botão de enviar mostra carregamento e para de aceitar toque. Os campos continuam editáveis.The submit button shows a loading state and stops accepting taps. The fields stay editable.El botón de enviar muestra carga y deja de aceptar toques. Los campos siguen editables.
Pendência no encerramento (BR/ZA)Visit-end pending (BR/ZA)Pendiente en el cierre (BR/ZA)
Fora da tela: se algum produto do catálogo tiver histórico de contagem para aquele varejo, a contagem é considerada devida na visita e aparece na lista de pendências do encerramento. Marcada como não bloqueante — nunca impede finalizar a visita. O contador é sempre 1, nunca o número de produtos.Outside the screen: if any catalog product has count history for that retail, the count is considered due in the visit and shows up in the visit-end pending list. Flagged as non-blocking — it never prevents finishing the visit. The counter is always 1, never the number of products.Fuera de la pantalla: si algún producto del catálogo tiene historial de conteo para ese punto de venta, el conteo se considera debido en la visita y aparece en la lista de pendientes del cierre. Marcado como no bloqueante — nunca impide finalizar la visita. El contador es siempre 1, nunca el número de productos.

Nada é salvo em rascunhoNothing is saved as a draftNada se guarda como borrador As quantidades digitadas vivem só na memória da tela. Sair pelo botão de voltar — sem modal de confirmação — descarta tudo, e fechar o app também. Depois de um envio bem-sucedido as quantidades são apagadas de propósito, e reabrir a ferramenta mostra a lista completa com todos os campos vazios: o app não guarda marca de "já contado" no domínio. O único vestígio de que a contagem aconteceu é o registro de despacho, e é exatamente ele que o encerramento de visita consulta. The typed quantities live only in the screen's memory. Leaving via the back button — with no confirmation modal — discards everything, and so does killing the app. After a successful submission the quantities are wiped on purpose, and reopening the tool shows the full list with every field empty: the app keeps no "already counted" marker in the domain. The only trace that the count happened is the dispatch record, and that is exactly what visit end consults. Las cantidades escritas viven solo en la memoria de la pantalla. Salir por el botón de volver — sin modal de confirmación — descarta todo, y cerrar la app también. Tras un envío exitoso las cantidades se borran a propósito, y reabrir la herramienta muestra la lista completa con todos los campos vacíos: la app no guarda marca de "ya contado" en el dominio. El único vestigio de que el conteo ocurrió es el registro de despacho, y es exactamente lo que consulta el cierre de visita.

05

Contar e enviarCount and submitContar y enviar

Filtrar a listaFiltering the listFiltrar la lista

Escolha uma categoria para reduzir a lista; só então a lista de marcas fica utilizável, já restrita às marcas daquela categoria. As categorias aparecem na ordem fixa do app (não em ordem alfabética); as marcas, em ordem alfabética. Cada lista tem sua própria opção de limpar. Filtrar não apaga o que já foi digitado: as quantidades de um produto que saiu da lista continuam guardadas e continuam entrando no envio.Pick a category to narrow the list; only then does the brand picker become usable, already restricted to that category's brands. Categories appear in the app's fixed order (not alphabetically); brands, alphabetically. Each picker has its own clear option. Filtering does not erase what was already typed: quantities for a product that left the list stay stored and still go into the submission.Elija una categoría para reducir la lista; solo entonces la lista de marcas queda utilizable, ya restringida a las marcas de esa categoría. Las categorías aparecen en el orden fijo de la app (no alfabético); las marcas, en orden alfabético. Cada lista tiene su propia opción de limpiar. Filtrar no borra lo ya escrito: las cantidades de un producto que salió de la lista siguen guardadas y siguen entrando en el envío.

Digitar uma quantidadeTyping a quantityEscribir una cantidad

Os dois campos de cada cartão aceitam apenas dígitos — ponto, vírgula e sinal de menos são recusados na digitação, e o teclado abre no modo numérico sem decimal. Ou seja: não é possível contar meia caixa; a contagem é sempre um número inteiro. Apagar o conteúdo volta o campo a "não contado": se as duas colunas do produto ficarem vazias, ele sai do resumo e do envio como se nunca tivesse sido tocado. O outro campo do mesmo produto nunca é afetado ao editar um deles.Both fields on each card accept digits only — dot, comma and minus sign are refused as you type, and the keyboard opens in numeric mode without a decimal. In other words: you cannot count half a case; a count is always a whole number. Clearing the content returns the field to "not counted": if both of a product's columns end up empty, it drops out of the summary and out of the submission as if never touched. Editing one field never affects the other on the same product.Los dos campos de cada tarjeta aceptan solo dígitos — punto, coma y signo menos se rechazan al escribir, y el teclado abre en modo numérico sin decimal. O sea: no se puede contar media caja; el conteo es siempre un número entero. Borrar el contenido devuelve el campo a "no contado": si las dos columnas del producto quedan vacías, sale del resumen y del envío como si nunca hubiera sido tocado. Editar un campo nunca afecta al otro del mismo producto.

Por ser inteiro e sem separadores, a leitura do número não depende da convenção do mercado — a contagem de estoque não tem o problema de vírgula-versus-ponto que a verificação de preço tem.Being integer and separator-free, reading the number doesn't depend on market convention — stock count doesn't have the comma-versus-dot problem price check has.Al ser entero y sin separadores, la lectura del número no depende de la convención del mercado — el conteo de stock no tiene el problema de coma-versus-punto que tiene la verificación de precio.

Limpar tudoClear allLimpiar todo

Dentro do painel expandido do resumo. Abre um modal de confirmação; confirmando, todas as quantidades são apagadas de uma vez — inclusive as de produtos escondidos pelo filtro — e a barra de resumo desaparece. Os campos da lista se limpam junto, inclusive o que estiver com o cursor dentro. Os filtros não são afetados.Inside the expanded summary panel. It opens a confirmation modal; on confirm, all quantities are wiped at once — including those of products hidden by the filter — and the summary bar disappears. The list fields clear along with it, including one that currently has the cursor in it. The filters are not affected.Dentro del panel expandido del resumen. Abre un modal de confirmación; al confirmar, todas las cantidades se borran de una vez — incluidas las de productos escondidos por el filtro — y la barra de resumen desaparece. Los campos de la lista se limpian junto, incluso el que tenga el cursor dentro. Los filtros no se afectan.

EnviarSubmitEnviar

  1. ConfirmaçãoConfirmationConfirmaciónO botão da barra de resumo abre um modal avisando que a contagem será enviada. Cancelar volta à lista sem enviar nada.The summary-bar button opens a modal warning that the count will be sent. Cancelling returns to the list without sending anything.El botón de la barra de resumen abre un modal avisando que el conteo será enviado. Cancelar vuelve a la lista sin enviar nada.
  2. EnvioSubmissionEnvíoO app monta uma linha por produto contado — varejo, produto, lote, as duas quantidades, os nomes das duas unidades de medida, a visita e o representante — e manda tudo num único envio. Produtos sem contagem não entram.The app builds one row per counted product — retail, product, batch, both quantities, both unit-of-measure names, the visit and the rep — and sends it all in a single submission. Products with no count are left out.La app arma una fila por producto contado — punto de venta, producto, lote, las dos cantidades, los nombres de las dos unidades de medida, la visita y el representante — y manda todo en un único envío. Los productos sin conteo no entran.
  3. Resultado em telaOn-screen resultResultado en pantallaSucesso mostra o aviso verde, apaga as quantidades e volta para o detalhe da visita. Falha mostra o aviso vermelho (com o código do erro) e mantém a tela como está, com tudo preenchido, para tentar de novo — inclusive o que foi digitado enquanto o envio estava em voo.Success shows the green notice, wipes the quantities and returns to the visit detail. Failure shows the red notice (with the error code) and leaves the screen as it is, everything filled in, so you can retry — including whatever was typed while the submission was in flight.El éxito muestra el aviso verde, borra las cantidades y vuelve al detalle de la visita. El fallo muestra el aviso rojo (con el código del error) y deja la pantalla como está, con todo completado, para reintentar — incluso lo que se escribió mientras el envío estaba en vuelo.

Sem conexão o envio falha na hora — e reenviar pode duplicarOffline the submission fails right away — and resending may duplicateSin conexión el envío falla en el momento — y reenviar puede duplicar A contagem de estoque não entra na fila de reenvio automático do Dispatcher — offline ela vai ao transporte, falha e é registrada como erro. O registro fica visível na Central de dados e pode ser reenviado à mão, mas — ao contrário da verificação de preço — a contagem não é um dos tipos marcados como reenvio seguro, então repetir o envio traz aviso de possível duplicidade. Nada bloqueia o encerramento da visita por causa de uma contagem pendente; pior, um envio que falhou ainda marca a pendência como resolvida (ver Pendências). Stock count does not enter the Dispatcher's automatic retry queue — offline it goes to transport, fails and is recorded as an error. The record shows up in the Data center and can be resent by hand, but — unlike price check — the count is not one of the types flagged as safe to resend, so repeating the submission carries a possible-duplicate warning. Nothing blocks closing the visit over a pending count; worse, a submission that failed still marks the pending as resolved (see Pending items). El conteo de stock no entra en la cola de reenvío automático del Dispatcher — offline va al transporte, falla y se registra como error. El registro queda visible en la Central de datos y puede reenviarse a mano, pero — a diferencia de la verificación de precio — el conteo no es uno de los tipos marcados como reenvío seguro, así que repetir el envío trae aviso de posible duplicidad. Nada bloquea el cierre de la visita por un conteo pendiente; peor, un envío que falló igual marca el pendiente como resuelto (ver Pendientes).

06

Arquitetura e fluxo de dadosArchitecture & data flowArquitectura y flujo de datos

Clean Architecture + Riverpod + Freezed + gRPC + ObjectBox, com dois caminhos que não se cruzam. A leitura é do catálogo de produtos — um agregado compartilhado por várias features (§29 do CLAUDE.md): um único RPC traz o catálogo do mercado inteiro, que é gravado no ObjectBox, e a contagem de estoque lê de lá através do seu UseCase próprio, que filtra pela flag de elegibilidade. A escrita não usa esse proto nem nenhum outro: as quantidades digitadas vão como JSON pelo Dispatcher.Clean Architecture + Riverpod + Freezed + gRPC + ObjectBox, with two paths that never cross. Reading is the product catalog — an aggregate shared by several features (CLAUDE.md §29): a single RPC brings the whole market catalog, it is written to ObjectBox, and stock count reads from there through its own UseCase, which filters on the eligibility flag. Writing uses neither that proto nor any other: the typed quantities go as JSON through the Dispatcher.Clean Architecture + Riverpod + Freezed + gRPC + ObjectBox, con dos caminos que no se cruzan. La lectura es del catálogo de productos — un agregado compartido por varias features (§29 del CLAUDE.md): un único RPC trae el catálogo del mercado entero, que se graba en ObjectBox, y el conteo de stock lee de allí a través de su UseCase propio, que filtra por la flag de elegibilidad. La escritura no usa ese proto ni ningún otro: las cantidades escritas van como JSON por el Dispatcher.

Leitura — catálogo de produtosRead — product catalogLectura — catálogo de productos

  • ProductCatalogReplygRPC proto
    • toDTOProductCatalogDTODTO · Freezed
      • toDomain · compute (isolate)ProductCatalogEntitydomain · lastSyncAt gerado aqui
        • toModelProductCatalogModelObjectBox · 11 boxes
          • toDomainProductCatalogEntitydomain · cache
            • GetProductsForStockCountUseCaseStockCountNotifier + Statefiltra isAvailableForStockCount
              • → UIStockCountPage

Escrita — contagensWrite — countsEscritura — conteos

  • StockCountState.entriesMap<productSfid, StockCountEntryEntity>
    • submit · + resource + visit (cache)StockCountDispatcherPayloadInputentities cruas · 7 campos
      • buildDispatcherEnvelope1 envelope · sem fatiamento
        • SubmitStockCountUseCaseDispatcherOrchestrator
          • sendTransactionLocationStockUploadAPIgRPC · Dispatcher · salesforce
            • ackDispatchTransactionhistórico local · lido pelo encerramento de visita

Os dois caminhos não se encontram — e nem tocam o estoque da vanThe two paths never meet — and neither touches van stockLos dos caminos no se encuentran — y ninguno toca el stock de la van O envio não grava nada no domínio do catálogo nem em nenhuma box da feature: as quantidades digitadas nunca são persistidas, nem antes nem depois do envio. StockCountEntryEntity existe só na camada de domínio — não tem proto, DTO nem Model, e não há pasta stock_count em lib/data. A única gravação local que um envio produz é o registro de auditoria do despacho. E, apesar do nome, o StockControlConectaRep.proto (o saldo da van) não é lido nem atualizado por esta feature: o grep de StockControl dentro de lib/presentation/stock_count/ devolve zero. A transação vive em 26 · LocationStockUpload. Submission writes nothing into the catalog domain nor into any box of the feature: the typed quantities are never persisted, before or after sending. StockCountEntryEntity exists only in the domain layer — it has no proto, no DTO and no Model, and there is no stock_count folder under lib/data. The only local write a submission produces is the dispatch audit record. And, despite the name, StockControlConectaRep.proto (the van balance) is neither read nor updated by this feature: grepping StockControl inside lib/presentation/stock_count/ returns zero. The transaction lives in 26 · LocationStockUpload. El envío no graba nada en el dominio del catálogo ni en ninguna box de la feature: las cantidades escritas nunca se persisten, ni antes ni después del envío. StockCountEntryEntity existe solo en la capa de dominio — no tiene proto, DTO ni Model, y no hay carpeta stock_count en lib/data. La única grabación local que un envío produce es el registro de auditoría del despacho. Y, a pesar del nombre, el StockControlConectaRep.proto (el saldo de la van) no se lee ni se actualiza por esta feature: el grep de StockControl dentro de lib/presentation/stock_count/ devuelve cero. La transacción vive en 26 · LocationStockUpload.

07

Modelo de dadosData modelModelo de datos

O catálogo de produtos existe em quatro representações quase idênticas ao longo das camadas — Proto (wire gRPC) → DTO (Freezed) → Model (ObjectBox) → Entity (domínio) — e cada fronteira é atravessada por um mapper. Os nomes se mantêm em todas as camadas e o que muda é pouquíssimo: as relações viram ToOne/ToMany no Model, um campo é renomeado no Proto e o lastSyncAt não existe no wire. O fetch é write-through: todo retorno remoto é gravado no ObjectBox (a gravação limpa e regrava 11 boxes) e a tela passa a ler do cache.The product catalog exists in four near-identical representations across the layers — Proto (gRPC wire) → DTO (Freezed) → Model (ObjectBox) → Entity (domain) — and each boundary is crossed by a mapper. Names stay the same across layers and very little changes: relations become ToOne/ToMany in the Model, one field is renamed in the Proto, and lastSyncAt doesn't exist on the wire. Fetch is write-through: every remote response is written to ObjectBox (the write clears and rewrites 11 boxes) and the screen then reads from cache.El catálogo de productos existe en cuatro representaciones casi idénticas a lo largo de las capas — Proto (wire gRPC) → DTO (Freezed) → Model (ObjectBox) → Entity (dominio) — y cada frontera se cruza con un mapper. Los nombres se mantienen en todas las capas y cambia muy poco: las relaciones pasan a ToOne/ToMany en el Model, un campo se renombra en el Proto y el lastSyncAt no existe en el wire. El fetch es write-through: toda respuesta remota se graba en ObjectBox (la grabación limpia y regraba 11 boxes) y la pantalla lee del caché.

O catálogo chega num container ProductCatalogEntity de 2 campos (lastSyncAt gerado no mapper + products[]); cada item é um Product de 24 campos, com 9 sub-estruturas aninhadas. Desses 24 campos, a contagem de estoque lê oito: productSfid, name, categoryGroup e brandFamily (filtros e agrupamento), o bloco eligibility (para filtrar), salesHistory (a grade de histórico), uom (os nomes das duas unidades no payload) e manufacturingSkus (lote e SKU no payload). Os outros 16 são carregados e ignorados — inclusive pricingByGroup, ou seja, a tela nunca resolve preço, e halfPack, ou seja, não há meia-embalagem aqui. Existe ainda uma décima segunda estrutura, StockCountEntry, que só existe no domínio: é o que o representante digita, e não tem proto, DTO nem Model. A seguir, na ordem: o proto, as estruturas de dados campo-a-campo por camada, e os mappers.The catalog arrives in a 2-field ProductCatalogEntity container (lastSyncAt generated in the mapper + products[]); each item is a 24-field Product with 9 nested sub-structures. Of those 24 fields, stock count reads eight: productSfid, name, categoryGroup and brandFamily (filters and grouping), the eligibility block (to filter), salesHistory (the history grid), uom (both unit names in the payload) and manufacturingSkus (batch and SKU in the payload). The other 16 are loaded and ignored — including pricingByGroup, i.e. the screen never resolves a price, and halfPack, i.e. there is no half-pack here. There is also a twelfth structure, StockCountEntry, which exists only in the domain: it is what the rep types, and it has no proto, no DTO and no Model. Next, in order: the proto, the field-by-field data structures per layer, and the mappers.El catálogo llega en un container ProductCatalogEntity de 2 campos (lastSyncAt generado en el mapper + products[]); cada ítem es un Product de 24 campos, con 9 sub-estructuras anidadas. De esos 24 campos, el conteo de stock lee ocho: productSfid, name, categoryGroup y brandFamily (filtros y agrupamiento), el bloque eligibility (para filtrar), salesHistory (la grilla de historial), uom (los nombres de las dos unidades en el payload) y manufacturingSkus (lote y SKU en el payload). Los otros 16 se cargan y se ignoran — incluido pricingByGroup, o sea, la pantalla nunca resuelve precio, y halfPack, o sea, no hay media-envase aquí. Existe además una duodécima estructura, StockCountEntry, que solo existe en el dominio: es lo que el representante escribe, y no tiene proto, DTO ni Model. A continuación, en orden: el proto, las estructuras de datos campo a campo por capa, y los mappers.

Proto

ProductCatalogConectaRep.proto · proto3 · package mn.bat.conectarep.streambridge. Um serviço (ProductCatalogConectaRepService), um método unário, 12 messages e nenhum enum. A contagem de estoque não tem proto de escrita — o envio é JSON pelo Dispatcher:One service (ProductCatalogConectaRepService), a single unary method, 12 messages and no enums. Stock count has no write proto — the submission is JSON through the Dispatcher:Un servicio (ProductCatalogConectaRepService), un método unario, 12 messages y ningún enum. El conteo de stock no tiene proto de escritura — el envío es JSON por el Dispatcher:

getProductCatalogunary
MétodoMethodMétodo

rpc getProductCatalog(ProductCatalogRequest) returns (ProductCatalogReply)

path /mn.bat.conectarep.streambridge.ProductCatalogConectaRepService/getProductCatalog

Request · ProductCatalogRequest
locationHierarchySfid
string · #1 · hierarquia do representante de vendas (resolvida no repository, §25)sales rep hierarchy (resolved in the repository, §25)jerarquía del representante de ventas (resuelta en el repository, §25)
dateReference
string · #2 · optional — o datasource aceita, e o único caller não passa (ver Pendências)optional — the datasource accepts it, and the only caller never passes it (see Pending items)optional — el datasource lo acepta, y el único caller no lo pasa (ver Pendientes)
lastModifiedDate
string · #3 · optional — não plumbado em camada nenhuma (ver Pendências)optional — not plumbed in any layer (see Pending items)optional — no plumbeado en ninguna capa (ver Pendientes)
Reply · ProductCatalogReply

repeated Product productso catálogo do mercado inteiro, sem recorte por varejo. Os 24 campos de Product e as 9 sub-estruturas estão detalhados nas Estruturas de dados abaixo.the whole market catalog, with no per-retail slice. Product's 24 fields and the 9 sub-structures are detailed in Data structures below.el catálogo del mercado entero, sin recorte por punto de venta. Los 24 campos de Product y las 9 sub-estructuras están detallados en Estructuras de datos abajo.

Estruturas de dadosData structuresEstructuras de datos

Um dropdown por estrutura, aninhados pela hierarquia. Cada tabela tem uma coluna por camada — Proto · DTO · Model · Entity; o texto em destaque marca onde o tipo primeiro muda. ¹ = optional no proto. Todo Model tem ainda um @Id() int id que não aparece nas outras camadas e não está contado.One dropdown per structure, nested by hierarchy. Each table has one column per layer — Proto · DTO · Model · Entity; the highlighted text marks where the type first changes. ¹ = optional in the proto. Every Model also carries an @Id() int id that doesn't appear in the other layers and isn't counted.Un dropdown por estructura, anidados por jerarquía. Cada tabla tiene una columna por capa — Proto · DTO · Model · Entity; el texto destacado marca dónde primero cambia el tipo. ¹ = optional en el proto. Todo Model lleva además un @Id() int id que no aparece en las otras capas y no está contado.

  • ProductCatalog raiz 2 campos
    CampoProtoDTOModelEntity
    lastSyncAtDateTimeDateTime
    productsrepeated ProductList<ProductDTO>ToMany<ProductModel>List<ProductEntity>
    • Product ProductCatalog.products[] 24 campos
      CampoProtoDTOModelEntity
      productSfidstringStringStringString
      namestringStringStringString
      codestringStringStringString
      erpNumberstringStringStringString
      imageUrlstringStringStringString
      sequenceint32intintint
      categorystringStringStringString
      categoryGroupstringStringStringString
      brandFamilystringStringStringString
      materialGroupstringStringStringString
      brandVariantstringStringStringString
      packContentSizestringStringStringString
      isFreeOfChargeboolboolboolbool
      isVatRetention ¹boolbool?bool?bool?
      splitGroupstringStringStringString
      internalIdstringStringStringString
      invoiceUomstringStringStringString
      uomProductUomProductUomDTOToOne<ProductUomModel>ProductUomEntity
      halfPackProductHalfPackProductHalfPackDTOToOne<ProductHalfPackModel>ProductHalfPackEntity
      eligibilityProductEligibilityProductEligibilityDTOToOne<ProductEligibilityModel>ProductEligibilityEntity
      pricingByGrouprepeated PricingGroupList<PricingGroupDTO>ToMany<PricingGroupModel>List<PricingGroupEntity>
      soqByAccountrepeated SoqByAccountList<SoqByAccountDTO>ToMany<SoqByAccountModel>List<SoqByAccountEntity>
      salesHistoryrepeated SalesHistoryList<SalesHistoryDTO>ToMany<SalesHistoryModel>List<SalesHistoryEntity>
      manufacturingSkusrepeated ManufacturingSkuList<ManufacturingSkuDTO>ToMany<ManufacturingSkuModel>List<ManufacturingSkuEntity>

      Nenhum campo desta estrutura é tipado como enum na Entity. categoryGroup e category continuam String crua nas quatro camadas; a conversão para CategoryGroup só acontece na leitura, dentro dos getters do State, para ordenar e rotular. Consequência: um valor desconhecido do backend atravessa a stack inteira sem log e só se manifesta na tela. O proto declara splitGroup/internalId/invoiceUom depois das sub-estruturas; DTO, Model e Entity os declaram antes — mesmos 24 campos, ordem de declaração diferente.No field of this structure is typed as an enum in the Entity. categoryGroup and category stay raw String across all four layers; conversion to CategoryGroup happens only on read, inside the State getters, to sort and label. Consequence: an unknown backend value crosses the whole stack with no log and only shows up on screen. The proto declares splitGroup/internalId/invoiceUom after the sub-structures; DTO, Model and Entity declare them before — same 24 fields, different declaration order.Ningún campo de esta estructura está tipado como enum en la Entity. categoryGroup y category siguen siendo String cruda en las cuatro capas; la conversión a CategoryGroup ocurre solo en la lectura, dentro de los getters del State, para ordenar y rotular. Consecuencia: un valor desconocido del backend atraviesa la stack entera sin log y solo se manifiesta en la pantalla. El proto declara splitGroup/internalId/invoiceUom después de las sub-estructuras; DTO, Model y Entity los declaran antes — mismos 24 campos, orden de declaración diferente.

      • ProductEligibility Product.eligibility · o filtro da feature 6 campos
        CampoProtoDTOModelEntity
        isAvailableForOrderboolboolboolbool
        isAvailableForBuybackboolboolboolbool
        isAvailableForVanLoadboolboolboolbool
        isAvailableForPriceCheckboolboolboolbool
        isAvailableForStockCountboolboolboolbool
        isAvailableForPromotionReward ¹boolbool?bool?bool?

        isAvailableForStockCount é o campo #5 e o único que esta feature consulta. É o sinal mecânico da §29: um bloco de flags isAvailableFor* significa agregado compartilhado, logo um repository neutro e um UseCase por consumidora. Os 5 primeiros caem em false quando ausentes do JSON; o 6º, sendo optional no proto, é o único que preserva null.isAvailableForStockCount is field #5 and the only one this feature consults. It is §29's mechanical signal: a block of isAvailableFor* flags means a shared aggregate, hence one neutral repository and one UseCase per consumer. The first 5 fall back to false when absent from the JSON; the 6th, being optional in the proto, is the only one that preserves null.isAvailableForStockCount es el campo #5 y el único que esta feature consulta. Es la señal mecánica de la §29: un bloque de flags isAvailableFor* significa agregado compartido, por lo tanto un repository neutro y un UseCase por consumidora. Los 5 primeros caen en false cuando están ausentes del JSON; el 6º, siendo optional en el proto, es el único que preserva null.

      • ProductUom Product.uom · a origem de "Alta" e "Baixa" 3 campos
        CampoProtoDTOModelEntity
        primaryNamestringStringStringString
        secondaryNamestringStringStringString
        conversionFactordoubledoubledoubledouble

        primaryName é a unidade Alta e secondaryName a Baixa; os dois saem no payload como uom1Name e uom2Name. conversionFactor não é usado pela contagem de estoque — a tela não converte Baixa em Alta nem soma as duas: elas são enviadas como duas medidas independentes.primaryName is the High unit and secondaryName the Low one; both go into the payload as uom1Name and uom2Name. conversionFactor is not used by stock count — the screen neither converts Low into High nor adds them up: they are sent as two independent measures.primaryName es la unidad Alta y secondaryName la Baja; las dos salen en el payload como uom1Name y uom2Name. conversionFactor no se usa en el conteo de stock — la pantalla no convierte Baja en Alta ni suma las dos: se envían como dos medidas independientes.

      • ProductHalfPack Product.halfPack · não usado aqui 2 campos
        CampoProtoDTOModelEntity
        allowedboolboolboolbool
        incrementdoubledoubledoubledouble

        Carregado e ignorado: como os dois campos de contagem só aceitam inteiros, não há fração de embalagem nesta tela. Quem usa halfPack é a criação de pedido e a recompra.Loaded and ignored: since both count fields accept integers only, there is no pack fraction on this screen. halfPack is used by order creation and buyback.Cargado e ignorado: como los dos campos de conteo solo aceptan enteros, no hay fracción de envase en esta pantalla. Quien usa halfPack es la creación de pedido y la recompra.

      • SalesHistory Product.salesHistory[] · a grade de histórico 3 campos
        CampoProtoDTOModelEntity
        accountSfidstringStringStringString
        isStockCountboolboolboolbool
        recordsrepeated SalesHistoryRecordList<SalesHistoryRecordDTO>ToMany<SalesHistoryRecordModel>List<SalesHistoryRecordEntity>

        Esta é uma das duas estruturas do catálogo recortadas por varejo (a outra é SoqByAccount): cada produto pode ter um histórico por accountSfid. O cartão escolhe o bloco cujo accountSfid casa com o varejo da rota. O isStockCount é o que o encerramento de visita usa para decidir se a contagem é devida — e é a única leitura de negócio de salesHistory fora desta tela (o merge de visita ad hoc também o percorre, para retê-lo por conta).This is one of the two catalog structures sliced per retail (the other is SoqByAccount): each product may have one history per accountSfid. The card picks the block whose accountSfid matches the route's retail. isStockCount is what visit end uses to decide whether the count is due — and it is the only business read of salesHistory outside this screen (the ad-hoc visit merge also walks it, to retain it per account).Esta es una de las dos estructuras del catálogo recortadas por punto de venta (la otra es SoqByAccount): cada producto puede tener un historial por accountSfid. La tarjeta elige el bloque cuyo accountSfid coincide con el punto de venta de la ruta. El isStockCount es lo que el cierre de visita usa para decidir si el conteo es debido — y es la única lectura de negocio de salesHistory fuera de esta pantalla (el merge de visita ad hoc también lo recorre, para retenerlo por cuenta).

        • SalesHistoryRecord SalesHistory.records[] · ppq = Alta · psq = Baixa 4 campos
          CampoProtoDTOModelEntity
          dateReferencestringStringStringString
          ppqint32intintint
          psqstringStringStringString
          hasCountStockboolboolboolbool

          A estrutura mais importante e a mais mal nomeada da feature. ppq e psq são nomes de wire que nunca são expandidos nem renomeados em camada alguma — não existe primaryPackage/salesQuantity em lugar nenhum do repositório. A redefinição de Jul/2026 (Stock/VendaAlta/Baixa) foi feita só na apresentação: ppq passou a ser lido como a linha Alta e psq como a linha Baixa, sem tocar proto, DTO, Model, Entity nem o streambridge. Note a assimetria de tipo, que é do contrato e não um erro de mapeamento: ppq é numérico e psq é texto — por isso a linha Baixa mostra - quando vem vazia (uma string em branco), enquanto a Alta sempre mostra um número, inclusive 0. dateReference é a string que vira a pílula de data, ordenada decrescente antes de cortar as 4 primeiras. hasCountStock chega, é mapeado nas 4 camadas e não tem nenhum leitor no app.The feature's most important and worst-named structure. ppq and psq are wire names that are never expanded or renamed in any layer — there is no primaryPackage/salesQuantity anywhere in the repository. The Jul/2026 redefinition (Stock/SaleHigh/Low) was done in the presentation only: ppq came to be read as the High row and psq as the Low row, without touching proto, DTO, Model, Entity or the streambridge. Note the type asymmetry, which comes from the contract and is not a mapping bug: ppq is numeric and psq is text — that is why the Low row shows - when it arrives empty (a blank string), while High always shows a number, including 0. dateReference is the string that becomes the date pill, sorted descending before taking the first 4. hasCountStock arrives, is mapped across all 4 layers and has no reader in the app.La estructura más importante y peor nombrada de la feature. ppq y psq son nombres de wire que nunca se expanden ni se renombran en ninguna capa — no existe primaryPackage/salesQuantity en ningún lugar del repositorio. La redefinición de Jul/2026 (Stock/VentaAlta/Baja) se hizo solo en la presentación: ppq pasó a leerse como la fila Alta y psq como la fila Baja, sin tocar proto, DTO, Model, Entity ni el streambridge. Note la asimetría de tipo, que viene del contrato y no es un error de mapeo: ppq es numérico y psq es texto — por eso la fila Baja muestra - cuando llega vacía (una cadena en blanco), mientras Alta siempre muestra un número, incluso 0. dateReference es la cadena que se vuelve la píldora de fecha, ordenada descendente antes de cortar las 4 primeras. hasCountStock llega, se mapea en las 4 capas y no tiene ningún lector en la app.

      • ManufacturingSku Product.manufacturingSkus[] · lote no payload 5 campos
        CampoProtoDTOModelEntity
        manufacturingSkuSfidstringStringStringString
        batchIdstringStringStringString
        namestringStringStringString
        codestringStringStringString
        erpNumberstringStringStringString

        O builder do payload usa só o primeiro item da lista, e dele só manufacturingSkuSfid e batchId. Produto sem nenhum SKU de fabricação envia os dois campos como string vazia (ver Pendências).The payload builder uses only the first item of the list, and from it only manufacturingSkuSfid and batchId. A product with no manufacturing SKU sends both fields as an empty string (see Pending items).El builder del payload usa solo el primer ítem de la lista, y de él solo manufacturingSkuSfid y batchId. Un producto sin ningún SKU de fabricación envía los dos campos como cadena vacía (ver Pendientes).

      • PricingGroup Product.pricingByGroup[] · não usado aqui 2 campos
        CampoProtoDTOModelEntity
        pricingGroupIdstringStringStringString
        priceEntriesrepeated PriceEntryList<PriceEntryDTO>ToMany<PriceEntryModel>List<PriceEntryEntity>

        Carregado e ignorado. A contagem de estoque é a única das consumidoras do catálogo que não resolve preço — não há grupo de preço, nem valor, nem total monetário em nenhum lugar da tela ou do payload.Loaded and ignored. Stock count is the only catalog consumer that doesn't resolve a price — there is no pricing group, no value and no monetary total anywhere on the screen or in the payload.Cargado e ignorado. El conteo de stock es la única de las consumidoras del catálogo que no resuelve precio — no hay grupo de precio, ni valor, ni total monetario en ningún lugar de la pantalla o del payload.

        • PriceEntry PricingGroup.priceEntries[] 10 campos
          CampoProtoDTOModelEntity
          manufacturingSkuIdmanufacturingSKUIdStringStringString
          manufacturingSkuBatchIdstringStringStringString
          validFromstringStringStringString
          validTostringStringStringString
          priceWithVatdoubledoubledoubledouble
          priceWithoutVatdoubledoubledoubledouble
          rrpWithVat ¹doubledouble?double?double?
          withholdingTax ¹doubledouble?double?double?
          priceEntryIdstringStringStringString
          cashFeePercentage ¹doubledouble?double?double?

          O único rename de todo o catálogo: o proto escreve manufacturingSKUId (SKU em maiúsculas) e as camadas Dart usam manufacturingSkuId. Nenhum destes 10 campos é lido pela contagem de estoque.The catalog's only rename: the proto writes manufacturingSKUId (uppercase SKU) and the Dart layers use manufacturingSkuId. None of these 10 fields is read by stock count.El único rename de todo el catálogo: el proto escribe manufacturingSKUId (SKU en mayúsculas) y las capas Dart usan manufacturingSkuId. Ninguno de estos 10 campos es leído por el conteo de stock.

      • SoqByAccount Product.soqByAccount[] · não usado aqui 2 campos
        CampoProtoDTOModelEntity
        accountSfidstringStringStringString
        valueint32intintint

        Quantidade sugerida de pedido por varejo. Carregada e ignorada — a contagem não sugere quantidade nenhuma; o campo é vazio até o representante digitar.Suggested order quantity per retail. Loaded and ignored — the count suggests no quantity at all; the field is empty until the rep types.Cantidad sugerida de pedido por punto de venta. Cargada e ignorada — el conteo no sugiere ninguna cantidad; el campo está vacío hasta que el representante escribe.

    • StockCountEntry domain-only · nunca persistido 3 campos
      CampoProtoDTOModelEntity
      productSfidString
      highUomCountint?
      lowUomCountint?

      O que o representante digita. Só existe na camada de domínio — sem proto, sem DTO, sem Model, sem box. Um getter hasAnyCount devolve verdadeiro quando ao menos uma das duas contagens é não-nula; é ele que decide se o produto entra no resumo e no envio. Os dois campos são int? e não int de propósito: null significa "não contado" e é semanticamente diferente de zero ("contei, e não tem nenhum"). A entrada é removida do mapa — não zerada — quando as duas colunas ficam vazias. Antes de Jul/2026 esta estrutura tinha um único campo newStockCount.What the rep types. It exists only in the domain layer — no proto, no DTO, no Model, no box. A hasAnyCount getter returns true when at least one of the two counts is non-null; it is what decides whether the product enters the summary and the submission. Both fields are int? rather than int on purpose: null means "not counted" and is semantically different from zero ("I counted, and there are none"). The entry is removed from the map — not zeroed — when both columns go empty. Before Jul/2026 this structure had a single newStockCount field.Lo que el representante escribe. Solo existe en la capa de dominio — sin proto, sin DTO, sin Model, sin box. Un getter hasAnyCount devuelve verdadero cuando al menos uno de los dos conteos es no nulo; es él quien decide si el producto entra en el resumen y en el envío. Los dos campos son int? y no int a propósito: null significa "no contado" y es semánticamente distinto de cero ("conté, y no hay ninguno"). La entrada se elimina del mapa — no se pone en cero — cuando las dos columnas quedan vacías. Antes de Jul/2026 esta estructura tenía un único campo newStockCount.

Mappers

O catálogo tem 11 arquivos de mapper (um por estrutura), cada um com as 5 direções — 55 métodos de mapeamento em 44 extensions. StockCountEntry não tem mapper: nasce e morre no domínio.The catalog has 11 mapper files (one per structure), each with the 5 directions — 55 mapping methods across 44 extensions. StockCountEntry has no mapper: it is born and dies in the domain.El catálogo tiene 11 archivos de mapper (uno por estructura), cada uno con las 5 direcciones — 55 métodos de mapeo en 44 extensions. StockCountEntry no tiene mapper: nace y muere en el dominio.

DireçãoDirectionDirecciónMétodoMethodMétodoQuem chamaCallerQuién llama
JSON → DTOfromMap(map) (static)(static)(static)o datasource Mock, dentro de um isolatethe Mock datasource, inside an isolateel datasource Mock, dentro de un isolate
Proto → DTOtoDTO()o datasource Remote, sobre o ProductCatalogReplythe Remote datasource, on ProductCatalogReplyel datasource Remote, sobre el ProductCatalogReply
DTO → EntitytoDomain()o repository — no caminho remoto dentro de um compute (isolate); é aqui que o lastSyncAt nascethe repository — on the remote path inside a compute (isolate); this is where lastSyncAt is bornel repository — en el camino remoto dentro de un compute (isolate); es aquí donde nace el lastSyncAt
Entity → ModeltoModel()o datasource Local, ao gravar (preenche os ToOne/ToMany)the Local datasource, on write (fills the ToOne/ToMany)el datasource Local, al grabar (llena los ToOne/ToMany)
Model → EntitytoDomain()o datasource Local, ao ler — o caminho que a tela usa em 99% dos casosthe Local datasource, on read — the path the screen uses 99% of the timeel datasource Local, al leer — el camino que la pantalla usa en el 99% de los casos

Duas notas de robustez: o mapper Proto→DTO é o único ponto onde optional vira null (via hasX()); e o Model→Entity do Product usa asserção de não-nulo nos três ToOne (uom, halfPack, eligibility) — um cache gravado sem eles lançaria exceção na leitura.Two robustness notes: the Proto→DTO mapper is the only place where optional becomes null (via hasX()); and Product's Model→Entity uses a non-null assertion on all three ToOne relations (uom, halfPack, eligibility) — a cache written without them would throw on read.Dos notas de robustez: el mapper Proto→DTO es el único punto donde optional se vuelve null (vía hasX()); y el Model→Entity del Product usa aserción de no nulo en las tres relaciones ToOne (uom, halfPack, eligibility) — un caché grabado sin ellas lanzaría excepción en la lectura.

Os únicos deltasThe only deltasLos únicos deltas

  • Relações → ToOne/ToMany no Model. Onze campos: products, os três ToOne do Product (uom, halfPack, eligibility), os quatro ToMany dele (pricingByGroup, soqByAccount, salesHistory, manufacturingSkus), priceEntries e records. Proto e Entity usam listas e objetos; só o ObjectBox usa relação.Relations → ToOne/ToMany in the Model. Eleven fields: products, Product's three ToOne (uom, halfPack, eligibility), its four ToMany (pricingByGroup, soqByAccount, salesHistory, manufacturingSkus), priceEntries and records. Proto and Entity use lists and objects; only ObjectBox uses relations.Relaciones → ToOne/ToMany en el Model. Once campos: products, los tres ToOne del Product (uom, halfPack, eligibility), sus cuatro ToMany (pricingByGroup, soqByAccount, salesHistory, manufacturingSkus), priceEntries y records. Proto y Entity usan listas y objetos; solo ObjectBox usa relaciones.
  • optional do proto → tipo nulável a partir do DTO. Cinco campos, todos marcados ¹: isVatRetention, isAvailableForPromotionReward, cashFeePercentage, rrpWithVat e withholdingTax. Nenhum deles é lido pela contagem de estoque.Proto optional → nullable type from the DTO on. Five fields, all marked ¹: isVatRetention, isAvailableForPromotionReward, cashFeePercentage, rrpWithVat and withholdingTax. None of them is read by stock count.optional del proto → tipo nulable a partir del DTO. Cinco campos, todos marcados ¹: isVatRetention, isAvailableForPromotionReward, cashFeePercentage, rrpWithVat y withholdingTax. Ninguno de ellos es leído por el conteo de stock.
  • Um rename, no Proto. manufacturingSKUId (proto) → manufacturingSkuId (Dart), em PriceEntry. É a única divergência de nome em todo o catálogo.One rename, in the Proto. manufacturingSKUId (proto) → manufacturingSkuId (Dart), in PriceEntry. It is the only name divergence in the whole catalog.Un rename, en el Proto. manufacturingSKUId (proto) → manufacturingSkuId (Dart), en PriceEntry. Es la única divergencia de nombre en todo el catálogo.
  • lastSyncAt não existe no wire. É gerado no mapper DTO→Entity e sobrevive ao filtro do UseCase porque este devolve o container, não a lista crua — é ele que alimenta a faixa de sincronização (§23).lastSyncAt doesn't exist on the wire. It is generated in the DTO→Entity mapper and survives the UseCase filter because the latter returns the container, not the bare list — it is what feeds the sync strip (§23).lastSyncAt no existe en el wire. Se genera en el mapper DTO→Entity y sobrevive al filtro del UseCase porque este devuelve el container, no la lista cruda — es lo que alimenta la franja de sincronización (§23).
  • Nenhum enum tipado, em nenhuma camada. Ao contrário de quase toda outra feature do app, o catálogo não converte string em enum no mapper: categoryGroup, category e os nomes de unidade de medida ficam String nas quatro camadas, e a tipagem só acontece na leitura da tela.No typed enum, in any layer. Unlike almost every other feature in the app, the catalog doesn't convert strings into enums in the mapper: categoryGroup, category and the unit-of-measure names stay String across all four layers, and typing happens only when the screen reads them.Ningún enum tipado, en ninguna capa. A diferencia de casi toda otra feature de la app, el catálogo no convierte cadenas en enums en el mapper: categoryGroup, category y los nombres de unidad de medida quedan String en las cuatro capas, y el tipado ocurre solo cuando la pantalla los lee.
  • E um não-delta que engana: ppq (numérico) e psq (texto) atravessam as quatro camadas sem nenhuma mudança de nome ou tipo. A reinterpretação Alta/Baixa é puramente de apresentação — por isso ela não aparece como delta em lugar nenhum desta seção.And one non-delta that misleads: ppq (numeric) and psq (text) cross all four layers with no change of name or type. The High/Low reinterpretation is purely presentational — which is why it appears as a delta nowhere in this section.Y un no-delta que engaña: ppq (numérico) y psq (texto) atraviesan las cuatro capas sin ningún cambio de nombre o tipo. La reinterpretación Alta/Baja es puramente de presentación — por eso no aparece como delta en ningún lugar de esta sección.
08

Repository

A contagem de estoque não tem repository próprio. Ela lê pelo ProductCatalogRepositoryInterface — o repository único e neutro do catálogo, que não conhece nenhuma feature consumidora (§29). São 5 métodos, nenhum deles nomeado por feature; a contagem usa exatamente um (getProductCatalog), através do seu UseCase. A escrita não passa por aqui: vai pelo repository do Dispatcher, que só conhece o gateway gRPC.Stock count has no repository of its own. It reads through ProductCatalogRepositoryInterface — the catalog's single, neutral repository, which knows no consuming feature (§29). There are 5 methods, none named after a feature; the count uses exactly one (getProductCatalog), through its UseCase. Writing doesn't go through here: it goes through the Dispatcher repository, which knows only the gRPC gateway.El conteo de stock no tiene repository propio. Lee por el ProductCatalogRepositoryInterface — el repository único y neutro del catálogo, que no conoce ninguna feature consumidora (§29). Son 5 métodos, ninguno nombrado por feature; el conteo usa exactamente uno (getProductCatalog), a través de su UseCase. La escritura no pasa por aquí: va por el repository del Dispatcher, que solo conoce el gateway gRPC.

getProductCatalog({source}) mock / local / remote

RetornoReturnRetorno Future<Result<ProductCatalogEntity, Failure>>

O único método que a contagem de estoque exercita, e o único com árvore de decisão. A origem é escolhida pelo parâmetro source (default local) cruzado com a sessão mock e a conectividade:The only method stock count exercises, and the only one with a decision tree. The source is chosen by the source parameter (default local) crossed with the mock session and connectivity:El único método que el conteo de stock ejercita, y el único con árbol de decisión. El origen se elige por el parámetro source (default local) cruzado con la sesión mock y la conectividad:

  • getProductCatalog(source)
    • useMock || source == mock → JSON do mercado, decodificado em isolate→ market JSON, decoded in an isolate→ JSON del mercado, decodificado en isolate
    • source == local || !isConnected → cache; cache vazio devolve NetworkFailure→ cache; an empty cache returns NetworkFailure→ caché; un caché vacío devuelve NetworkFailure
    • source == remote && isConnected → remoto com fallback→ remote with fallback→ remoto con fallback
      • currentResourceProvider resolve o locationHierarchySfid aqui (§25); resource nulo → cacheresolves locationHierarchySfid here (§25); null resource → cacheresuelve el locationHierarchySfid aquí (§25); resource nulo → caché
      • toDomain em compute (isolate)
      • saveProductCatalog write-through: limpa e regrava as 11 boxeswrite-through: clears and rewrites the 11 boxeswrite-through: limpia y regraba las 11 boxes
      • falhafailurefallo → cai para o cache; cache vazio → Error(failure)→ falls back to cache; empty cache → Error(failure)→ cae al caché; caché vacío → Error(failure)

A tela abre sempre com local e só pede remote no pull-to-refresh. Consequência prática: abrir a ferramenta nunca vai à rede — se o catálogo nunca foi sincronizado, a tela abre em erro em vez de buscar. O locationHierarchySfid é resolvido dentro do repository, nunca passado pelo Notifier (§25).The screen always opens with local and only asks for remote on pull-to-refresh. Practical consequence: opening the tool never hits the network — if the catalog was never synced, the screen opens in error instead of fetching. locationHierarchySfid is resolved inside the repository, never passed by the Notifier (§25).La pantalla siempre abre con local y solo pide remote en el pull-to-refresh. Consecuencia práctica: abrir la herramienta nunca va a la red — si el catálogo nunca fue sincronizado, la pantalla abre en error en vez de buscar. El locationHierarchySfid se resuelve dentro del repository, nunca lo pasa el Notifier (§25).

getCachedProductCatalog() local

RetornoReturnRetorno Future<Result<ProductCatalogEntity?, Failure>>

Leitura pura do cache, sem fallback. Devolve null dentro de um Success quando não há nada gravado. A contagem de estoque não o chama — o filtro por elegibilidade obriga a passar pelo UseCase, que usa o método acima.A pure cache read, no fallback. Returns null inside a Success when nothing is stored. Stock count doesn't call it — the eligibility filter forces going through the UseCase, which uses the method above.Lectura pura del caché, sin fallback. Devuelve null dentro de un Success cuando no hay nada grabado. El conteo de stock no lo llama — el filtro por elegibilidad obliga a pasar por el UseCase, que usa el método de arriba.

getCachedProductCatalogLastSyncAt() local

RetornoReturnRetorno Future<DateTime?>

O único método da interface que não devolve Result: em caso de exceção ele registra o erro e devolve null, engolindo a falha de propósito — quem pergunta "quando sincronizou?" não deve quebrar por causa disso. Serve ao motor de frescor de dados, não à tela: a faixa de sincronização da contagem de estoque lê o lastSyncAt que já vem dentro do container (§23).The only interface method that doesn't return a Result: on exception it logs the error and returns null, swallowing the failure on purpose — asking "when did it sync?" shouldn't break over it. It serves the data-freshness engine, not the screen: stock count's sync strip reads the lastSyncAt that already comes inside the container (§23).El único método de la interfaz que no devuelve Result: en caso de excepción registra el error y devuelve null, tragando el fallo a propósito — quien pregunta "¿cuándo sincronizó?" no debe romperse por eso. Sirve al motor de frescura de datos, no a la pantalla: la franja de sincronización del conteo de stock lee el lastSyncAt que ya viene dentro del container (§23).

getCachedProductBySfid({productSfid}) local · §28 A

RetornoReturnRetorno Future<Result<ProductEntity?, Failure>>

Lookup de item único no padrão §28 categoria A (cache-only, nunca remoto), por varredura linear sobre o catálogo inteiro. Nenhuma feature do app o chama — nem a contagem de estoque, que já tem os produtos no State e os indexa por productSfid no próprio mapa de contagens.Single-item lookup in the §28 category A pattern (cache-only, never remote), by linear scan over the whole catalog. No app feature calls it — not stock count either, which already holds the products in State and indexes them by productSfid in the counts map itself.Lookup de ítem único en el patrón §28 categoría A (cache-only, nunca remoto), por barrido lineal sobre el catálogo entero. Ninguna feature de la app lo llama — tampoco el conteo de stock, que ya tiene los productos en el State y los indexa por productSfid en el propio mapa de conteos.

saveProductCatalog({productCatalog}) local

RetornoReturnRetorno Future<Result<void, Failure>>

Gravação explícita do catálogo no cache, usada pelo motor de sincronização. Não é o caminho do write-through do fetch remoto — esse chama o datasource local direto, dentro do próprio método de busca.Explicit catalog write to cache, used by the sync engine. It is not the remote fetch's write-through path — that one calls the local datasource directly, inside the fetch method itself.Grabación explícita del catálogo en el caché, usada por el motor de sincronización. No es el camino del write-through del fetch remoto — ese llama al datasource local directo, dentro del propio método de búsqueda.

09

Datasources

Três datasources do catálogo, com três tratamentos de erro diferentes. Nenhum deles é da contagem de estoque: são compartilhados por todas as consumidoras do catálogo. A feature não tem datasource próprio — nem local (não persiste nada) nem remoto (a escrita vai pelo gateway do Dispatcher).Three catalog datasources, with three different error treatments. None of them belongs to stock count: they are shared by every catalog consumer. The feature has no datasource of its own — neither local (it persists nothing) nor remote (the write goes through the Dispatcher gateway).Tres datasources del catálogo, con tres tratamientos de error diferentes. Ninguno de ellos es del conteo de stock: son compartidos por todas las consumidoras del catálogo. La feature no tiene datasource propio — ni local (no persiste nada) ni remoto (la escritura va por el gateway del Dispatcher).

Remote ProductCatalogRemoteDataSource gRPC · 1
getProductCatalog({locationHierarchySfid, dateReference?})
MétodoMethodMétodo
Future<ProductCatalogDTO>o único método da classethe class's only methodel único método de la clase
EnvioSendsEnvío
ProductCatalogRequestlocationHierarchySfid sempre; dateReference só é atribuído se não-nulo; lastModifiedDate (#3) nunca é atribuídolocationHierarchySfid always; dateReference is only assigned when non-null; lastModifiedDate (#3) is never assignedlocationHierarchySfid siempre; dateReference solo se asigna si no es nulo; lastModifiedDate (#3) nunca se asigna
RetornoReturnsRetorno
ProductCatalogDTOreply.toDTO(), o catálogo do mercado inteiroProductCatalogDTOreply.toDTO(), the whole market catalogProductCatalogDTOreply.toDTO(), el catálogo del mercado entero
Fluxo de usoUsage flowFlujo de uso
Alcançado só pelo caminho remoto do repository — na contagem de estoque, apenas pelo pull-to-refreshReached only by the repository's remote path — in stock count, only via pull-to-refreshAlcanzado solo por el camino remoto del repository — en el conteo de stock, solo por pull-to-refresh
Tratamento de erroError handlingManejo de error
Dois ramos: GrpcError vai ao GrpcExceptionHandler; qualquer outro vira ServerException com log. O repository converte em Failure.Two branches: GrpcError goes to GrpcExceptionHandler; anything else becomes a logged ServerException. The repository converts it into a Failure.Dos ramas: GrpcError va al GrpcExceptionHandler; cualquier otro se vuelve ServerException con log. El repository lo convierte en Failure.
Local ProductCatalogLocalDataSource ObjectBox · 5

Envio nenhum (banco local). Fluxo de uso: é a origem real de tudo que a contagem de estoque mostra. Tratamento de erro uniforme: todos os métodos lançam CacheException com log e mensagem própria por operação. Note que todos são síncronos — não há Future nesta classe.No send (local database). Usage flow: it is the real source of everything stock count shows. Uniform error handling: every method throws a logged CacheException with its own per-operation message. Note that all are synchronous — there is no Future in this class.Ningún envío (base local). Flujo de uso: es el origen real de todo lo que el conteo de stock muestra. Manejo de error uniforme: todos los métodos lanzan CacheException con log y mensaje propio por operación. Note que todos son síncronos — no hay Future en esta clase.

getProductCatalog()

ProductCatalogEntity?o primeiro (e único) registro da box convertido em Entity, ou null se a box está vazia. É a leitura que abre a tela.the box's first (and only) row converted to an Entity, or null when the box is empty. This is the read that opens the screen.el primer (y único) registro de la box convertido en Entity, o null si la box está vacía. Es la lectura que abre la pantalla.

getProductCatalogLastSyncAt()

DateTime?só o timestamp, sem materializar o catálogo. Serve ao motor de frescor.just the timestamp, without materialising the catalog. It serves the freshness engine.solo el timestamp, sin materializar el catálogo. Sirve al motor de frescura.

saveProductCatalog({entity})

voidchama clearProductCatalog() antes de gravar: é substituição total, não merge. O catálogo é sempre um registro só.calls clearProductCatalog() before writing: it is a full replacement, not a merge. The catalog is always a single row.llama clearProductCatalog() antes de grabar: es sustitución total, no merge. El catálogo es siempre un solo registro.

mergeAdhocProductCatalog({incoming, accountSfid})

voidmerge aditivo dos produtos que chegam num reply de visita ad hoc, e o único método desta classe ausente da interface do repository. Importa para a contagem de estoque porque é por aqui que uma visita ad hoc pode acrescentar produtos (e histórico) ao catálogo já em cache.additive merge of products arriving in an ad hoc visit reply, and the only method of this class absent from the repository interface. It matters to stock count because this is how an ad hoc visit can add products (and history) to the already-cached catalog.merge aditivo de los productos que llegan en un reply de visita ad hoc, y el único método de esta clase ausente de la interfaz del repository. Importa al conteo de stock porque es por aquí que una visita ad hoc puede agregar productos (e historial) al catálogo ya en caché.

clearProductCatalog()

voidremove em cascata 11 boxes, dos filhos para o pai: PriceEntry, PricingGroup, SoqByAccount, SalesHistoryRecord, SalesHistory, ManufacturingSku, ProductUom, ProductHalfPack, ProductEligibility, Product e por fim o próprio catálogo. É o custo real de cada pull-to-refresh.cascade-removes 11 boxes, children first: PriceEntry, PricingGroup, SoqByAccount, SalesHistoryRecord, SalesHistory, ManufacturingSku, ProductUom, ProductHalfPack, ProductEligibility, Product and finally the catalog itself. This is the real cost of every pull-to-refresh.elimina en cascada 11 boxes, de los hijos al padre: PriceEntry, PricingGroup, SoqByAccount, SalesHistoryRecord, SalesHistory, ManufacturingSku, ProductUom, ProductHalfPack, ProductEligibility, Product y por fin el propio catálogo. Es el costo real de cada pull-to-refresh.

Mock ProductCatalogMockDataSource JSON · 1
getProductCatalog()
MétodoMethodMétodo
Future<ProductCatalogDTO>memoizado em memória: a primeira chamada carrega, as seguintes reusam o mesmo Futurememoized in memory: the first call loads, the following ones reuse the same Futurememoizado en memoria: la primera llamada carga, las siguientes reusan el mismo Future
EnvioSendsEnvío
nenhum — lê o asset do mercado ativo, escolhendo entre o mock sintético e o dump real conforme a flag de mock realnone — reads the active market's asset, choosing between the synthetic mock and the real dump per the real-mock flagninguno — lee el asset del mercado activo, eligiendo entre el mock sintético y el dump real según la flag de mock real
RetornoReturnsRetorno
o DTO decodificado num compute (isolate), porque o maior arquivo real tem 745 KBthe DTO decoded in a compute (isolate), because the largest real file is 745 KBel DTO decodificado en un compute (isolate), porque el mayor archivo real tiene 745 KB
Fluxo de usoUsage flowFlujo de uso
sessão mock, ou source == mock explícitomock session, or an explicit source == mocksesión mock, o source == mock explícito
Tratamento de erroError handlingManejo de error
reseta a memoização e lança CacheException com log — para uma falha transitória de asset não ficar grudada na sessãoresets the memoization and throws a logged CacheException — so a transient asset failure doesn't stick to the sessionresetea la memoización y lanza CacheException con log — para que un fallo transitorio de asset no quede pegado a la sesión
10

Enums e labelsEnums & labelsEnums y labels

A contagem de estoque não define nenhum enum próprio — nem de status, nem de tipo, nem de aba. Ela consome quatro enums de outras áreas. Os rótulos Alta e Baixa, apesar de parecerem candidatos naturais a enum, são chaves de tradução: as unidades de medida reais vêm como texto livre do backend, no ProductUom.Stock count defines no enum of its own — no status, no type, no tab. It consumes four enums from other areas. The High and Low labels, though they look like natural enum candidates, are translation keys: the real units of measure arrive as free text from the backend, inside ProductUom.El conteo de stock no define ningún enum propio — ni de estado, ni de tipo, ni de pestaña. Consume cuatro enums de otras áreas. Los rótulos Alta y Baja, aunque parezcan candidatos naturales a enum, son claves de traducción: las unidades de medida reales llegan como texto libre del backend, dentro del ProductUom.

CategoryGroup 9 · o filtro e a ordemthe filter and the orderel filtro y el orden
casevalueshortLabel
fmc"fmc""FMC"
nc"nc""NC"
ryo"ryo""RYO"
modi"modi""VUSE"
oral"oral""Oral"
otp"otp""OTP"
partnership"partnership""Partnership"
other"other""Other"
unknown""""

Usado em dois lugares da feature, ambos por fromValue (compara em minúsculas, cai em unknown): a ordem da lista de categorias do filtro, e a ordem dos grupos no painel de resumo. Nos dois casos o critério é a posição de declaração do enum, não ordem alfabética — por isso FMC vem sempre primeiro. O shortLabel é o texto exibido; note que modi aparece como "VUSE", e que unknown tem rótulo vazio. Como o campo atravessa as camadas como String crua (§07), um grupo desconhecido do backend só é detectado aqui, sem log, e vai para o fim da ordenação.Used in two places in the feature, both via fromValue (compares lowercased, falls back to unknown): the order of the filter's category list, and the order of the groups in the summary panel. In both cases the criterion is the enum's declaration position, not alphabetical order — which is why FMC always comes first. shortLabel is the displayed text; note that modi shows as "VUSE", and that unknown has an empty label. Since the field crosses the layers as a raw String (§07), a group unknown to the backend is only detected here, with no log, and sorts last.Usado en dos lugares de la feature, ambos por fromValue (compara en minúsculas, cae en unknown): el orden de la lista de categorías del filtro, y el orden de los grupos en el panel de resumen. En los dos casos el criterio es la posición de declaración del enum, no orden alfabético — por eso FMC viene siempre primero. El shortLabel es el texto exhibido; note que modi aparece como "VUSE", y que unknown tiene rótulo vacío. Como el campo atraviesa las capas como String cruda (§07), un grupo desconocido del backend solo se detecta aquí, sin log, y va al final del ordenamiento.

DataSourceType 3
caseuso na featureuse in the featureuso en la feature
mocknunca pedido explicitamente pela feature — a sessão mock é decidida no repositorynever asked for explicitly by the feature — the mock session is decided in the repositorynunca pedido explícitamente por la feature — la sesión mock se decide en el repository
localo default do UseCase e do _load: como a tela abrethe UseCase's and _load's default: how the screen opensel default del UseCase y del _load: cómo abre la pantalla
remotepassado apenas pelo refresh(), ou seja, só pelo pull-to-refreshpassed only by refresh(), i.e. only by pull-to-refreshpasado solo por el refresh(), o sea, solo por pull-to-refresh

Enum sem propriedade value — é um seletor interno de camada, não um código de wire.An enum with no value property — it is an internal layer selector, not a wire code.Enum sin propiedad value — es un selector interno de capa, no un código de wire.

ConectaInputType 6 · a variante do campo-pílulathe pill field variantla variante del campo-píldora
casenotanotenota
outlinedcampo padrão de formuláriostandard form fieldcampo estándar de formulario
outlinedCompactvariante baixa — a usada pela verificação de preçolow variant — the one price check usesvariante baja — la que usa la verificación de precio
outlinedPillcantos totalmente arredondadosfully rounded cornersesquinas totalmente redondeadas
outlinedPillCompacta variante desta feature — pílula achatada, criada em Jul/2026 para os dois campos Alta/Baixathis feature's variant — flattened pill, created in Jul/2026 for the two High/Low fieldsla variante de esta feature — píldora achatada, creada en Jul/2026 para los dos campos Alta/Baja
borderlesssem bordano bordersin borde
searchbarra de busca — não usada aqui, a tela não tem buscasearch bar — not used here, the screen has no searchbarra de búsqueda — no usada aquí, la pantalla no tiene búsqueda
DispatcherType 42 · 1 relevante1 relevant1 relevante
propriedadepropertypropiedadvalorvaluevalor
casestockCount
serviceName"LocationStockUploadAPI"
enabledMarkets[BR, CL, ZA]
destinationnão declarado → default salesforcenot declared → defaults to salesforceno declarado → default salesforce
lightweightnão — o conjunto tem só 3 tipos (notificationRead, answerTask, priceCheck)no — the set has only 3 types (notificationRead, answerTask, priceCheck)no — el conjunto tiene solo 3 tipos (notificationRead, answerTask, priceCheck)
resendMayDuplicatetrue — derivado de não ser lightweighttrue — derived from not being lightweighttrue — derivado de no ser lightweight
resolveServiceName(hasPromotion: false)"LocationStockUploadAPI" (sem prefixo Promo_)(no Promo_ prefix)(sin prefijo Promo_)

O enum tem 42 valores, dos quais 36 têm builder de payload. A diferença mais consequente entre a contagem de estoque e a feature irmã está nesta tabela: a verificação de preço é um dos 3 tipos lightweight, e a contagem não — logo o reenvio manual de uma contagem carrega risco de duplicidade que o da verificação não tem. Note também que existem quatro outros valores com "stock" no nome (stockReconciliation, stockRequest, stockAllocationExecution, stockUnload), todos sem builder e todos do domínio da van — nenhum tem relação com esta feature.The enum has 42 values, 36 of which have a payload builder. The most consequential difference between stock count and its sibling feature is in this table: price check is one of the 3 lightweight types, and the count is not — so manually resending a count carries a duplicate risk that resending a check doesn't. Note too that there are four other values with "stock" in the name (stockReconciliation, stockRequest, stockAllocationExecution, stockUnload), all without a builder and all from the van domain — none relates to this feature.El enum tiene 42 valores, de los cuales 36 tienen builder de payload. La diferencia más consecuente entre el conteo de stock y la feature hermana está en esta tabla: la verificación de precio es uno de los 3 tipos lightweight, y el conteo no — así que el reenvío manual de un conteo lleva riesgo de duplicidad que el de la verificación no tiene. Note también que existen cuatro otros valores con "stock" en el nombre (stockReconciliation, stockRequest, stockAllocationExecution, stockUnload), todos sin builder y todos del dominio de la van — ninguno se relaciona con esta feature.

Chaves de traduçãoTranslation keysClaves de traducción

São 27 chaves com o prefixo stock_count_*, todas presentes nos 6 mercados — zero assimetria (§10). Quatro delas não têm consumidor no app: stock_count_stock e stock_count_sale ("Estoque"/"Venda"), órfãs desde a troca das colunas para Alta/Baixa; stock_count_clear_filter, órfã desde a remoção do botão de limpar filtro; e stock_count_summary_stat_units ("Total Unidades"), substituída pelos dois totais separados. Fora do prefixo, a feature ainda depende de visit_detail_stock_history (o rótulo do tile) e visit_end_pending_stock_count (o rótulo do cartão de pendência), e o painel de resumo reutiliza três chaves de price_check_* — coluna "Marca", marcador de vazio e rótulo de subtotal.There are 27 keys prefixed stock_count_*, all present in all 6 markets — zero asymmetry (§10). Four of them have no consumer in the app: stock_count_stock and stock_count_sale ("Stock"/"Sale"), orphaned since the columns became High/Low; stock_count_clear_filter, orphaned since the clear-filter button was removed; and stock_count_summary_stat_units ("Total Units"), replaced by the two separate totals. Outside the prefix, the feature also depends on visit_detail_stock_history (the tile label) and visit_end_pending_stock_count (the pending card label), and the summary panel reuses three price_check_* keys — the "Brand" column, the empty marker and the subtotal label.Son 27 claves con el prefijo stock_count_*, todas presentes en los 6 mercados — cero asimetría (§10). Cuatro de ellas no tienen consumidor en la app: stock_count_stock y stock_count_sale ("Stock"/"Venta"), huérfanas desde el cambio de las columnas a Alta/Baja; stock_count_clear_filter, huérfana desde la remoción del botón de limpiar filtro; y stock_count_summary_stat_units ("Total Unidades"), sustituida por los dos totales separados. Fuera del prefijo, la feature depende además de visit_detail_stock_history (el rótulo del tile) y visit_end_pending_stock_count (el rótulo de la tarjeta de pendiente), y el panel de resumen reutiliza tres claves de price_check_* — la columna "Marca", el marcador de vacío y el rótulo de subtotal.

ChaveKeyClaveBRZACL · AR · PY · PE
stock_count_titleContagem de EstoqueStock CountConteo de Stock
stock_count_high_uomAltaHighAlta
stock_count_low_uomBaixaLowBaja
stock_count_history_titleHistórico de contagem de estoqueStock count historyHistorial de conteo de stock
stock_count_categoryCategoriaCategoryCategoría
stock_count_brandMarcaBrandMarca
stock_count_empty_stateNenhum produto com histórico de vendas para este varejoNo products with sales history for this retailSin productos con historial de ventas para este punto de venta
stock_count_filled_labelPreenchidosFilledLlenados
stock_count_summary_modal_titleContagens a enviarStock counts to sendConteos a enviar
stock_count_summary_subtitle{filled} de {total} produtos contados{filled} of {total} products counted{filled} de {total} productos contados
stock_count_summary_stat_totalTotal ProdutosTotal ProductsTotal Productos
stock_count_summary_stat_emptyVaziosEmptyVacíos
stock_count_summary_clear_allLimpar tudoClear all entriesLimpiar todo
stock_count_submit_confirm_titleEnviar contagem de estoque?Submit stock count?¿Enviar conteo de stock?
stock_count_submit_confirm_yesSim, enviarYes, submitSí, enviar
stock_count_submit_success_messageContagem de estoque salva com sucesso.Stock count saved successfully.Conteo de stock guardado correctamente.
stock_count_submit_error_messageFalha ao salvar a contagem de estoque. Tente novamente.Failed to save stock count. Please try again.No se pudo guardar el conteo de stock. Inténtalo de nuevo.
visit_detail_stock_history (tile)(tile)(tile)FaltaStock CountConteo de Stock
visit_end_pending_stock_countContagem de estoqueStock countConteo de inventario
dispatcher_type_stockCountContagem de estoqueStock countConteo de stock

Recorte das chaves visíveis; as 10 restantes do prefixo são as 4 chaves órfãs e as 6 mensagens e botões dos dois modais de confirmação. Três nomes diferentes para a mesma coisa convivem hoje: o tile do Brasil diz "Falta", o cartão de pendência diz "Contagem de estoque" e o título da tela diz "Contagem de Estoque" — e em espanhol o cartão de pendência diz "Conteo de inventario" enquanto todo o resto diz "Conteo de stock" (ver Pendências).A slice of the visible keys; the remaining 10 in the prefix are the 4 orphan keys and the 6 messages and buttons of the two confirmation modals. Three different names for the same thing coexist today: Brazil's tile says "Falta" (shortage), the pending card says "Contagem de estoque" and the screen title says "Contagem de Estoque" — and in Spanish the pending card says "Conteo de inventario" while everything else says "Conteo de stock" (see Pending items).Recorte de las claves visibles; las 10 restantes del prefijo son las 4 claves huérfanas y los 6 mensajes y botones de los dos modales de confirmación. Tres nombres diferentes para la misma cosa conviven hoy: el tile de Brasil dice "Falta", la tarjeta de pendiente dice "Contagem de estoque" y el título de la pantalla dice "Contagem de Estoque" — y en español la tarjeta de pendiente dice "Conteo de inventario" mientras todo el resto dice "Conteo de stock" (ver Pendientes).

11

UseCases

A feature usa quatro UseCases: um para ler o catálogo filtrado, um para resolver a visita, um para montar o payload e um para despachar. Todos com provider keepAlive. O de leitura é a materialização da §29 do CLAUDE.md: o catálogo tem um repository neutro e um UseCase por feature consumidora — hoje são 7 UseCases sobre o mesmo repository (pedido, recompra, carga de van, verificação de preço, contagem de estoque, prêmio de promoção e o de passagem sem filtro), cada um filtrando pela sua flag de elegibilidade.The feature uses four UseCases: one to read the filtered catalog, one to resolve the visit, one to build the payload and one to dispatch. All with a keepAlive provider. The read one is CLAUDE.md §29 made concrete: the catalog has one neutral repository and one UseCase per consuming feature — today 7 UseCases sit on the same repository (order, buyback, van load, price check, stock count, promotion reward and the unfiltered pass-through), each filtering on its own eligibility flag.La feature usa cuatro UseCases: uno para leer el catálogo filtrado, uno para resolver la visita, uno para armar el payload y uno para despachar. Todos con provider keepAlive. El de lectura es la materialización de la §29 del CLAUDE.md: el catálogo tiene un repository neutro y un UseCase por feature consumidora — hoy son 7 UseCases sobre el mismo repository (pedido, recompra, carga de van, verificación de precio, conteo de stock, premio de promoción y el de paso sin filtro), cada uno filtrando por su flag de elegibilidad.

GetProductsForStockCountUseCase 1 · a listathe listla lista
MétodoMethodMétodoRetornaReturnsDevuelveUsoUseUso
execute({source = local})Result<ProductCatalogEntity, Failure>Chama repository.getProductCatalog(source:) e devolve o container inteiro com copyWith(products: …where(eligibility.isAvailableForStockCount)). Devolver o container e não a lista crua é deliberado: assim o lastSyncAt sobrevive ao filtro e alimenta a faixa de sincronização da tela (§23). Dois chamadores: o StockCountNotifier e o cálculo de pendências do encerramento de visita.Calls repository.getProductCatalog(source:) and returns the whole container with copyWith(products: …where(eligibility.isAvailableForStockCount)). Returning the container instead of the bare list is deliberate: that way lastSyncAt survives the filter and feeds the screen's sync strip (§23). Two callers: the StockCountNotifier and the visit-end pendings computation.Llama repository.getProductCatalog(source:) y devuelve el container entero con copyWith(products: …where(eligibility.isAvailableForStockCount)). Devolver el container y no la lista cruda es deliberado: así el lastSyncAt sobrevive al filtro y alimenta la franja de sincronización de la pantalla (§23). Dos llamadores: el StockCountNotifier y el cálculo de pendientes del cierre de visita.

O filtro é a flag de elegibilidade. Não há recorte por varejo — e é essa a origem da divergência com o texto do estado vazio e com o critério do encerramento de visita (ver Pendências). Também não há recorte por preço nem por estoque, ao contrário da vitrine de pedido. Os dois filtros visíveis (categoria e marca) são aplicados depois, nos getters do State — o UseCase não os conhece.The filter is only the eligibility flag. There is no per-retail slice — and that is the origin of the divergence with the empty-state copy and with the visit-end criterion (see Pending items). There is no slice by price or stock either, unlike the order showcase. The two visible filters (category and brand) are applied later, in the State getters — the UseCase doesn't know about them.El filtro es solo la flag de elegibilidad. No hay recorte por punto de venta — y ese es el origen de la divergencia con el texto del estado vacío y con el criterio del cierre de visita (ver Pendientes). Tampoco hay recorte por precio ni por stock, a diferencia de la vitrina de pedido. Los dos filtros visibles (categoría y marca) se aplican después, en los getters del State — el UseCase no los conoce.

GetVisitsUseCase 1 · a visita, do cachethe visit, from cachela visita, del caché
MétodoMethodMétodoRetornaReturnsDevuelveUsoUseUso
getCachedByAccountSfid({accountSfid})Result<VisitEntity?, Failure>Único método deste UseCase que a feature usa, e só no momento do envio. Como a tela recebe apenas o identificador do varejo (§17), é daqui que saem o sfid da visita, o código SAP e o nome do varejo para o payload. Cache-only, no padrão §28 categoria A. O resultado é reduzido a valueOrNull: uma falha de leitura é indistinguível de "não há visita", e o envio segue com os três campos vazios.The only method of this UseCase the feature uses, and only at submit time. Since the screen receives just the retail identifier (§17), this is where the visit sfid, the SAP code and the retail name for the payload come from. Cache-only, in the §28 category A pattern. The result is reduced to valueOrNull: a read failure is indistinguishable from "there is no visit", and the submission proceeds with those three fields empty.Único método de este UseCase que la feature usa, y solo en el momento del envío. Como la pantalla recibe apenas el identificador del punto de venta (§17), es de aquí que salen el sfid de la visita, el código SAP y el nombre del punto de venta para el payload. Cache-only, en el patrón §28 categoría A. El resultado se reduce a valueOrNull: un fallo de lectura es indistinguible de "no hay visita", y el envío sigue con los tres campos vacíos.
BuildStockCountDispatcherPayloadUseCase 1 · payload
MétodoMethodMétodoRetornaReturnsDevuelveUsoUseUso
build({input})DispatcherEnvelopeContrato do DispatcherPayloadBuilder e único método da classe — não existe buildAll e não há fatiamento: sai um envelope só, com todos os itens num array, independentemente de quantos produtos foram contados. É síncrono e const, sem nenhum método privado. Toda construção wire mora aqui (§36): a formatação da data, a derivação do resourceSfid primário/secundário, a resolução do código ISO da moeda e a decisão de omitir produto sem contagem.The DispatcherPayloadBuilder contract and the class's only method — there is no buildAll and no chunking: one single envelope comes out, with every item in one array, no matter how many products were counted. It is synchronous and const, with no private methods at all. All wire construction lives here (§36): date formatting, deriving the primary/secondary resourceSfid, resolving the currency ISO code and the decision to omit a product with no count.Contrato del DispatcherPayloadBuilder y único método de la clase — no existe buildAll y no hay corte: sale un solo envelope, con todos los ítems en un array, independientemente de cuántos productos se contaron. Es síncrono y const, sin ningún método privado. Toda construcción wire vive aquí (§36): el formateo de la fecha, la derivación del resourceSfid primario/secundario, la resolución del código ISO de la moneda y la decisión de omitir un producto sin conteo.

O inputThe inputEl inputStockCountDispatcherPayloadInput, Freezed, 7 campos, todos obrigatórios e sem default (um deles nulável):Freezed, 7 fields, all required and with no default (one of them nullable):Freezed, 7 campos, todos obligatorios y sin default (uno de ellos nulable):

accountSfid
String · identificador do varejo, vindo da rotaretail identifier, from the routeidentificador del punto de venta, desde la ruta
entries
List<StockCountEntryEntity> · entity crua, 3 campos cadaraw entity, 3 fields eachentity cruda, 3 campos cada una
products
List<ProductEntity> · entity crua, os 24 campos do catálogoraw entity, the catalog's 24 fieldsentity cruda, los 24 campos del catálogo
resource
ResourceEntity · o representante cru — o builder é que decide entre primaryResourceSfid e secondaryResourceSfid, exatamente como a §36 exigethe raw rep — the builder is what decides between primaryResourceSfid and secondaryResourceSfid, exactly as §36 requiresel representante crudo — el builder es quien decide entre primaryResourceSfid y secondaryResourceSfid, exactamente como la §36 exige
visit
VisitEntity? · obrigatório porém nulável — sem visita em cache, três campos do payload saem vaziosrequired yet nullable — with no cached visit, three payload fields go out emptyobligatorio pero nulable — sin visita en caché, tres campos del payload salen vacíos
market
EndMarket · o mercado ativo, lido pelo Notifier e nunca guardado no State (§34)the active market, read by the Notifier and never stored in State (§34)el mercado activo, leído por el Notifier y nunca guardado en el State (§34)
submittedAt
DateTime · o relógio injetado pelo Notifier, não uma data já formatada — exatamente o que a §36 exigethe clock injected by the Notifier, not a pre-formatted date — exactly what §36 requiresel reloj inyectado por el Notifier, no una fecha ya formateada — exactamente lo que la §36 exige

O que o payload carregaWhat the payload carriesQué lleva el payloaduma chave raiz, "StockTrackingDeatils" (o erro de grafia é do contrato), com um array de 24 chaves por item:one root key, "StockTrackingDeatils" (the misspelling is the contract's), holding an array of 24 keys per item:una clave raíz, "StockTrackingDeatils" (el error de grafía es del contrato), con un array de 24 claves por ítem:

Chave JSONJSON keyClave JSONOrigemOriginOrigen
orderPoNumberfixo ""fixed ""fijo ""
stockIdfixo ""fixed ""fijo ""
isAvailablecalculado — alta > 0 || baixa > 0. O único campo que lê a Baixa junto com a Alta.computed — high > 0 || low > 0. The only field that reads Low alongside High.calculado — alta > 0 || baja > 0. El único campo que lee la Baja junto con la Alta.
availableQuantityentry.highUomCount ?? 0
baseUOMproduct.uom.primaryName
baseUOMQuatityentry.highUomCount ?? 0 (grafia do contrato)(contract spelling)(grafía del contrato)
batchIdproduct.manufacturingSkus.first.batchId ou ""or ""o ""
damageQuantityfixo ""String, não númerofixed "" — a String, not a numberfijo ""String, no número
defaultUOMproduct.uom.primaryName (igual a baseUOM)(same as baseUOM)(igual a baseUOM)
defaultUOMQuatityentry.highUomCount ?? 0
locationresource.locationSfid
productIdproduct.productSfid
SKUIdproduct.manufacturingSkus.first.manufacturingSkuSfid ou ""or ""o ""
uom1Quantitya Altaentry.highUomCount?.toString() ?? "" (String, e vazio em vez de zero)the Highentry.highUomCount?.toString() ?? "" (String, and empty rather than zero)la Altaentry.highUomCount?.toString() ?? "" (String, y vacío en vez de cero)
uom2Quantitya Baixaentry.lowUomCount?.toString() ?? "" (String, e vazio em vez de zero)the Lowentry.lowUomCount?.toString() ?? "" (String, and empty rather than zero)la Bajaentry.lowUomCount?.toString() ?? "" (String, y vacío en vez de cero)
uom1Nameproduct.uom.primaryName
uom2Nameproduct.uom.secondaryName
currencyIsoCodecalculado do mercado — BR "BRL" · CL "CLP" · ZA "ZAR"computed from the market — BR "BRL" · CL "CLP" · ZA "ZAR"calculado del mercado — BR "BRL" · CL "CLP" · ZA "ZAR"
typefixo "Stock Check"fixed "Stock Check"fijo "Stock Check"
visitinput.visit?.sfid ?? ""
marketIsoinput.market.name"BR" / "CL" / "ZA"
sellableQuantityentry.highUomCount ?? 0
sapCustomerIdinput.visit?.accountData.customerCode ?? ""
resourceIdcalculado — primaryResourceSfid ou secondaryResourceSfid, conforme resource.isPrimaryResourcecomputed — primaryResourceSfid or secondaryResourceSfid, per resource.isPrimaryResourcecalculado — primaryResourceSfid o secondaryResourceSfid, según resource.isPrimaryResource

O builder itera os produtos (não as entradas), então a ordem dos itens segue a ordem do catálogo; um produto sem entrada, ou com as duas colunas vazias, é pulado. Note a assimetria entre as duas unidades: availableQuantity, baseUOMQuatity, defaultUOMQuatity e sellableQuantityquatro chaves —, mais uma quinta em uom1Quantity, carregam a Alta e a Alta; a Baixa chega ao backend por uma única chave, uom2Quantity. Note também que as quatro chaves numéricas usam ?? 0 enquanto as duas uomNQuantity usam ?? "": contar só a Baixa produz availableQuantity: 0 e uom1Quantity: "" no mesmo item. O envelope leva ainda o serviceName "LocationStockUploadAPI", o varejo (sfid + código SAP + nome), a data em dateReference e o accountSfid como referência da transação. A tabela completa campo-a-campo com tipos e regras e o exemplo de JSON estão em 26 · LocationStockUpload.The builder iterates the products (not the entries), so item order follows the catalog order; a product with no entry, or with both columns empty, is skipped. Note the asymmetry between the two units: availableQuantity, baseUOMQuatity, defaultUOMQuatity and sellableQuantityfour keys —, plus a fifth in uom1Quantity, carry the High and only the High; the Low reaches the backend through one single key, uom2Quantity. Note too that the four numeric keys use ?? 0 while the two uomNQuantity use ?? "": counting only the Low produces availableQuantity: 0 and uom1Quantity: "" on the same item. The envelope also carries the serviceName "LocationStockUploadAPI", the retail (sfid + SAP code + name), the date in dateReference and the accountSfid as the transaction reference. The full field-by-field table with types and rules, plus the JSON example, is in 26 · LocationStockUpload.El builder itera los productos (no las entradas), así que el orden de los ítems sigue el orden del catálogo; un producto sin entrada, o con las dos columnas vacías, se omite. Note la asimetría entre las dos unidades: availableQuantity, baseUOMQuatity, defaultUOMQuatity y sellableQuantitycuatro claves —, más una quinta en uom1Quantity, llevan la Alta y solo la Alta; la Baja llega al backend por una única clave, uom2Quantity. Note también que las cuatro claves numéricas usan ?? 0 mientras las dos uomNQuantity usan ?? "": contar solo la Baja produce availableQuantity: 0 y uom1Quantity: "" en el mismo ítem. El envelope lleva además el serviceName "LocationStockUploadAPI", el punto de venta (sfid + código SAP + nombre), la fecha en dateReference y el accountSfid como referencia de la transacción. La tabla completa campo a campo con tipos y reglas y el ejemplo de JSON están en 26 · LocationStockUpload.

SubmitStockCountUseCase 1 · enviosubmitenvío
MétodoMethodMétodoRetornaReturnsDevuelveUsoUseUso
submit({envelope})Future<Result<DispatcherAck, Failure>>Delegação pura ao DispatcherOrchestrator.dispatch — corpo de uma linha, sem validação, sem log e sem gravação. Não monta payload e não conhece o tipo, que vem dentro do envelope. Como só existe um envelope, não há Future.wait nem alinhamento por índice (ao contrário de pesquisas e merchandising).Pure delegation to DispatcherOrchestrator.dispatch — a one-line body, with no validation, no logging and no write. It builds no payload and doesn't know the type, which comes inside the envelope. Since there is only one envelope, there is no Future.wait and no index alignment (unlike surveys and merchandising).Delegación pura al DispatcherOrchestrator.dispatch — cuerpo de una línea, sin validación, sin log y sin grabación. No arma payload y no conoce el tipo, que viene dentro del envelope. Como solo existe un envelope, no hay Future.wait ni alineación por índice (a diferencia de encuestas y merchandising).

O orquestrador é quem grava o registro no histórico local de despachos, com o tipo, o serviceName, a referência, o mercado e o estado do envio. O caminho é remote-first por construção (§36): a escrita não tem datasource local nenhum — o repository do Dispatcher só conhece o gateway gRPC, e a única persistência é o registro de auditoria gravado depois que a chamada remota resolve. Em caso de sucesso, o payload é apagado do registro, então um envio bem-sucedido não pode ser reenviado; em erro o payload é preservado e o reenvio manual fica disponível — com aviso de duplicidade, porque a contagem não é lightweight. Offline o envelope nem entra na fila: a fila atende um único tipo, o de visita, então a contagem vai ao transporte, falha e é gravada como erro.The orchestrator is what writes the record into the local dispatch history, with the type, the serviceName, the reference, the market and the send state. The path is remote-first by construction (§36): the write has no local datasource at all — the Dispatcher repository knows only the gRPC gateway, and the only persistence is the audit record written after the remote call resolves. On success the payload is erased from the record, so a successful submission cannot be resent; on error the payload is preserved and manual resend is available — with a duplicate warning, because the count is not lightweight. Offline the envelope doesn't even enter the queue: the queue serves a single type, the visit one, so the count goes to transport, fails and is recorded as an error.El orquestador es quien graba el registro en el historial local de despachos, con el tipo, el serviceName, la referencia, el mercado y el estado del envío. El camino es remote-first por construcción (§36): la escritura no tiene datasource local alguno — el repository del Dispatcher solo conoce el gateway gRPC, y la única persistencia es el registro de auditoría grabado después de que la llamada remota resuelve. En caso de éxito el payload se borra del registro, así que un envío exitoso no puede reenviarse; en error el payload se preserva y el reenvío manual queda disponible — con aviso de duplicidad, porque el conteo no es lightweight. Offline el envelope ni entra en la cola: la cola atiende un único tipo, el de visita, así que el conteo va al transporte, falla y se graba como error.

12

Notifier & State

O StockCountNotifier (@riverpod, with AsyncGuard<StockCountState>) é o cérebro da tela e é uma family chaveada por accountSfid — um único parâmetro, ao contrário da verificação de preço, que precisa de dois. O build() é magro: observa os 4 UseCases e devolve _load() dentro do guardedBuild. O State (StockCountState, Freezed) é a fonte única de verdade: guarda o catálogo, o mapa de contagens digitadas (chaveado por productSfid), os dois filtros, a paginação e as flags de envio. Todos os totais, contagens, listas de opções e o agrupamento por categoria são calculados em getters do State — a Page e os widgets não decidem nada (§27). Segue a §37 — build() magro, _load() como dono único da montagem, refresh() sem sufixo de feature, sem invalidateSelf e sem AsyncValue.loading no refresh.The StockCountNotifier (@riverpod, with AsyncGuard<StockCountState>) is the screen's brain and is a family keyed by accountSfid — a single parameter, unlike price check, which needs two. build() is thin: it watches the 4 UseCases and returns _load() inside guardedBuild. The State (StockCountState, Freezed) is the single source of truth: it holds the catalog, the map of typed counts (keyed by productSfid), the two filters, pagination and the submit flags. Every total, count, option list and the per-category grouping are computed in State getters — the Page and the widgets decide nothing (§27). It follows §37 — thin build(), _load() as sole owner of assembly, refresh() with no feature suffix, no invalidateSelf and no AsyncValue.loading on refresh.El StockCountNotifier (@riverpod, with AsyncGuard<StockCountState>) es el cerebro de la pantalla y es una family indexada por accountSfid — un único parámetro, a diferencia de la verificación de precio, que necesita dos. El build() es delgado: observa los 4 UseCases y devuelve _load() dentro del guardedBuild. El State (StockCountState, Freezed) es la fuente única de verdad: guarda el catálogo, el mapa de conteos escritos (indexado por productSfid), los dos filtros, la paginación y las flags de envío. Todos los totales, conteos, listas de opciones y el agrupamiento por categoría se calculan en getters del State — la Page y los widgets no deciden nada (§27). Sigue la §37 — build() delgado, _load() como dueño único del armado, refresh() sin sufijo de feature, sin invalidateSelf y sin AsyncValue.loading en el refresh.

MétodosMethodsMétodos

build({accountSfid}) @override

RetornoReturnRetorno FutureOr<StockCountState>

Atribui os 4 UseCases via ref.watch (catálogo filtrado, visitas, builder de payload, envio) e devolve guardedBuild(body: () => _load(accountSfid:)). Nada é montado inline. O provider é autoDispose: sair da tela descarta o State — é isso que faz as contagens não sobreviverem à navegação.Assigns the 4 UseCases via ref.watch (filtered catalog, visits, payload builder, submit) and returns guardedBuild(body: () => _load(accountSfid:)). Nothing is assembled inline. The provider is autoDispose: leaving the screen discards the State — that is what makes the counts not survive navigation.Asigna los 4 UseCases vía ref.watch (catálogo filtrado, visitas, builder de payload, envío) y devuelve guardedBuild(body: () => _load(accountSfid:)). Nada se arma inline. El provider es autoDispose: salir de la pantalla descarta el State — eso es lo que hace que los conteos no sobrevivan a la navegación.

_load({accountSfid, source = local}) private

RetornoReturnRetorno Future<StockCountState>

Dono único da montagem. Chama execute(source:), faz getOrThrow() (a falha sobe como Failure e o AsyncGuard a converte em estado de erro) e devolve um State novo com products, lastSyncAt e visibleCount: 20. Não preserva nada — quem preserva é o refresh().Sole owner of assembly. Calls execute(source:), does getOrThrow() (the failure bubbles as a Failure and AsyncGuard turns it into an error state) and returns a fresh State with products, lastSyncAt and visibleCount: 20. It preserves nothing — preserving is refresh()'s job.Dueño único del armado. Llama execute(source:), hace getOrThrow() (el fallo sube como Failure y el AsyncGuard lo convierte en estado de error) y devuelve un State nuevo con products, lastSyncAt y visibleCount: 20. No preserva nada — quien preserva es el refresh().

refresh() pull-to-refresh

RetornoReturnRetorno Future<void>

Null-guard no state.value, depois runGuarded em torno de _load(source: remote) seguido de fresh.copyWith(...) que devolve três campos client do State anterior: entries, selectedCategoryGroup e selectedBrandFamily. Não devolve visibleCount — de propósito: a lista volta ao topo com 20 itens. Também não devolve isSubmitting nem submitSucceeded, então um refresh disparado durante um envio em voo zera as duas flags.Null-guards state.value, then runGuarded around _load(source: remote) followed by fresh.copyWith(...) restoring three client fields from the previous State: entries, selectedCategoryGroup and selectedBrandFamily. It doesn't restore visibleCount — deliberately: the list returns to the top with 20 items. It also doesn't restore isSubmitting or submitSucceeded, so a refresh fired during an in-flight submission clears both flags.Null-guard en el state.value, luego runGuarded alrededor de _load(source: remote) seguido de fresh.copyWith(...) que devuelve tres campos client del State anterior: entries, selectedCategoryGroup y selectedBrandFamily. No devuelve visibleCount — a propósito: la lista vuelve al tope con 20 ítems. Tampoco devuelve isSubmitting ni submitSucceeded, así que un refresh disparado durante un envío en vuelo pone en cero las dos flags.

loadMore()

RetornoReturnRetorno Future<void>

Guarda por hasMoreToLoad, espera 600 ms, e então relê o State e revalida a guarda antes de subir o visibleCount em 20 (limitado ao total filtrado). A releitura pós-await é a disciplina correta e evita crescer a janela sobre um State que mudou durante a espera — por exemplo por causa de um filtro aplicado nesse intervalo.Guards on hasMoreToLoad, waits 600 ms, and then re-reads the State and re-validates the guard before raising visibleCount by 20 (clamped to the filtered total). The post-await re-read is the correct discipline and avoids growing the window over a State that changed during the wait — for instance because a filter was applied in that interval.Guarda por hasMoreToLoad, espera 600 ms, y entonces relee el State y revalida la guarda antes de subir el visibleCount en 20 (limitado al total filtrado). La relectura post-await es la disciplina correcta y evita crecer la ventana sobre un State que cambió durante la espera — por ejemplo por un filtro aplicado en ese intervalo.

selectCategory({categoryGroup})

RetornoReturnRetorno void

Grava a categoria, limpa a marca e volta o visibleCount a 20. Aceita null, e é assim que a opção "limpar" da própria lista funciona. Nada é apagado do mapa de contagens.Writes the category, clears the brand and resets visibleCount to 20. It accepts null, which is how the picker's own "clear" option works. Nothing is erased from the counts map.Graba la categoría, limpia la marca y devuelve el visibleCount a 20. Acepta null, y así funciona la opción "limpiar" de la propia lista. Nada se borra del mapa de conteos.

selectBrand({brandFamily})

RetornoReturnRetorno void

Grava a marca e volta o visibleCount a 20. Não toca a categoria — a cascata é de mão única.Writes the brand and resets visibleCount to 20. It doesn't touch the category — the cascade is one-way.Graba la marca y devuelve el visibleCount a 20. No toca la categoría — la cascada es de una sola vía.

clearFilters() sem chamadorno callersin llamador

RetornoReturnRetorno void

Zeraria os dois filtros de uma vez, com guarda por hasActiveFilters. Nenhum widget o chama: o botão "Limpar Filtro" que o acionava foi removido em Jul/2026, e cada lista passou a limpar a sua própria seleção. Cinco features do app têm um clearFilters() e esta é a única sem chamador (ver Pendências).It would clear both filters at once, guarded by hasActiveFilters. No widget calls it: the "Clear Filter" button that drove it was removed in Jul/2026, and each picker now clears its own selection. Five features in the app have a clearFilters() and this is the only one with no caller (see Pending items).Pondría en cero los dos filtros de una vez, con guarda por hasActiveFilters. Ningún widget lo llama: el botón "Limpiar Filtro" que lo accionaba fue removido en Jul/2026, y cada lista pasó a limpiar su propia selección. Cinco features de la app tienen un clearFilters() y esta es la única sin llamador (ver Pendientes).

clearAllEntries()

RetornoReturnRetorno void

Substitui o mapa de contagens por um mapa vazio, com guarda por mapa já vazio. Apaga tudo, inclusive contagens de produtos que o filtro corrente esconde. Chamado pelo Limpar tudo do painel de resumo, depois da confirmação no modal.Replaces the counts map with an empty one, guarded against an already-empty map. It wipes everything, including counts of products the current filter hides. Called by the summary panel's Clear all, after the modal confirmation.Sustituye el mapa de conteos por un mapa vacío, con guarda por mapa ya vacío. Borra todo, incluidos conteos de productos que el filtro corriente esconde. Llamado por el Limpiar todo del panel de resumen, tras la confirmación en el modal.

updateHighUomCount({productSfid, value}) · updateLowUomCount({productSfid, value})

RetornoReturnRetorno void (cada um)(each)(cada uno)

Os dois pontos de escrita das contagens, um por coluna. Cada um delega a um _updateEntry privado passando a flag de preservação do outro lado (keepLow / keepHigh) — é esse mecanismo que garante que editar a Alta nunca mexa na Baixa. O _updateEntry copia o mapa, e então: se as duas contagens resultarem nulas, remove a chave do mapa em vez de guardar uma entrada zerada; caso contrário grava a entity nova. Em qualquer caso, força submitSucceeded: false — digitar depois de um envio bem-sucedido invalida o sinal de sucesso.The two write points for the counts, one per column. Each delegates to a private _updateEntry passing the other side's preservation flag (keepLow / keepHigh) — that mechanism is what guarantees editing High never touches Low. _updateEntry copies the map, and then: if both counts end up null, it removes the key from the map instead of storing a zeroed entry; otherwise it writes the new entity. Either way, it forces submitSucceeded: false — typing after a successful submission invalidates the success signal.Los dos puntos de escritura de los conteos, uno por columna. Cada uno delega a un _updateEntry privado pasando la flag de preservación del otro lado (keepLow / keepHigh) — ese mecanismo es el que garantiza que editar la Alta nunca toque la Baja. El _updateEntry copia el mapa, y entonces: si las dos cuentas resultan nulas, elimina la clave del mapa en vez de guardar una entrada en cero; si no, graba la entity nueva. En cualquier caso, fuerza submitSucceeded: false — escribir tras un envío exitoso invalida la señal de éxito.

dismissSuccessNotice() sem chamadorno callersin llamador

RetornoReturnRetorno void

Baixaria a flag submitSucceeded, com guarda por flag já baixada. Nenhum widget o chama — e como é o único leitor da flag em todo o app, o par método+flag é inerte: a tela sinaliza sucesso pelo aviso verde e sai. Ver Pendências.It would lower the submitSucceeded flag, guarded against an already-lowered flag. No widget calls it — and since it is the flag's only reader in the entire app, the method+flag pair is inert: the screen signals success with the green notice and leaves. See Pending items.Bajaría la flag submitSucceeded, con guarda por flag ya baja. Ningún widget lo llama — y como es el único lector de la flag en toda la app, el par método+flag es inerte: la pantalla señala el éxito con el aviso verde y sale. Ver Pendientes.

submit() enviosubmitenvío

RetornoReturnRetorno Future<Failure?>null em sucesso; a falha em erro. Não lança.null on success; the failure on error. It doesn't throw.null en éxito; el fallo en error. No lanza.

O único método com efeito remoto, e o dono do disparo (§39 — o widget só o chama). Sequência: guarda por State nulo e por hasAnyEntry (devolvendo UnknownFailure nos dois casos, indistinguíveis de um erro real para quem chama); liga isSubmitting; resolve o representante (nulo → aborta); resolve a visita do cache; monta o input de 7 campos com market e submittedAt lidos na hora; chama o builder e depois o envio. No fim, relê o State e grava isSubmitting: false, submitSucceeded: sent e — só em sucesso — entries vazio.The only method with a remote effect, and the owner of the dispatch (§39 — the widget merely calls it). Sequence: guard on null State and on hasAnyEntry (returning UnknownFailure in both cases, indistinguishable from a real error to the caller); turn on isSubmitting; resolve the rep (null → abort); resolve the visit from cache; build the 7-field input with market and submittedAt read on the spot; call the builder and then the submit. At the end, it re-reads the State and writes isSubmitting: false, submitSucceeded: sent and — on success only — an empty entries.El único método con efecto remoto, y el dueño del disparo (§39 — el widget solo lo llama). Secuencia: guarda por State nulo y por hasAnyEntry (devolviendo UnknownFailure en los dos casos, indistinguibles de un error real para quien llama); enciende isSubmitting; resuelve el representante (nulo → aborta); resuelve la visita del caché; arma el input de 7 campos con market y submittedAt leídos en el momento; llama al builder y luego al envío. Al final, relee el State y graba isSubmitting: false, submitSucceeded: sent y — solo en éxito — entries vacío.

Essa releitura final é o ponto onde a contagem de estoque acerta e a feature irmã erra: a verificação de preço grava o snapshot capturado antes do await e reverte em silêncio o que foi digitado durante o envio. Aqui não — no caminho de erro as contagens novas sobrevivem. A disciplina, porém, não é uniforme dentro do próprio método: o ramo de representante nulo ainda escreve sobre o snapshot pré-await, e o payload é montado a partir dele (ver Pendências).That final re-read is where stock count gets it right and the sibling feature gets it wrong: price check writes back the snapshot captured before the await and silently reverts whatever was typed during the submission. Not here — on the error path the new counts survive. The discipline, however, is not uniform within the method itself: the null-rep branch still writes over the pre-await snapshot, and the payload is assembled from it (see Pending items).Esa relectura final es el punto donde el conteo de stock acierta y la feature hermana falla: la verificación de precio graba el snapshot capturado antes del await y revierte en silencio lo que se escribió durante el envío. Aquí no — en el camino de error los conteos nuevos sobreviven. La disciplina, sin embargo, no es uniforme dentro del propio método: la rama de representante nulo aún escribe sobre el snapshot pre-await, y el payload se arma a partir de él (ver Pendientes).

State disponível para a PageState available to the PageState disponible para la Page

StockCountState 9 campos + 14 getters9 fields + 14 getters9 campos + 14 getters
campotipodefault
accountSfidStringobrigatóriorequiredobligatorio
productsList<ProductEntity>[]
entriesMap<String, StockCountEntryEntity>{}
visibleCountint0
selectedCategoryGroupString?null
selectedBrandFamilyString?null
isSubmittingboolfalse
submitSucceededboolfalse
lastSyncAtDateTime?null

Getters (14): accountProducts (refiltra a elegibilidade), filteredProducts (aplica categoria + marca), availableCategoryGroups, availableBrandFamilies, hasActiveFilters, hasAnyEntry, totalFiltered, visibleProducts, hasMoreToLoad, filledCount, emptyCount, totalHighUomUnits, totalLowUomUnits e filledProductsByCategory. O único campo obrigatório é o accountSfid, e o default visibleCount: 0 nunca é exercitado porque o _load sempre grava 20. Note que o State guarda os dois filtros como String crua, não como enum, e que o mercado deliberadamente não está aqui (§34).Getters (14): accountProducts (re-filters eligibility), filteredProducts (applies category + brand), availableCategoryGroups, availableBrandFamilies, hasActiveFilters, hasAnyEntry, totalFiltered, visibleProducts, hasMoreToLoad, filledCount, emptyCount, totalHighUomUnits, totalLowUomUnits and filledProductsByCategory. The only required field is accountSfid, and the visibleCount: 0 default is never exercised because _load always writes 20. Note that the State keeps both filters as raw String, not as enums, and that the market deliberately isn't here (§34).Getters (14): accountProducts (refiltra la elegibilidad), filteredProducts (aplica categoría + marca), availableCategoryGroups, availableBrandFamilies, hasActiveFilters, hasAnyEntry, totalFiltered, visibleProducts, hasMoreToLoad, filledCount, emptyCount, totalHighUomUnits, totalLowUomUnits y filledProductsByCategory. El único campo obligatorio es el accountSfid, y el default visibleCount: 0 nunca se ejercita porque el _load siempre graba 20. Note que el State guarda los dos filtros como String cruda, no como enum, y que el mercado deliberadamente no está aquí (§34).

Duas notas de desenho. (a) accountProducts e filteredProducts são memoizados num par de Expando de nível de arquivo — cache por identidade de objeto, ou seja, todo copyWith é um miss; o ganho existe só dentro da vida de uma instância (vários getters lidos no mesmo rebuild). Os quatro contadores e somatórios não são memoizados e são recomputados várias vezes por rebuild do rodapé. (b) O nome accountProducts promete recorte por varejo que o getter não faz: ele só repete o filtro de elegibilidade que o UseCase já aplicou (ver Pendências).Two design notes. (a) accountProducts and filteredProducts are memoized in a pair of file-level Expandos — a cache keyed by object identity, meaning every copyWith is a miss; the gain exists only within one instance's lifetime (several getters read in the same rebuild). The four counters and sums are not memoized and are recomputed several times per footer rebuild. (b) The name accountProducts promises a per-retail slice the getter doesn't perform: it merely repeats the eligibility filter the UseCase already applied (see Pending items).Dos notas de diseño. (a) accountProducts y filteredProducts están memoizados en un par de Expando de nivel de archivo — caché por identidad de objeto, o sea, todo copyWith es un miss; la ganancia existe solo dentro de la vida de una instancia (varios getters leídos en el mismo rebuild). Los cuatro contadores y sumatorios no están memoizados y se recomputan varias veces por rebuild del pie. (b) El nombre accountProducts promete recorte por punto de venta que el getter no hace: solo repite el filtro de elegibilidad que el UseCase ya aplicó (ver Pendientes).

13

Page e widgetsPage & widgetsPage y widgets

A StockCountPage (ConsumerWidget) recebe só o identificador do varejo (§17), observa a family do notifier e resolve loading/error/data num when global — o conteúdo existe só no ramo data. O corpo é um ConsumerStatefulWidget privado porque precisa de um ScrollController próprio: é a rolagem da página, não a da lista, que dispara o carregamento incremental (a 200 px do fim). Um Stack de dois Positioned.fill mantém a barra de resumo por cima do conteúdo rolável. A Page não decide nada: passa o State inteiro adiante e cada filho resolve a própria visibilidade (§27). Árvore de composição, com os modais aninhados sob quem os abre:StockCountPage (ConsumerWidget) receives only the retail identifier (§17), watches the notifier family and resolves loading/error/data in a global when — content exists only in the data branch. The body is a private ConsumerStatefulWidget because it needs its own ScrollController: it is the page's scroll, not the list's, that triggers incremental loading (200 px from the end). A Stack of two Positioned.fill keeps the summary bar above the scrollable content. The Page decides nothing: it passes the whole State down and each child resolves its own visibility (§27). Composition tree, with the modals nested under whoever opens them:La StockCountPage (ConsumerWidget) recibe solo el identificador del punto de venta (§17), observa la family del notifier y resuelve loading/error/data en un when global — el contenido existe solo en la rama data. El cuerpo es un ConsumerStatefulWidget privado porque necesita un ScrollController propio: es el desplazamiento de la página, no el de la lista, el que dispara la carga incremental (a 200 px del final). Un Stack de dos Positioned.fill mantiene la barra de resumen por encima del contenido desplazable. La Page no decide nada: pasa el State entero adelante y cada hijo resuelve su propia visibilidad (§27). Árbol de composición, con los modales anidados bajo quien los abre:

  • StockCountPage ConsumerWidget · accountSfid
    • AppPageShell displayBackButton · fundo padrão da página (§19)
      • CustomLoadingIndicator loading · centralizado
      • FailureStateView error → ref.invalidate(stockCountProvider)
      • _StockCountBody data · ConsumerStatefulWidget · ScrollController → loadMore()
        • Stack
          • CustomPullToRefresh → refresh() · controller compartilhado com o body
            • DataLoadInfo state.lastSyncAt (do próprio catálogo, §23)
            • StockCountHeaderWidget ícone + título · StatelessWidget sem parâmetros
            • StockCountFiltersWidget ConsumerWidget · sem early-return
              • CustomDropdown<String>.single categoria · rótulo via CategoryGroupUx · → selectCategory
              • CustomDropdown<String>.single marca · enabled só com categoria escolhida · → selectBrand
            • StockCountTableHeaderWidget SizedBox.shrink se totalFiltered == 0
              • _ColumnLabel Alta · largura md65, igual à do input
              • _ColumnLabel Baixa · largura md65
            • StockCountListWidget ConsumerWidget
              • CustomEmptyState totalFiltered == 0 · ícone de caixas + stock_count_empty_state
              • InfiniteScrollListView<ProductEntity> shrinkWrap · NeverScrollableScrollPhysics · onLoadMore → loadMore()
                • StockCountProductCardWidget ConsumerStatefulWidget · 2 controllers + 2 focus nodes · card xxl30 (§24)
                  • ProductInfoButton
                    • ProductInfoModalContent modal · shared · nome + foto
                  • _UomInputField Alta · outlinedPillCompact · digitsOnly · → updateHighUomCount
                  • _UomInputField Baixa · outlinedPillCompact · digitsOnly · → updateLowUomCount
                  • StockCountHistoryGridWidget 4 colunas · lê salesHistory do varejo, ordenado desc
                    • _DateCell pílula azul cheia · dia/mês
                    • _RowLabel Alta / Baixa · uma vez cada
                    • _ValueCell ppq / psq · "-" quando vazio
                • CustomLoadingIndicator linha extra do próprio InfiniteScrollListView enquanto há mais a carregar
            • PaginationCountIndicator X de Y · visibleProducts.length / totalFiltered
          • StockCountStickyFooterWidget ConsumerWidget · Positioned.fill por cima
            • CustomSummaryBar visible: hasAnyEntry · 3 stats fechada / 5 no rodapé expandido
              • StockCountSummaryModalContent expandedContent (inline, não é modal) · SizedBox.shrink se nada agrupado
              • CustomSummaryAction enviar · isLoading = isSubmitting
                • StockCountSubmitConfirmModalContent modal · devolve bool · confirmar → notifier.submit()
              • CustomSummaryAction secundária · Limpar tudo
                • StockCountClearConfirmModalContent modal · devolve bool · confirmar → clearAllEntries()

O fluxo de envio mora no rodapé: ele abre o modal de confirmação, e se a resposta for afirmativa chama notifier.submit(); em sucesso mostra o aviso verde e volta, em erro mostra o aviso vermelho com o código da falha e permanece. Isso satisfaz a §39 — a lógica e o disparo vivem no Notifier, e o widget só orquestra a sequência de UI que precisa de BuildContext. Três observações sobre a árvore: (a) o StockCountSummaryModalContent tem nome de modal mas não é um — é conteúdo embutido do painel expandido, e não devolve valor; (b) o cartão de produto sincroniza os dois campos de texto a cada rebuild com uma guarda de foco, para nunca sobrescrever o campo em que o representante está digitando; (c) o InfiniteScrollListView traz o próprio ScrollController e a própria detecção de fim de lista, mas como aqui ele é shrinkWrap com física não-rolável, quem realmente dispara o loadMore() é a rolagem da página (ver Pendências).The submit flow lives in the footer: it opens the confirmation modal and only if the answer is affirmative calls notifier.submit(); on success it shows the green notice and returns, on error it shows the red notice with the failure code and stays. This satisfies §39 — the logic and the dispatch live in the Notifier, and the widget merely orchestrates the UI sequence that needs a BuildContext. Three notes on the tree: (a) StockCountSummaryModalContent is named like a modal but isn't one — it is embedded content of the expanded panel, and returns no value; (b) the product card syncs both text fields on every rebuild with a focus guard, so it never overwrites the field the rep is typing in; (c) InfiniteScrollListView brings its own ScrollController and its own end-of-list detection, but since here it is shrinkWrap with non-scrollable physics, what actually fires loadMore() is the page's scroll (see Pending items).El flujo de envío vive en el pie: abre el modal de confirmación y solo si la respuesta es afirmativa llama notifier.submit(); en éxito muestra el aviso verde y vuelve, en error muestra el aviso rojo con el código del fallo y permanece. Esto satisface la §39 — la lógica y el disparo viven en el Notifier, y el widget solo orquesta la secuencia de UI que necesita BuildContext. Tres observaciones sobre el árbol: (a) el StockCountSummaryModalContent tiene nombre de modal pero no lo es — es contenido embebido del panel expandido, y no devuelve valor; (b) la tarjeta de producto sincroniza los dos campos de texto en cada rebuild con una guarda de foco, para nunca sobrescribir el campo en que el representante está escribiendo; (c) el InfiniteScrollListView trae su propio ScrollController y su propia detección de fin de lista, pero como aquí es shrinkWrap con física no desplazable, quien realmente dispara el loadMore() es el desplazamiento de la página (ver Pendientes).

Componentes compartilhados reusados (§31), com os nomes reais das classes: AppPageShell, CustomLoadingIndicator, CustomPullToRefresh, FailureStateView, DataLoadInfo, PaginationCountIndicator, CustomEmptyState, CustomDropdown, InfiniteScrollListView, CustomInput, ProductInfoButton, CustomSummaryBar, ConectaModal + ConectaModalScaffold, ConectaNotice, CustomButton, CustomText, CustomIcon. Zero reimplementação: nem barra de resumo própria, nem estado vazio próprio, nem spinner cru. Note que os nomes canônicos do projeto são CustomLoadingIndicator, CustomPullToRefresh e CustomSummaryBar — não as variantes com prefixo Conecta que aparecem em resumos antigos.Reused shared components (§31), with the real class names: AppPageShell, CustomLoadingIndicator, CustomPullToRefresh, FailureStateView, DataLoadInfo, PaginationCountIndicator, CustomEmptyState, CustomDropdown, InfiniteScrollListView, CustomInput, ProductInfoButton, CustomSummaryBar, ConectaModal + ConectaModalScaffold, ConectaNotice, CustomButton, CustomText, CustomIcon. Zero reimplementation: no bespoke summary bar, no bespoke empty state, no raw spinner. Note that the project's canonical names are CustomLoadingIndicator, CustomPullToRefresh and CustomSummaryBar — not the Conecta-prefixed variants that appear in older summaries.Componentes compartidos reusados (§31), con los nombres reales de las clases: AppPageShell, CustomLoadingIndicator, CustomPullToRefresh, FailureStateView, DataLoadInfo, PaginationCountIndicator, CustomEmptyState, CustomDropdown, InfiniteScrollListView, CustomInput, ProductInfoButton, CustomSummaryBar, ConectaModal + ConectaModalScaffold, ConectaNotice, CustomButton, CustomText, CustomIcon. Cero reimplementación: ni barra de resumen propia, ni estado vacío propio, ni spinner crudo. Note que los nombres canónicos del proyecto son CustomLoadingIndicator, CustomPullToRefresh y CustomSummaryBar — no las variantes con prefijo Conecta que aparecen en resúmenes antiguos.

Notas por mercadoMarket notesNotas por mercado

A contagem de estoque está disponível em três mercados — Brasil, Chile e África do Sul — e tem dois gates independentes no End Market Configuration, com chaves de nomes diferentes: o tile da grade de ferramentas da visita, declarado como stock_history (presente nos três), e a categoria de pendência do encerramento de visita, declarada como stock_count (presente só em BR e ZA). A própria tela não lê configuração de mercado nenhuma: o único dado de mercado que ela consulta é o código ISO da moeda, e só no momento de montar o payload.Stock count is available in three markets — Brazil, Chile and South Africa — and has two independent gates in the End Market Configuration, under differently-named keys: the visit tools grid tile, declared as stock_history (present in all three), and the visit-end pending category, declared as stock_count (present only in BR and ZA). The screen itself reads no market configuration at all: the only market data it consults is the currency ISO code, and only when assembling the payload.El conteo de stock está disponible en tres mercados — Brasil, Chile y Sudáfrica — y tiene dos gates independientes en el End Market Configuration, con claves de nombres diferentes: el tile de la grilla de herramientas de la visita, declarado como stock_history (presente en los tres), y la categoría de pendiente del cierre de visita, declarada como stock_count (presente solo en BR y ZA). La propia pantalla no lee configuración de mercado alguna: el único dato de mercado que consulta es el código ISO de la moneda, y solo al armar el payload.

BRx CLx ZAx AR PY PE
disponívelavailabledisponible presente, desligadopresent, offpresente, apagado ausenteabsentausente

Gate 1: o tile na grade de ferramentas da visita (stock_history)Gate 1: the tile in the visit tools grid (stock_history)Gate 1: el tile en la grilla de herramientas de la visita (stock_history)

ChaveKeyClaveBRCLZAARPYPE
visitDetailConfigxxx
modules[]1588
modules[visit_detail_tools_grid].isVisiblexxx
details[]9109
details[stock_history].moduleDetailName"stock_history""stock_history""stock_history"
details[stock_history].isVisiblexxx
posição do tile na gradetile position in the gridposición del tile en la grilla6/95/104/9

Cada item da grade tem exatamente 2 propriedades no arquivo real (moduleDetailName e isVisible); a ordem é posicional, e ícone e rótulo vêm do código. Para o tile aparecer basta isVisible: true: ao contrário de Prime e Conecta Você, a contagem de estoque não tem regra de elegibilidade adicional no código — o único portão extra é a verificação de visita iniciada, comum a todos os tiles. Nos três mercados, todos os itens da grade estão visíveis com uma única exceção em toda a grade: performance no Chile, que é o único isVisible: false das 28 declarações de detalhe. Os três arquivos de EMC (produção, UAT e pré-produção) são idênticos neste bloco — a varredura confirmou que visitDetailConfig, visitEndConfig, dataFreshnessConfig e orderCreationConfig não divergem em nenhum dos 6 mercados.Each grid item has exactly 2 properties in the real file (moduleDetailName and isVisible); order is positional, and icon and label come from code. For the tile to appear, isVisible: true is enough: unlike Prime and Conecta Você, stock count has no additional eligibility rule in code — the only extra gate is the visit-started check, common to every tile. In all three markets every grid item is visible with a single exception across the whole grid: performance in Chile, the only isVisible: false among the 28 detail declarations. The three EMC files (production, UAT and pre-production) are identical in this block — the sweep confirmed that visitDetailConfig, visitEndConfig, dataFreshnessConfig and orderCreationConfig don't diverge in any of the 6 markets.Cada ítem de la grilla tiene exactamente 2 propiedades en el archivo real (moduleDetailName e isVisible); el orden es posicional, y el ícono y el rótulo vienen del código. Para que el tile aparezca basta isVisible: true: a diferencia de Prime y Conecta Você, el conteo de stock no tiene regla de elegibilidad adicional en el código — el único portón extra es la verificación de visita iniciada, común a todos los tiles. En los tres mercados todos los ítems de la grilla están visibles con una única excepción en toda la grilla: performance en Chile, el único isVisible: false de las 28 declaraciones de detalle. Los tres archivos de EMC (producción, UAT y preproducción) son idénticos en este bloque — el barrido confirmó que visitDetailConfig, visitEndConfig, dataFreshnessConfig y orderCreationConfig no divergen en ninguno de los 6 mercados.

Gate 2: a pendência do encerramento de visita (stock_count)Gate 2: the visit-end pending (stock_count)Gate 2: el pendiente del cierre de visita (stock_count)

ChaveKeyClaveBRCLZAARPYPE
visitEndConfigxxx
categories[]625
categories[stock_count]xx
isVisiblexx
isBlockingfalsefalse
allowedResourceTypes[]66
posição na listaposition in the listposición en la lista3/63/5

A categoria tem exatamente 4 propriedades (category, isVisible, isBlocking, allowedResourceTypes) — não há campo de ordem nem de rótulo. Em BR e ZA os conteúdos são idênticos, incluindo os mesmos 6 tipos de representante permitidos: Pre-sales Rep, Prompt-sales Rep, Universal Rep, Delivery Rep, Telesales Analyst e Web Agent - Direct. O único tipo de representante existente que fica de fora é o Trade Marketing Rep: ele vê o tile (tiles não têm gate de papel) mas nunca vê a pendência. isBlocking: false nos dois mercados — a contagem nunca impede finalizar a visita; em todo o arquivo de EMC, a única categoria bloqueante é mandatory_surveys. O Chile tem visitEndConfig, mas declara só duas categorias (pending_tasks e unsynced_transactions), nenhuma delas a contagem.The category has exactly 4 properties (category, isVisible, isBlocking, allowedResourceTypes) — there is no order or label field. In BR and ZA the contents are identical, including the same 6 allowed rep types: Pre-sales Rep, Prompt-sales Rep, Universal Rep, Delivery Rep, Telesales Analyst and Web Agent - Direct. The only existing rep type left out is Trade Marketing Rep: it sees the tile (tiles have no role gate) but never sees the pending. isBlocking: false in both markets — the count never prevents finishing the visit; across the whole EMC file, the only blocking category is mandatory_surveys. Chile has visitEndConfig but declares only two categories (pending_tasks and unsynced_transactions), neither of them the count.La categoría tiene exactamente 4 propiedades (category, isVisible, isBlocking, allowedResourceTypes) — no hay campo de orden ni de rótulo. En BR y ZA los contenidos son idénticos, incluidos los mismos 6 tipos de representante permitidos: Pre-sales Rep, Prompt-sales Rep, Universal Rep, Delivery Rep, Telesales Analyst y Web Agent - Direct. El único tipo de representante existente que queda fuera es el Trade Marketing Rep: ve el tile (los tiles no tienen gate de rol) pero nunca ve el pendiente. isBlocking: false en los dos mercados — el conteo nunca impide finalizar la visita; en todo el archivo de EMC, la única categoría bloqueante es mandatory_surveys. Chile tiene visitEndConfig, pero declara solo dos categorías (pending_tasks y unsynced_transactions), ninguna de ellas el conteo.

Frescor do catálogo, sincronização e despachoCatalog freshness, sync and dispatchFrescura del catálogo, sincronización y despacho

ChaveKeyClaveBRCLZAARPYPE
dataFreshnessConfigxxx
ttlSecondsByType.productCatalog864008640086400
ttlSecondsByType.stockControl ¹900900900
ttlSecondsByType.stockCount
defaultTtlSeconds300300300
sweepIntervalSeconds606060
DataSyncType.productCatalog.enabledMarketsxxx
DataSyncType.stockCount ²
DispatcherType.stockCount.enabledMarketsxxx

O TTL de 24 h põe o catálogo de produtos na faixa mais folgada do app — 288× o default de 5 min, e muito acima de visitas e pedidos (10 min). Consequência prática: o sweep de frescor raramente força uma releitura remota do catálogo, e a atualização real da lista vem do pull-to-refresh. Os três blocos dataFreshnessConfig declaram as mesmas 18 chaves de TTL, idênticas nos três arquivos de EMC. ¹ stockControl aparece aqui apenas para deixar claro que não é desta feature: é o TTL do saldo da van, e a contagem de estoque não o lê. ² Não existe DataSyncType.stockCount — dos 26 tipos de sincronização, nenhum é a contagem, e é correto que seja assim: a feature é somente de escrita, então não há o que sincronizar. AR/PY/PE não têm o bloco: para eles a configuração cai nos defaults do código (5 min de TTL, sweep de 60 s), o que é inócuo porque o catálogo também não está entre as estruturas habilitadas.The 24 h TTL puts the product catalog in the app's loosest tier — 288× the 5-minute default, and far above visits and orders (10 min). Practical consequence: the freshness sweep rarely forces a remote re-read of the catalog, and the list's real update comes from pull-to-refresh. The three dataFreshnessConfig blocks declare the same 18 TTL keys, identical across the three EMC files. ¹ stockControl appears here only to make clear it is not this feature's: it is the van balance TTL, and stock count doesn't read it. ² There is no DataSyncType.stockCount — of the 26 sync types, none is the count, and that is correct: the feature is write-only, so there is nothing to sync. AR/PY/PE have no block: for them configuration falls back to the code defaults (5-minute TTL, 60 s sweep), which is harmless because the catalog isn't among the enabled structures either.El TTL de 24 h pone al catálogo de productos en la franja más holgada de la app — 288× el default de 5 min, y muy arriba de visitas y pedidos (10 min). Consecuencia práctica: el sweep de frescura raramente fuerza una relectura remota del catálogo, y la actualización real de la lista viene del pull-to-refresh. Los tres bloques dataFreshnessConfig declaran las mismas 18 claves de TTL, idénticas en los tres archivos de EMC. ¹ stockControl aparece aquí solo para dejar claro que no es de esta feature: es el TTL del saldo de la van, y el conteo de stock no lo lee. ² No existe DataSyncType.stockCount — de los 26 tipos de sincronización, ninguno es el conteo, y es correcto que sea así: la feature es solo de escritura, así que no hay qué sincronizar. AR/PY/PE no tienen el bloque: para ellos la configuración cae en los defaults del código (5 min de TTL, sweep de 60 s), lo que es inocuo porque el catálogo tampoco está entre las estructuras habilitadas.

Mocks e traduções por mercadoMocks and translations per marketMocks y traducciones por mercado

Arquivo · medidaFile · measureArchivo · medidaBRCLZAARPYPE
{mercado}_products.jsonprodutosproductsproductos3363000
elegíveis para contagem de estoqueeligible for stock countelegibles para conteo de stock3363000
com salesHistory (a grade)with salesHistory (the grid)con salesHistory (la grilla)221000
{mercado}_real_products.jsonprodutosproductsproductos17813150
elegíveis para contagem de estoqueeligible for stock countelegibles para conteo de stock408121
com salesHistory (a grade)with salesHistory (the grid)con salesHistory (la grilla)201
chaves de tradução stock_count_*stock_count_* translation keysclaves de traducción stock_count_*272727272727

Contagens medidas nos arquivos. Nos mocks sintéticos todos os produtos são elegíveis para contagem de estoque (3, 3 e 63); nos dumps reais a proporção cai bastante em BR (40 de 178) e CL (8 de 13), mas é altíssima em ZA (121 de 150) — logo só a África do Sul exercita a paginação de verdade. O ponto crítico está na terceira e na sexta linha: a grade de histórico praticamente não tem dado em nenhum mercado. No melhor caso são 2 produtos com histórico, e no dump real do Chile são zero — ou seja, no Chile, em modo mock real, os 8 produtos elegíveis aparecem com a grade inteira preenchida com - e a pendência do encerramento nunca seria disparada (se o Chile a declarasse). As 27 chaves de tradução estão presentes nos 6 mercados, sem nenhuma assimetria; ZA em inglês, BR em português e CL/AR/PY/PE compartilhando o mesmo espanhol, byte a byte.Counts measured in the files. In the synthetic mocks every product is eligible for stock count (3, 3 and 63); in the real dumps the proportion drops sharply in BR (40 of 178) and CL (8 of 13), but is very high in ZA (121 of 150) — so only South Africa really exercises pagination. The critical point is in the third and sixth rows: the history grid has almost no data in any market. At best there are 2 products with history, and in Chile's real dump there are zero — meaning that in Chile, in real-mock mode, the 8 eligible products show up with the whole grid filled with - and the visit-end pending would never fire (if Chile declared it). The 27 translation keys are present in all 6 markets, with no asymmetry; ZA in English, BR in Portuguese and CL/AR/PY/PE sharing the same Spanish, byte for byte.Conteos medidos en los archivos. En los mocks sintéticos todos los productos son elegibles para conteo de stock (3, 3 y 63); en los dumps reales la proporción cae bastante en BR (40 de 178) y CL (8 de 13), pero es altísima en ZA (121 de 150) — así que solo Sudáfrica ejercita la paginación de verdad. El punto crítico está en la tercera y la sexta fila: la grilla de historial casi no tiene dato en ningún mercado. En el mejor caso son 2 productos con historial, y en el dump real de Chile son cero — o sea, en Chile, en modo mock real, los 8 productos elegibles aparecen con la grilla entera llena de - y el pendiente del cierre nunca se dispararía (si Chile lo declarara). Las 27 claves de traducción están presentes en los 6 mercados, sin ninguna asimetría; ZA en inglés, BR en portugués y CL/AR/PY/PE compartiendo el mismo español, byte a byte.

BRZA

Os dois mercados completos — as duas portasThe two complete markets — both doorsLos dos mercados completos — las dos puertas Brasil e África do Sul são os únicos com os dois gates ligados: o tile na grade de ferramentas e a pendência no encerramento de visita, com configuração byte-idêntica nos dois (mesmos 6 papéis permitidos, isBlocking: false, 3ª posição na lista). Diferem no entorno: o tile brasileiro é o 6º de 9 numa grade de 15 módulos, o sul-africano é o 4º de 9 numa de 8; e o Brasil tem 6 categorias de pendência contra 5 da África do Sul (o Brasil declara também smart_investment). O contraste de dados é grande: o dump real do Brasil tem 178 produtos com apenas 40 elegíveis, enquanto o da África do Sul tem 150 com 121 elegíveis — na prática, a lista sul-africana é três vezes maior. O rótulo do tile é o ponto fraco do Brasil: diz "Falta", enquanto o cartão de pendência e o título da tela dizem "Contagem de estoque" (ver Pendências). Moeda no payload: BRL e ZAR. Brazil and South Africa are the only ones with both gates on: the tools-grid tile and the visit-end pending, with byte-identical configuration in both (same 6 allowed roles, isBlocking: false, 3rd position in the list). They differ in their surroundings: Brazil's tile is the 6th of 9 in a 15-module grid, South Africa's is the 4th of 9 in an 8-module one; and Brazil has 6 pending categories against South Africa's 5 (Brazil also declares smart_investment). The data contrast is large: Brazil's real dump has 178 products with only 40 eligible, while South Africa's has 150 with 121 eligible — in practice the South African list is three times bigger. The tile label is Brazil's weak spot: it says "Falta" (shortage), while the pending card and the screen title say "Contagem de estoque" (see Pending items). Payload currency: BRL and ZAR. Brasil y Sudáfrica son los únicos con los dos gates activos: el tile en la grilla de herramientas y el pendiente en el cierre de visita, con configuración byte-idéntica en los dos (mismos 6 roles permitidos, isBlocking: false, 3ª posición en la lista). Difieren en el entorno: el tile brasileño es el 6º de 9 en una grilla de 15 módulos, el sudafricano es el 4º de 9 en una de 8; y Brasil tiene 6 categorías de pendiente contra 5 de Sudáfrica (Brasil declara además smart_investment). El contraste de datos es grande: el dump real de Brasil tiene 178 productos con apenas 40 elegibles, mientras el de Sudáfrica tiene 150 con 121 elegibles — en la práctica, la lista sudafricana es tres veces mayor. El rótulo del tile es el punto débil de Brasil: dice "Falta", mientras la tarjeta de pendiente y el título de la pantalla dicen "Contagem de estoque" (ver Pendientes). Moneda en el payload: BRL y ZAR.

CL

Tem a ferramenta, não tem a cobrançaHas the tool, not the nudgeTiene la herramienta, no el recordatorio O Chile tem o tile (5º de 10, a grade mais longa dos três mercados) e tem o tipo de despacho habilitado — a contagem é enviada e aceita normalmente. O que ele não tem é a categoria de pendência: das 6 categorias que o Brasil declara, o Chile declara só 2 (pending_tasks e unsynced_transactions), e nenhuma é a contagem. Consequência: no Chile a contagem é uma ferramenta puramente opcional — o encerramento de visita nunca lembra o representante de fazê-la, e o cartão tocável que serve de segundo caminho de acesso não existe. Habilitar bastaria acrescentar a categoria ao visitEndConfig, sem nenhuma mudança de código. Agrava a situação o estado dos dados: o dump real do Chile é o único dos três sem nenhum produto com histórico de contagem, então mesmo que a categoria fosse declarada, o critério que dispara a pendência nunca casaria. Moeda no payload: CLP — sem casas decimais, coerente com contagens inteiras. Chile has the tile (5th of 10, the longest grid of the three markets) and has the dispatch type enabled — the count is sent and accepted normally. What it does not have is the pending category: of the 6 categories Brazil declares, Chile declares only 2 (pending_tasks and unsynced_transactions), and neither is the count. Consequence: in Chile the count is a purely optional tool — visit end never reminds the rep to do it, and the tappable card that serves as the second entry path doesn't exist. Enabling it would just mean adding the category to visitEndConfig, with no code change. The data state makes it worse: Chile's real dump is the only one of the three with no product carrying count history, so even if the category were declared, the criterion that fires the pending would never match. Payload currency: CLP — no decimal places, consistent with whole-number counts. Chile tiene el tile (5º de 10, la grilla más larga de los tres mercados) y tiene el tipo de despacho habilitado — el conteo se envía y se acepta normalmente. Lo que no tiene es la categoría de pendiente: de las 6 categorías que Brasil declara, Chile declara solo 2 (pending_tasks y unsynced_transactions), y ninguna es el conteo. Consecuencia: en Chile el conteo es una herramienta puramente opcional — el cierre de visita nunca le recuerda al representante hacerlo, y la tarjeta tocable que sirve de segundo camino de acceso no existe. Habilitarlo bastaría con agregar la categoría al visitEndConfig, sin ningún cambio de código. Agrava la situación el estado de los datos: el dump real de Chile es el único de los tres sin ningún producto con historial de conteo, así que incluso si la categoría estuviera declarada, el criterio que dispara el pendiente nunca coincidiría. Moneda en el payload: CLP — sin decimales, coherente con conteos enteros.

ARPYPE

AR · PY · PE — sem a featureAR · PY · PE — feature absentAR · PY · PE — sin la feature Existem como mercados do app, mas com configuração mínima: o EMC deles tem apenas quatro blocos de topo (versão, atualização, novo varejo e visitas), contra 25 do Brasil e do Chile e 23 da África do Sul — nem visitDetailConfig, nem visitEndConfig, nem dataFreshnessConfig estão entre eles. Sem a grade de ferramentas da visita não há atalho para chegar à tela, e sem a categoria de pendência não há o segundo caminho; o catálogo de produtos também não está entre as estruturas sincronizadas; o mock é um objeto vazio de 21 bytes, sem versão de dump real; e o tipo de despacho não os habilita. Nada falha — a feature simplesmente não existe nesses mercados, embora as 27 traduções estejam completas nos três. Se algum deles fosse habilitado, herdaria tudo o que está descrito nas Pendências, mais o fato de que a tela abriria com o cartão de estado vazio, porque o catálogo estaria vazio. They exist as app markets, but with minimal configuration: their EMC has only four top-level blocks (version, update, new retail and visits), against 25 for Brazil and Chile and 23 for South Africa — neither visitDetailConfig, nor visitEndConfig, nor dataFreshnessConfig is among them. With no visit tools grid there is no shortcut to reach the screen, and with no pending category there is no second path; the product catalog isn't among the synced structures either; the mock is an empty 21-byte object, with no real-dump version; and the dispatch type doesn't enable them. Nothing fails — the feature simply doesn't exist in those markets, even though the 27 translations are complete in all three. If any of them were enabled, it would inherit everything described in Pending items, plus the fact that the screen would open on the empty-state card, because the catalog would be empty. Existen como mercados de la app, pero con configuración mínima: su EMC tiene apenas cuatro bloques de tope (versión, actualización, nuevo punto de venta y visitas), contra 25 de Brasil y Chile y 23 de Sudáfrica — ni visitDetailConfig, ni visitEndConfig, ni dataFreshnessConfig están entre ellos. Sin la grilla de herramientas de la visita no hay atajo para llegar a la pantalla, y sin la categoría de pendiente no hay segundo camino; el catálogo de productos tampoco está entre las estructuras sincronizadas; el mock es un objeto vacío de 21 bytes, sin versión de dump real; y el tipo de despacho no los habilita. Nada falla — la feature simplemente no existe en esos mercados, aunque las 27 traducciones estén completas en los tres. Si alguno fuera habilitado, heredaría todo lo descrito en Pendientes, más el hecho de que la pantalla abriría con la tarjeta de estado vacío, porque el catálogo estaría vacío.

Pendências / roadmapPending items / roadmapPendientes / roadmap

  • Um envio que FALHOU marca a pendência do encerramento como resolvida. O critério de "já contou hoje" é a simples existência de um registro de despacho do tipo, para aquele varejo, naquela data — dispatches.isNotEmpty, sem olhar o status. Como o orquestrador grava um registro em todos os desfechos, inclusive falha de transporte e rejeição do backend, uma contagem enviada offline (que falha na hora, porque a fila só atende visitas) faz o cartão de pendência aparecer como resolvido. Prova de que é defeito e não desenho: no mesmo arquivo, o contador de transações não sincronizadas filtra por !status.isSuccessEquivalent — ou seja, a disciplina de status existe e não foi aplicada aqui. Nuance importante: o erro não desaparece do encerramento — o mesmo despacho falhado passa a ser contado pela categoria genérica unsynced_transactions, declarada nos três mercados; ele só muda de linha, e o rep perde o vínculo com o varejo que faltou contar. Correção barata: filtrar o mesmo predicado em _wasHandledToday. Mitiga o impacto o fato de a categoria ser isBlocking: false nos dois mercados que a declaram, então nada travava; o dano é de informação.A submission that FAILED marks the visit-end pending as resolved. The "already counted today" criterion is the mere existence of a dispatch record of the type, for that retail, on that date — dispatches.isNotEmpty, without looking at the status. Since the orchestrator writes a record on every outcome, including transport failure and backend rejection, a count submitted offline (which fails right away, because the queue serves visits only) makes the pending card show up as resolved. Proof that this is a defect and not design: in the same file, the unsynced-transactions counter filters on !status.isSuccessEquivalent — that is, the status discipline exists and wasn't applied here. An important nuance: the error does not vanish from visit end — the same failed dispatch is then counted by the generic unsynced_transactions category, declared in all three markets; it only changes rows, and the rep loses the link to the retail that was left uncounted. Cheap fix: filter the same predicate in _wasHandledToday. The impact is mitigated by the category being isBlocking: false in both markets that declare it, so nothing was blocked; the damage is informational.Un envío que FALLÓ marca el pendiente del cierre como resuelto. El criterio de "ya contó hoy" es la simple existencia de un registro de despacho del tipo, para ese punto de venta, en esa fecha — dispatches.isNotEmpty, sin mirar el estado. Como el orquestador graba un registro en todos los desenlaces, incluidos fallo de transporte y rechazo del backend, un conteo enviado offline (que falla en el momento, porque la cola solo atiende visitas) hace que la tarjeta de pendiente aparezca como resuelta. Prueba de que es defecto y no diseño: en el mismo archivo, el contador de transacciones no sincronizadas filtra por !status.isSuccessEquivalent — o sea, la disciplina de estado existe y no se aplicó aquí. Un matiz importante: el error no desaparece del cierre — el mismo despacho fallido pasa a contarse por la categoría genérica unsynced_transactions, declarada en los tres mercados; solo cambia de línea, y el rep pierde el vínculo con el punto de venta que faltó contar. Corrección barata: filtrar el mismo predicado en _wasHandledToday. Mitiga el impacto el hecho de que la categoría sea isBlocking: false en los dos mercados que la declaran, así que nada se trababa; el daño es de información.
  • A tela promete recorte por varejo e não faz nenhum. Três sinais dizem ao leitor que a lista é do varejo: o parâmetro accountSfid da rota, o nome do getter accountProducts e — o mais explícito — o texto do estado vazio, "nenhum produto com histórico de vendas para este varejo". Nenhum dos três se realiza: o UseCase filtra apenas eligibility.isAvailableForStockCount, e o getter repete o mesmo filtro. A lista é o catálogo elegível do mercado inteiro, igual para todos os varejos. O accountSfid só é usado para escolher qual bloco de salesHistory alimenta a grade e para preencher o payload. Pior, o encerramento de visita usa o critério que a tela deveria usar — histórico de contagem daquele varejo —, então os dois lados discordam: pode haver pendência sem que a tela mostre nada de específico, e a tela pode listar 121 produtos num varejo para o qual nenhuma pendência existe. Decidir qual das duas semânticas é a correta é pré-requisito para corrigir qualquer uma delas.The screen promises a per-retail slice and performs none. Three signals tell the reader the list belongs to the retail: the route's accountSfid parameter, the getter name accountProducts and — most explicitly — the empty-state copy, "no products with sales history for this retail". None of the three materialises: the UseCase filters only on eligibility.isAvailableForStockCount, and the getter repeats the same filter. The list is the whole market's eligible catalog, identical for every retail. accountSfid is used only to pick which salesHistory block feeds the grid and to populate the payload. Worse, visit end uses the criterion the screen ought to use — count history for that retail — so the two sides disagree: there can be a pending while the screen shows nothing specific, and the screen can list 121 products for a retail that has no pending at all. Deciding which of the two semantics is correct is a prerequisite to fixing either.La pantalla promete recorte por punto de venta y no hace ninguno. Tres señales le dicen al lector que la lista es del punto de venta: el parámetro accountSfid de la ruta, el nombre del getter accountProducts y — el más explícito — el texto del estado vacío, "sin productos con historial de ventas para este punto de venta". Ninguna de las tres se realiza: el UseCase filtra solo eligibility.isAvailableForStockCount, y el getter repite el mismo filtro. La lista es el catálogo elegible del mercado entero, igual para todos los puntos de venta. El accountSfid solo se usa para elegir qué bloque de salesHistory alimenta la grilla y para llenar el payload. Peor, el cierre de visita usa el criterio que la pantalla debería usar — historial de conteo de ese punto de venta —, así que los dos lados discrepan: puede haber pendiente sin que la pantalla muestre nada específico, y la pantalla puede listar 121 productos para un punto de venta que no tiene pendiente alguno. Decidir cuál de las dos semánticas es la correcta es prerrequisito para corregir cualquiera de ellas.
  • Com filtro ativo, a barra de resumo pode dizer "12 de 5". O primeiro número conta todas as contagens digitadas, e o segundo conta só os produtos que passam pelo filtro corrente. Como filtrar não apaga o que já foi digitado (comportamento correto, e necessário para o envio incluir tudo), preencher 12 produtos sem filtro e depois escolher uma categoria com 5 produtos produz exatamente isso. O contador de vazios da mesma barra usa clamp para não ficar negativo — a própria existência desse clamp é a evidência de que a subtração podia dar negativo e que o descasamento é conhecido. Correção: contar preenchidos dentro do conjunto filtrado, ou mostrar o total não filtrado nas duas pontas.With an active filter, the summary bar can read "12 of 5". The first number counts all typed counts, and the second counts only the products passing the current filter. Since filtering doesn't erase what was typed (correct behaviour, and necessary for the submission to include everything), filling 12 products with no filter and then choosing a category with 5 products produces exactly that. The same bar's empty counter uses clamp to avoid going negative — the very existence of that clamp is the evidence that the subtraction could go negative and that the mismatch is known. Fix: count filled within the filtered set, or show the unfiltered total on both sides.Con filtro activo, la barra de resumen puede decir "12 de 5". El primer número cuenta todos los conteos escritos, y el segundo cuenta solo los productos que pasan por el filtro corriente. Como filtrar no borra lo ya escrito (comportamiento correcto, y necesario para que el envío incluya todo), completar 12 productos sin filtro y luego elegir una categoría con 5 productos produce exactamente eso. El contador de vacíos de la misma barra usa clamp para no quedar negativo — la propia existencia de ese clamp es la evidencia de que la resta podía dar negativo y que el descalce se conoce. Corrección: contar llenados dentro del conjunto filtrado, o mostrar el total no filtrado en las dos puntas.
  • O backend ainda não preenche a coluna Baixa. A reinterpretação de Jul/2026 mapeou a linha Baixa da grade de histórico para o campo psq, que é o campo de texto do contrato — e que hoje chega vazio. Na prática, a grade de histórico exibe números na linha Alta e - na linha Baixa em todos os mercados. Isso está previsto e planejado; o registro aqui serve para que ninguém trate a coluna vazia como bug de app. Vale notar o efeito colateral: se o backend passar a mandar psq, ele o mandará como string, e a tela a exibirá crua, sem validar que é numérica.The backend doesn't populate the Low column yet. The Jul/2026 reinterpretation mapped the history grid's Low row to the psq field, which is the contract's text field — and which arrives empty today. In practice, the history grid shows numbers on the High row and - on the Low row in every market. This is expected and planned; recording it here is so nobody treats the empty column as an app bug. Note the side effect: if the backend starts sending psq, it will send it as a string, and the screen will display it raw, without validating that it is numeric.El backend todavía no llena la columna Baja. La reinterpretación de Jul/2026 mapeó la fila Baja de la grilla de historial al campo psq, que es el campo de texto del contrato — y que hoy llega vacío. En la práctica, la grilla de historial muestra números en la fila Alta y - en la fila Baja en todos los mercados. Esto está previsto y planeado; el registro aquí sirve para que nadie trate la columna vacía como bug de app. Vale notar el efecto colateral: si el backend pasa a mandar psq, lo mandará como string, y la pantalla lo mostrará crudo, sin validar que sea numérico.
  • Envio offline não é enfileirado, e o reenvio manual pode duplicar. A fila de reenvio automático do Dispatcher atende um único tipo, o de visita; a contagem vai direto ao transporte, falha e é gravada como erro, sem entrar no flush quando a rede volta. O reenvio manual existe pela Central de dados, mas — ao contrário da verificação de preço, que é um dos 3 tipos lightweight — a contagem não é marcada como segura para reenvio, então repetir carrega risco de duplicidade no backend. A combinação com o item anterior é o pior cenário: offline, a contagem falha, a pendência aparece resolvida, e o reenvio que corrigiria a situação é o que pode duplicar.Offline submission isn't queued, and manual resend may duplicate. The Dispatcher's automatic retry queue serves a single type, the visit one; the count goes straight to transport, fails and is recorded as an error, without entering the flush when the network returns. Manual resend exists through the Data center, but — unlike price check, which is one of the 3 lightweight types — the count is not flagged safe to resend, so repeating carries a duplication risk on the backend. Combining this with the previous item is the worst case: offline, the count fails, the pending shows resolved, and the resend that would fix it is the one that may duplicate.El envío offline no se encola, y el reenvío manual puede duplicar. La cola de reenvío automático del Dispatcher atiende un único tipo, el de visita; el conteo va directo al transporte, falla y se graba como error, sin entrar en el flush cuando la red vuelve. El reenvío manual existe por la Central de datos, pero — a diferencia de la verificación de precio, que es uno de los 3 tipos lightweight — el conteo no está marcado como seguro para reenvío, así que repetir lleva riesgo de duplicidad en el backend. La combinación con el ítem anterior es el peor escenario: offline, el conteo falla, el pendiente aparece resuelto, y el reenvío que corregiría la situación es el que puede duplicar.
  • A Baixa chega ao backend por uma única chave; a Alta, por cinco. No payload, availableQuantity, baseUOMQuatity, defaultUOMQuatity e sellableQuantity carregam todas a contagem Alta, e uom1Quantity a repete; a Baixa só existe em uom2Quantity. Junte-se a isso a divergência de default — as quatro chaves numéricas usam 0 quando não há contagem, e as duas uomNQuantity usam string vazia — e o resultado é que contar apenas a Baixa produz um item que diz availableQuantity: 0, ou seja, um item que afirma não haver estoque enquanto reporta estoque na unidade menor. Dúvida de contrato a confirmar com o backend: qual chave é a fonte de verdade da quantidade, e o que ele espera quando só a unidade Baixa foi contada.The Low reaches the backend through a single key; the High, through five. In the payload, availableQuantity, baseUOMQuatity, defaultUOMQuatity and sellableQuantity all carry the High count, and uom1Quantity repeats it; the Low exists only in uom2Quantity. Add the default divergence — the four numeric keys use 0 when there is no count, and the two uomNQuantity use an empty string — and the result is that counting only the Low produces an item saying availableQuantity: 0, i.e. an item asserting there is no stock while reporting stock in the smaller unit. Contract question to confirm with the backend: which key is the quantity's source of truth, and what it expects when only the Low unit was counted.La Baja llega al backend por una única clave; la Alta, por cinco. En el payload, availableQuantity, baseUOMQuatity, defaultUOMQuatity y sellableQuantity llevan todas el conteo Alta, y uom1Quantity lo repite; la Baja solo existe en uom2Quantity. Súmese a eso la divergencia de default — las cuatro claves numéricas usan 0 cuando no hay conteo, y las dos uomNQuantity usan cadena vacía — y el resultado es que contar solo la Baja produce un ítem que dice availableQuantity: 0, o sea, un ítem que afirma que no hay stock mientras reporta stock en la unidad menor. Duda de contrato a confirmar con el backend: cuál clave es la fuente de verdad de la cantidad, y qué espera cuando solo la unidad Baja fue contada.
  • Só o primeiro lote de fabricação é enviado. O builder usa manufacturingSkus.first para preencher batchId e SKUId. Um produto com vários lotes tem a contagem atribuída ao primeiro que o backend listou, arbitrariamente; um produto sem nenhum SKU de fabricação envia os dois campos como string vazia, silenciosamente. Como a tela não mostra lote nenhum, o representante não tem como saber nem corrigir a atribuição. Dúvida de contrato: confirmar se o backend aceita contagem sem lote e como ele resolve o produto multi-lote.Only the first manufacturing batch is sent. The builder uses manufacturingSkus.first to fill batchId and SKUId. A product with several batches has its count attributed to whichever the backend listed first, arbitrarily; a product with no manufacturing SKU sends both fields as an empty string, silently. Since the screen shows no batch at all, the rep has no way to know or correct the attribution. Contract question: confirm whether the backend accepts a count with no batch and how it resolves a multi-batch product.Solo el primer lote de fabricación se envía. El builder usa manufacturingSkus.first para llenar batchId y SKUId. Un producto con varios lotes tiene su conteo atribuido al primero que el backend listó, arbitrariamente; un producto sin ningún SKU de fabricación envía los dos campos como cadena vacía, silenciosamente. Como la pantalla no muestra ningún lote, el representante no tiene manera de saber ni corregir la atribución. Duda de contrato: confirmar si el backend acepta conteo sin lote y cómo resuelve el producto multi-lote.
  • Sem visita em cache, três campos do payload saem vazios em silêncio. A visita é resolvida no momento do envio, do cache, e o resultado é reduzido a "valor ou nulo" — uma falha de leitura fica indistinguível de "não há visita". Nos dois casos o envio prossegue com visit, sapCustomerId e o nome do varejo vazios, sem aviso ao representante e sem log específico. É um caminho plausível: a tela é alcançável a partir da grade de ferramentas de uma visita cujo cache pode ter sido varrido entre a abertura da tela e o toque em enviar.With no cached visit, three payload fields go out empty and silently. The visit is resolved at submit time, from cache, and the result is reduced to "value or null" — a read failure is indistinguishable from "there is no visit". In both cases the submission proceeds with visit, sapCustomerId and the retail name empty, with no warning to the rep and no specific log. It is a plausible path: the screen is reachable from the tools grid of a visit whose cache may have been swept between opening the screen and tapping submit.Sin visita en caché, tres campos del payload salen vacíos en silencio. La visita se resuelve en el momento del envío, del caché, y el resultado se reduce a "valor o nulo" — un fallo de lectura queda indistinguible de "no hay visita". En los dos casos el envío prosigue con visit, sapCustomerId y el nombre del punto de venta vacíos, sin aviso al representante y sin log específico. Es un camino plausible: la pantalla es alcanzable desde la grilla de herramientas de una visita cuyo caché puede haber sido barrido entre la apertura de la pantalla y el toque en enviar.
  • Texto em inglês fixo na barra de resumo. O primeiro número da barra é montado como "{preenchidos} of {total}" com o " of " escrito à mão em inglês, num app de 6 mercados — então no Brasil a barra diz "3 of 120". Não é falta de infraestrutura: três linhas acima, o mesmo widget usa o template traduzido stock_count_summary_subtitle ("{filled} de {total} produtos contados") para dizer a mesma coisa no cabeçalho expandido. O mesmo defeito existe na verificação de preço, onde passa despercebido porque a tela só roda em inglês; aqui ele está visível em produção em dois mercados.Hardcoded English text in the summary bar. The bar's first number is assembled as "{filled} of {total}" with the " of " hand-written in English, in a 6-market app — so in Brazil the bar reads "3 of 120". It isn't a lack of infrastructure: three lines above, the same widget uses the translated template stock_count_summary_subtitle ("{filled} of {total} products counted") to say the same thing in the expanded header. The same defect exists in price check, where it goes unnoticed because that screen only runs in English; here it is visible in production in two markets.Texto en inglés fijo en la barra de resumen. El primer número de la barra se arma como "{llenados} of {total}" con el " of " escrito a mano en inglés, en una app de 6 mercados — así que en Brasil la barra dice "3 of 120". No es falta de infraestructura: tres líneas arriba, el mismo widget usa el template traducido stock_count_summary_subtitle ("{filled} de {total} productos contados") para decir lo mismo en el encabezado expandido. El mismo defecto existe en la verificación de precio, donde pasa desapercibido porque esa pantalla solo corre en inglés; aquí está visible en producción en dos mercados.
  • Três nomes para a mesma ferramenta, e um deles está errado. O tile brasileiro diz "Falta" — palavra que descreve ruptura de estoque, não contagem — enquanto o cartão de pendência do mesmo mercado diz "Contagem de estoque" e o título da tela diz "Contagem de Estoque". Em espanhol há uma segunda divergência: o cartão de pendência diz "Conteo de inventario" e todo o resto diz "Conteo de stock". Só a África do Sul é consistente ("Stock Count" / "Stock count"). São defeitos de conteúdo, corrigíveis nas traduções, sem toque no código.Three names for the same tool, and one of them is wrong. Brazil's tile says "Falta" — a word describing a stock-out, not a count — while the same market's pending card says "Contagem de estoque" and the screen title says "Contagem de Estoque". In Spanish there is a second divergence: the pending card says "Conteo de inventario" and everything else says "Conteo de stock". Only South Africa is consistent ("Stock Count" / "Stock count"). These are content defects, fixable in the translations, with no code change.Tres nombres para la misma herramienta, y uno de ellos está equivocado. El tile brasileño dice "Falta" — palabra que describe quiebre de stock, no conteo — mientras la tarjeta de pendiente del mismo mercado dice "Contagem de estoque" y el título de la pantalla dice "Contagem de Estoque". En español hay una segunda divergencia: la tarjeta de pendiente dice "Conteo de inventario" y todo el resto dice "Conteo de stock". Solo Sudáfrica es consistente ("Stock Count" / "Stock count"). Son defectos de contenido, corregibles en las traducciones, sin tocar el código.
  • Dois métodos públicos do Notifier sem chamador, e uma flag inerte. clearFilters() ficou órfão quando o botão "Limpar Filtro" saiu da tela — e é a única das cinco implementações de clearFilters() do app sem um chamador, o que reforça que é resíduo e não desenho. dismissSuccessNotice() também não tem chamador, e como é o único leitor de submitSucceeded em todo o app, o par método+flag é inerte: a flag é escrita em três lugares e nunca consultada por ninguém que renderize algo. A limpeza natural é remover os dois métodos e a flag, ou dar-lhes uso.Two public Notifier methods with no caller, and one inert flag. clearFilters() was orphaned when the "Clear Filter" button left the screen — and it is the only one of the app's five clearFilters() implementations with no caller, which reinforces that it is residue and not design. dismissSuccessNotice() has no caller either, and since it is submitSucceeded's only reader in the whole app, the method+flag pair is inert: the flag is written in three places and never consulted by anyone who renders anything. The natural cleanup is to remove both methods and the flag, or give them a use.Dos métodos públicos del Notifier sin llamador, y una flag inerte. clearFilters() quedó huérfano cuando el botón "Limpiar Filtro" salió de la pantalla — y es la única de las cinco implementaciones de clearFilters() de la app sin llamador, lo que refuerza que es residuo y no diseño. dismissSuccessNotice() tampoco tiene llamador, y como es el único lector de submitSucceeded en toda la app, el par método+flag es inerte: la flag se escribe en tres lugares y nunca la consulta nadie que renderice algo. La limpieza natural es remover los dos métodos y la flag, o darles uso.
  • Disciplina de State pós-await não é uniforme dentro do submit(). O método relê o State depois do envio — e é justamente por isso que a contagem não perde o que foi digitado durante a chamada, ao contrário da feature irmã. Mas o ramo de representante nulo escreve sobre o snapshot capturado antes de dois await, e o payload também é montado a partir desse snapshot: quantidades digitadas entre o toque em enviar e a resolução do representante/visita não entram no envelope (embora sobrevivam na tela). Corrigir é mover a captura para depois dos dois await.Post-await State discipline isn't uniform inside submit(). The method re-reads the State after the submission — and that is precisely why the count doesn't lose what was typed during the call, unlike its sibling feature. But the null-rep branch writes over the snapshot captured before two awaits, and the payload is assembled from that snapshot too: quantities typed between tapping submit and resolving the rep/visit don't make it into the envelope (though they survive on screen). The fix is to move the capture after both awaits.La disciplina de State post-await no es uniforme dentro del submit(). El método relee el State después del envío — y es justamente por eso que el conteo no pierde lo escrito durante la llamada, a diferencia de la feature hermana. Pero la rama de representante nulo escribe sobre el snapshot capturado antes de dos await, y el payload también se arma a partir de ese snapshot: cantidades escritas entre el toque en enviar y la resolución del representante/visita no entran en el envelope (aunque sobrevivan en la pantalla). Corregir es mover la captura para después de los dos await.
  • Filtro de elegibilidade aplicado duas vezes, e memoização que quase nunca acerta. O UseCase já filtra por isAvailableForStockCount e o getter accountProducts repete o mesmo predicado sobre a lista já filtrada — trabalho redundante em cada leitura. A memoização desses getters usa Expando, cache chaveado por identidade de objeto: como o State é Freezed e cada tecla digitada gera um copyWith, praticamente todo acesso após uma digitação é um miss. Enquanto isso, os quatro contadores e somatórios que o rodapé lê várias vezes por rebuild não são memoizados. A otimização está no lugar errado.Eligibility filter applied twice, and memoization that almost never hits. The UseCase already filters on isAvailableForStockCount and the accountProducts getter repeats the same predicate over the already-filtered list — redundant work on every read. Memoization of those getters uses Expando, a cache keyed by object identity: since the State is Freezed and every keystroke produces a copyWith, virtually every access after typing is a miss. Meanwhile the four counters and sums the footer reads several times per rebuild are not memoized. The optimisation is in the wrong place.Filtro de elegibilidad aplicado dos veces, y memoización que casi nunca acierta. El UseCase ya filtra por isAvailableForStockCount y el getter accountProducts repite el mismo predicado sobre la lista ya filtrada — trabajo redundante en cada lectura. La memoización de esos getters usa Expando, caché indexado por identidad de objeto: como el State es Freezed y cada tecla escrita genera un copyWith, prácticamente todo acceso tras una escritura es un miss. Mientras tanto, los cuatro contadores y sumatorios que el pie lee varias veces por rebuild no están memoizados. La optimización está en el lugar equivocado.
  • Resíduos menores. Quatro chaves de tradução stock_count_* sem consumidor, presentes nos 6 mercados: stock_count_stock e stock_count_sale (as colunas antigas "Estoque"/"Venda"), stock_count_clear_filter e stock_count_summary_stat_units · o painel de resumo reutiliza 3 chaves de price_check_* em vez de ter as suas · hasCountStock atravessa proto, DTO, Model e Entity e não tem nenhum leitor · conversionFactor do ProductUom é carregado e ignorado, embora a tela mostre duas unidades · a paginação está ligada duas vezes (a rolagem da página e o detector interno da lista infinita apontam para o mesmo método; só a primeira dispara, porque a lista é shrinkWrap sem física de rolagem) · filledProductsByCategory itera products em vez de accountProducts — hoje inócuo, porque o UseCase já filtrou a elegibilidade, e a única inconsistência de estilo entre os getters · a linha de produto não recebe key no itemBuilder: o estado do cartão (dois controllers e dois focus nodes) é reusado por índice e o sync do didUpdateWidget é bloqueado quando o campo está em foco, então um refresh que reordene o catálogo poderia deixar um campo em foco com o valor do produto anterior · o refresh() preserva as entradas, mas o catálogo pode mudar: uma entrada cujo produto saiu continua contando no total e é silenciosamente descartada no envio, porque o builder itera os produtos · os campos de contagem não têm limite de dígitos: um número acima de 64 bits volta como "não contado" enquanto o campo continua exibindo os dígitos · o README dos mocks declara "*" como convenção de "não aplicável" para a Baixa, mas a grade só trata string vazia como placeholder, então um "*" do backend apareceria literalmente · o contador do cartão de pendência é sempre 1, nunca o número de produtos a contar · e o primaryActionKey do tipo de pendência ("Contar estoque") está traduzido nos 6 mercados e não tem consumidor.Minor residue. Four stock_count_* translation keys with no consumer, present in all 6 markets: stock_count_stock and stock_count_sale (the old "Stock"/"Sale" columns), stock_count_clear_filter and stock_count_summary_stat_units · the summary panel reuses 3 price_check_* keys instead of having its own · hasCountStock crosses proto, DTO, Model and Entity and has no reader · ProductUom's conversionFactor is loaded and ignored, even though the screen shows two units · pagination is wired twice (the page's scroll and the infinite list's internal detector point at the same method; only the first fires, because the list is shrinkWrap with no scroll physics) · filledProductsByCategory iterates products instead of accountProducts — harmless today, because the UseCase already filtered eligibility, and the only style inconsistency among the getters · the product row gets no key in the itemBuilder: the card state (two controllers and two focus nodes) is reused by index and the didUpdateWidget sync is blocked while the field has focus, so a refresh that reordered the catalog could leave a focused field showing the previous product's value · refresh() preserves the entries, but the catalog may change: an entry whose product is gone keeps counting in the total and is silently dropped on submit, because the builder iterates the products · the count fields have no digit limit: a number above 64 bits comes back as "not counted" while the field keeps showing the digits · the mocks README declares "*" as the "not applicable" convention for the Low unit, but the grid only treats an empty string as the placeholder, so a "*" from the backend would render literally · the pending card's counter is always 1, never the number of products to count · and the pending type's primaryActionKey ("Count stock") is translated in all 6 markets and has no consumer.Residuos menores. Cuatro claves de traducción stock_count_* sin consumidor, presentes en los 6 mercados: stock_count_stock y stock_count_sale (las columnas antiguas "Stock"/"Venta"), stock_count_clear_filter y stock_count_summary_stat_units · el panel de resumen reutiliza 3 claves de price_check_* en vez de tener las suyas · hasCountStock atraviesa proto, DTO, Model y Entity y no tiene ningún lector · el conversionFactor del ProductUom se carga y se ignora, aunque la pantalla muestre dos unidades · la paginación está conectada dos veces (el desplazamiento de la página y el detector interno de la lista infinita apuntan al mismo método; solo el primero dispara, porque la lista es shrinkWrap sin física de desplazamiento) · filledProductsByCategory itera products en vez de accountProducts — hoy inocuo, porque el UseCase ya filtró la elegibilidad, y la única inconsistencia de estilo entre los getters · la fila de producto no recibe key en el itemBuilder: el estado de la tarjeta (dos controllers y dos focus nodes) se reusa por índice y el sync del didUpdateWidget se bloquea cuando el campo está enfocado, así que un refresh que reordenara el catálogo podría dejar un campo enfocado con el valor del producto anterior · el refresh() preserva las entradas, pero el catálogo puede cambiar: una entrada cuyo producto salió sigue contando en el total y es silenciosamente descartada en el envío, porque el builder itera los productos · los campos de conteo no tienen límite de dígitos: un número por encima de 64 bits vuelve como "no contado" mientras el campo sigue mostrando los dígitos · el README de los mocks declara "*" como convención de "no aplicable" para la Baja, pero la grilla solo trata la cadena vacía como placeholder, así que un "*" del backend aparecería literalmente · el contador de la tarjeta de pendiente es siempre 1, nunca el número de productos a contar · y el primaryActionKey del tipo de pendiente ("Contar estoque") está traducido en los 6 mercados y no tiene consumidor.
  • Sincronização incremental do catálogo não implementada, e documentação de mock defasada. dateReference e lastModifiedDate existem como optional no request do proto; o primeiro é parâmetro do datasource e o único caller nunca o passa, e o segundo não é atribuído em camada nenhuma. Todo pull-to-refresh traz o catálogo inteiro do mercado — o maior dump real tem 745 KB — e a gravação limpa e regrava as 11 boxes. Separadamente, os README.md dos mocks contêm três afirmações falsas relevantes: citam arquivos ro_* de um mercado que não existe no app; afirmam que salesHistory é "exclusivo de BR e CL" e não populado em ZA, quando os dois arquivos de ZA o têm e o dump real de CL não; e dizem que falta o cl_real_stock_control.json, que existe. Corrigir a documentação dos mocks evita conclusões erradas em análises futuras.Incremental catalog sync not implemented, and stale mock documentation. dateReference and lastModifiedDate exist as optional on the proto request; the first is a datasource parameter and the only caller never passes it, and the second isn't assigned in any layer. Every pull-to-refresh brings the market's whole catalog — the largest real dump is 745 KB — and the write clears and rewrites all 11 boxes. Separately, the mocks' README.md files contain three relevant false statements: they cite ro_* files for a market that doesn't exist in the app; they claim salesHistory is "exclusive to BR and CL" and not populated in ZA, when both ZA files have it and CL's real dump does not; and they say cl_real_stock_control.json is missing, when it exists. Fixing the mock documentation avoids wrong conclusions in future analyses.Sincronización incremental del catálogo no implementada, y documentación de mock desactualizada. dateReference y lastModifiedDate existen como optional en el request del proto; el primero es parámetro del datasource y el único caller nunca lo pasa, y el segundo no se asigna en ninguna capa. Todo pull-to-refresh trae el catálogo entero del mercado — el mayor dump real tiene 745 KB — y la grabación limpia y regraba las 11 boxes. Por separado, los README.md de los mocks contienen tres afirmaciones falsas relevantes: citan archivos ro_* de un mercado que no existe en la app; afirman que salesHistory es "exclusivo de BR y CL" y no poblado en ZA, cuando los dos archivos de ZA lo tienen y el dump real de CL no; y dicen que falta el cl_real_stock_control.json, que existe. Corregir la documentación de los mocks evita conclusiones equivocadas en análisis futuros.

Onde continuar lendoWhere to read nextDónde seguir leyendo A transação do envio, campo a campo, está em 26 · LocationStockUpload. O atalho que abre esta tela e a visita que lhe dá contexto vivem em Detalhe da visita, cuja lista está em Visitas. A feature irmã — mesma anatomia de tela, dois campos por linha, barra de resumo e envio pelo Dispatcher — é a Verificação de preço, e vale ler as duas em paralelo: as divergências entre elas (paginação, filtros, estado vazio, reenvio seguro, disciplina de State no envio) estão apontadas ao longo deste documento. O mesmo catálogo de produtos, com o preço da BAT resolvido pelo grupo de preço do varejo, alimenta a criação de pedido e as promoções; a comparação entre preço de compra e de revenda é a Calculadora de margem. O outro estoque — o saldo da van, que esta feature não toca — é descrito em Prompt. O merge de catálogo de uma visita ad hoc é descrito em Varejos, e o registro do despacho — inclusive o reenvio manual de uma contagem que falhou — aparece na Central de dados. A tela de encerramento de visita, que é o segundo caminho de acesso, ainda não tem documento próprio. The submission transaction, field by field, is in 26 · LocationStockUpload. The shortcut that opens this screen and the visit giving it context live in Visit detail, whose list is in Visits. The sibling feature — same screen anatomy, two fields per row, summary bar and Dispatcher submission — is Price check, and the two are worth reading side by side: the divergences between them (pagination, filters, empty state, safe resend, State discipline on submit) are flagged throughout this document. The same product catalog, with the BAT price resolved through the retail's pricing group, feeds order creation and promotions; comparing purchase against resale price is the Margin calculator. The other stock — the van balance, which this feature never touches — is described in Prompt. The ad hoc visit catalog merge is described in Retails, and the dispatch record — including manually resending a count that failed — shows up in the Data center. The visit-end screen, which is the second entry path, doesn't have a document of its own yet. La transacción del envío, campo a campo, está en 26 · LocationStockUpload. El atajo que abre esta pantalla y la visita que le da contexto viven en Detalle de la visita, cuya lista está en Visitas. La feature hermana — misma anatomía de pantalla, dos campos por fila, barra de resumen y envío por el Dispatcher — es la Verificación de precio, y vale leer las dos en paralelo: las divergencias entre ellas (paginación, filtros, estado vacío, reenvío seguro, disciplina de State en el envío) están señaladas a lo largo de este documento. El mismo catálogo de productos, con el precio de BAT resuelto por el grupo de precio del punto de venta, alimenta la creación de pedido y las promociones; la comparación entre precio de compra y de reventa es la Calculadora de margen. El otro stock — el saldo de la van, que esta feature no toca — se describe en Prompt. El merge de catálogo de una visita ad hoc se describe en Puntos de venta, y el registro del despacho — incluido el reenvío manual de un conteo que falló — aparece en la Central de datos. La pantalla de cierre de visita, que es el segundo camino de acceso, todavía no tiene documento propio.