DocumentaçãoDocumentationDocumentaciónOne Conecta
ÍndiceIndexÍndice
Baixar .mdDownload .mdBajar .md
Feature · Calculadora de margemFeature · Margin calculatorFeature · Calculadora de margen

Calculadora de margemMargin calculatorCalculadora de margen

Uma ferramenta dentro da visita: o representante escolhe um produto, informa um preço de venda e uma quantidade, e a tela calcula na hora — tudo local, sem enviar nada — o lucro "bankable" e a margem do varejo, por unidade (stick) e por pacote (pack). Os preços de custo/RRP/RRSP vêm sincronizados do backend. An in-visit tool: the sales rep picks a product, types a selling price and a quantity, and the screen computes on the spot — all local, nothing is sent — the retailer's bankable profit and margin, per stick and per pack. Cost/RRP/RRSP prices are synced from the backend. Una herramienta dentro de la visita: el representante elige un producto, indica un precio de venta y una cantidad, y la pantalla calcula al instante — todo local, sin enviar nada — el lucro "bankable" y el margen del punto de venta, por unidad (stick) y por paquete (pack). Los precios de costo/RRP/RRSP vienen sincronizados del backend.

PúblicoAudiencePúblico
Representante · QA · Suporte · DevRep · QA · Support · DevRepresentante · QA · Soporte · Dev
Onde ficaWhereDónde
Detalhe da visita → Ferramentas → Calculadora de margemVisit detail → Tools → Margin calculatorDetalle de la visita → Herramientas → Calculadora de margen
EscritaWriteEscritura
Nenhuma (cálculo local)None (local compute)Ninguna (cálculo local)
AtualizadoUpdatedActualizado
22/07/20262026-07-22
Disponível emAvailable inDisponible en ZA
01

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

A Calculadora de margem é uma ferramenta que o representante de vendas abre de dentro de uma visita para mostrar ao dono do varejo quanto ele ganha vendendo um produto. Escolhido o produto, ela responde três perguntas: The Margin calculator is a tool the sales rep opens from inside a visit to show the retailer how much they earn selling a product. Once a product is picked, it answers three questions: La Calculadora de margen es una herramienta que el representante de ventas abre desde dentro de una visita para mostrarle al punto de venta cuánto gana vendiendo un producto. Elegido el producto, responde tres preguntas:

Quanto sobra por unidade?How much profit per unit?¿Cuánto queda por unidad?

O lucro "bankable" = preço de venda − custo do varejo, por stick e por pack.Bankable profit = selling price − retailer cost, per stick and per pack.El lucro "bankable" = precio de venta − costo del punto de venta, por stick y por pack.

Quanto no total?How much in total?¿Cuánto en total?

Multiplica pela quantidade digitada e soma stick + pack no bloco Totais combinados.Multiplies by the typed quantity and sums stick + pack in the Combined totals block.Multiplica por la cantidad digitada y suma stick + pack en el bloque Totales combinados.

Que margem %?What margin %?¿Qué margen %?

A incidência mostra a margem percentual sobre o custo, no preço recomendado (RRSP) e no preço real.The incidence shows the percentage margin over cost, at recommended (RRSP) and actual price.La incidencia muestra el margen porcentual sobre el costo, al precio recomendado (RRSP) y al precio real.

Só cálculo localLocal compute onlySolo cálculo local A tela não envia nada ao backend. Ela lê o catálogo de preços (sincronizado antes) e faz toda a matemática no aparelho, ao vivo, enquanto você digita. Não há "salvar" nem transação. The screen sends nothing to the backend. It reads the price catalog (synced beforehand) and does all the math on-device, live, as you type. There's no "save" and no transaction. La pantalla no envía nada al backend. Lee el catálogo de precios (sincronizado antes) y hace toda la matemática en el dispositivo, en vivo, mientras escribe. No hay "guardar" ni transacción.

02

Como acessarHow to openCómo acceder

  1. Abra uma visitaOpen a visitAbra una visitaA calculadora vive no detalhe da visita, na grade de Ferramentas. Ela precisa da visita para saber de qual varejo são os preços.The calculator lives in the visit detail, in the Tools grid. It needs the visit to know which retailer's prices to use.La calculadora vive en el detalle de la visita, en la grilla de Herramientas. Necesita la visita para saber de qué punto de venta son los precios.
  2. Toque em "Calculadora de margem"Tap "Margin calculator"Toque "Calculadora de margen"O bloco aparece só onde o mercado o habilita (hoje, África do Sul).The tile shows only where the market enables it (today, South Africa).El bloque aparece solo donde el mercado lo habilita (hoy, Sudáfrica).
  3. A tela abreThe screen opensLa pantalla abreJá com o primeiro produto do varejo selecionado e a aba Stick ativa. Puxe para baixo para re-sincronizar os preços.Already with the retailer's first product selected and the Stick tab active. Pull down to re-sync prices.Ya con el primer producto del punto de venta seleccionado y la pestaña Stick activa. Deslice hacia abajo para re-sincronizar los precios.
03

Estrutura da telaScreen structureEstructura de la pantalla

CabeçalhoHeaderEncabezado
Ícone + título "Calculadora de margem", com a data da última sincronização dos preços acima.Icon + "Margin calculator" title, with the prices' last sync date above.Ícono + título "Calculadora de margen", con la fecha de última sincronización de los precios arriba.
Seletor de produtoProduct pickerSelector de producto
Dropdown com os produtos daquele varejo. Trocar de produto zera os campos digitados.A dropdown with that retailer's products. Switching product clears the typed inputs.Un dropdown con los productos de ese punto de venta. Cambiar de producto borra los campos digitados.
Abas Stick / PackStick / Pack tabsPestañas Stick / Pack
Alternam o cálculo entre unidade (stick, azul) e pacote (pack, verde). Cada aba guarda seus próprios valores digitados.Switch the calc between unit (stick, blue) and pack (green). Each tab keeps its own typed values.Alternan el cálculo entre unidad (stick, azul) y paquete (pack, verde). Cada pestaña guarda sus propios valores digitados.
Valores de entradaInput valuesValores de entrada
Custo do varejo e preço recomendado (só leitura) + dois campos editáveis: preço de venda e quantidade em packs.Retailer cost and recommended price (read-only) + two editable fields: selling price and quantity in packs.Costo del punto de venta y precio recomendado (solo lectura) + dos campos editables: precio de venta y cantidad en packs.
BankableBankableBankable
O resultado: lucro por unidade e total, em duas colunas — RRSP (preço recomendado) e Actual (preço real). Na aba Stick, mostra dois avisos: a conversão pack→stick e a quantidade de sticks.The result: profit per unit and total, in two columns — RRSP (recommended) and Actual (real). On the Stick tab it shows two notices: the pack→stick conversion and the stick count.El resultado: lucro por unidad y total, en dos columnas — RRSP (recomendado) y Actual (real). En la pestaña Stick muestra dos avisos: la conversión pack→stick y la cantidad de sticks.
Totais combinadosCombined totalsTotales combinados
Card de cabeçalho roxo somando stick + pack (RRSP e Actual).A purple-header card summing stick + pack (RRSP and Actual).Un card de encabezado morado sumando stick + pack (RRSP y Actual).
IncidênciaIncidenceIncidencia
Card de cabeçalho laranja com a margem percentual (RRSP e Actual).An orange-header card with the percentage margin (RRSP and Actual).Un card de encabezado naranja con el margen porcentual (RRSP y Actual).
04

Como o cálculo funcionaHow the calc worksCómo funciona el cálculo

Cada produto traz seis preços do backend: custo do varejo, RRP (preço de venda recomendado) e RRSP (preço de venda recomendado ao consumidor), cada um por stick e por pack. A partir deles e do que você digita, a tela calcula três coisas:Each product brings six prices from the backend: retailer cost, RRP (recommended retail price) and RRSP (recommended retail selling price), each per stick and per pack. From these plus what you type, the screen computes three things:Cada producto trae seis precios del backend: costo del punto de venta, RRP (precio de venta recomendado) y RRSP (precio de venta recomendado al consumidor), cada uno por stick y por pack. A partir de ellos y de lo que digita, la pantalla calcula tres cosas:

  1. Lucro por unidadeProfit per unitLucro por unidadpreço de venda − custo do varejo. Na coluna RRSP usa o preço recomendado; na Actual, o preço que você digitou (ou o RRP como padrão, se em branco).selling price − retailer cost. The RRSP column uses the recommended price; Actual uses the price you typed (or RRP as default, if blank).precio de venta − costo del punto de venta. La columna RRSP usa el precio recomendado; Actual, el precio que digitó (o el RRP por defecto, si está vacío).
  2. TotalTotalTotallucro por unidade × quantidade. Como você informa a quantidade em packs, na aba Stick ela é multiplicada por sticks por pack para virar sticks.profit per unit × quantity. Since you enter quantity in packs, on the Stick tab it's multiplied by sticks per pack to become sticks.lucro por unidad × cantidad. Como informa la cantidad en packs, en la pestaña Stick se multiplica por sticks por pack para volverse sticks.
  3. Margem %Margin %Margen %lucro total combinado ÷ custo total combinado × 100. Se o custo total é zero, a margem é 0%.combined total profit ÷ combined total cost × 100. If total cost is zero, the margin is 0%.lucro total combinado ÷ costo total combinado × 100. Si el costo total es cero, el margen es 0%.

Preço abaixo do recomendadoBelow recommendedDebajo del recomendado Na aba Stick, se o preço de venda digitado ficar abaixo do RRSP, o valor Actual aparece em vermelho — um alerta visual de que a margem está sendo espremida. On the Stick tab, if the typed selling price is below RRSP, the Actual value shows in red — a visual alert that the margin is being squeezed. En la pestaña Stick, si el precio de venta digitado queda debajo del RRSP, el valor Actual aparece en rojo — una alerta visual de que el margen se está apretando.

05

AçõesActionsAcciones

Escolher produtoPick a productElegir producto

O dropdown abre um seletor com os produtos daquele varejo. Ao trocar, os dois campos digitados (preço e quantidade) são limpos, em ambas as abas.The dropdown opens a picker with that retailer's products. On change, the two typed fields (price and quantity) are cleared, in both tabs.El dropdown abre un selector con los productos de ese punto de venta. Al cambiar, los dos campos digitados (precio y cantidad) se limpian, en ambas pestañas.

Alternar Stick / PackToggle Stick / PackAlternar Stick / Pack

Muda a base do cálculo. Os valores digitados de cada aba são independentes e preservados ao alternar.Changes the calc basis. Each tab's typed values are independent and preserved when you switch.Cambia la base del cálculo. Los valores digitados de cada pestaña son independientes y se preservan al alternar.

Digitar preço e quantidadeType price and quantityDigitar precio y cantidad

Teclado numérico com decimais. Todo dígito recalcula os resultados na hora. Preço em branco usa o RRP como padrão; quantidade em branco vale 0.Numeric keyboard with decimals. Every keystroke recomputes results instantly. A blank price falls back to RRP; a blank quantity counts as 0.Teclado numérico con decimales. Cada dígito recalcula los resultados al instante. Precio en blanco usa el RRP por defecto; cantidad en blanco vale 0.

Atualizar (pull-to-refresh)Refresh (pull-to-refresh)Actualizar (pull-to-refresh)

Puxar para baixo re-sincroniza os preços do backend, mantendo o produto e a aba atuais.Pulling down re-syncs the prices from the backend, keeping the current product and tab.Deslizar hacia abajo re-sincroniza los precios del backend, manteniendo el producto y la pestaña actuales.

06

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

Clean Architecture + Riverpod + Freezed + gRPC + ObjectBox. É uma feature somente leitura + cálculo local: um único RPC (getMarginCalculator) traz o catálogo de preços por produto; toda a matemática (lucro, totais, margem) acontece em getters do State, no aparelho. O dado atravessa quatro representações, ligadas por mappers, com cache write-through:Clean Architecture + Riverpod + Freezed + gRPC + ObjectBox. It's a read-only + local-compute feature: a single RPC (getMarginCalculator) returns the per-product price catalog; all math (profit, totals, margin) happens in State getters, on-device. Data crosses four representations, linked by mappers, with cache write-through:Clean Architecture + Riverpod + Freezed + gRPC + ObjectBox. Es una feature solo lectura + cálculo local: un único RPC (getMarginCalculator) trae el catálogo de precios por producto; toda la matemática (lucro, totales, margen) ocurre en getters del State, en el dispositivo. El dato atraviesa cuatro representaciones, unidas por mappers, con cache write-through:

  • MarginCalculatorReplygRPC proto
    • toDTOMarginCalculatorDTODTO · Freezed
      • toEntityMarginCalculatorEntitydomain
        • toModelMarginCalculatorModelObjectBox
          • toEntityMarginCalculatorEntitydomain · cache
            • filter by accountMarginCalculatorNotifier + State
              • getters → UIMarginCalculatorPage
07

Modelo de dadosData modelModelo de datos

O mesmo catálogo 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. Aqui os campos são todos primitivos (String, int, double, List<String>): não há um único enum tipado na Entity, ao contrário da maioria das features. Muda pouquíssimo entre camadas — um rename no proto, a relação vira ToMany no Model, e o lastSyncAt é gerado no mapper.The same catalog exists in four near-identical representations across layers — Proto (gRPC wire) → DTO (Freezed) → Model (ObjectBox) → Entity (domain) — each boundary crossed by a mapper. Here every field is primitive (String, int, double, List<String>): there isn't a single typed enum in the Entity, unlike most features. Very little changes between layers — one proto rename, the relation becomes ToMany in the Model, and lastSyncAt is generated in the mapper.El mismo catálogo existe en cuatro representaciones casi idénticas a lo largo de las capas — Proto (wire gRPC) → DTO (Freezed) → Model (ObjectBox) → Entity (dominio) — cada frontera cruzada por un mapper. Aquí cada campo es primitivo (String, int, double, List<String>): no hay un solo enum tipado en la Entity, a diferencia de la mayoría de las features. Cambia poquísimo entre capas — un rename en el proto, la relación pasa a ToMany en el Model, y el lastSyncAt se genera en el mapper.

A lista chega num container MarginCalculator (lastSyncAt gerado no mapper + products[]); cada item é um MarginCalculatorProduct de 10 campos — nome, sticks por pack e os seis preços (custo, RRP, RRSP × stick/pack) + a lista de contas às quais o produto pertence. A seguir, na ordem: o proto, as estruturas de dados campo-a-campo por camada, e os mappers.The list arrives in a MarginCalculator container (lastSyncAt generated in the mapper + products[]); each item is a 10-field MarginCalculatorProduct — name, sticks per pack and the six prices (cost, RRP, RRSP × stick/pack) + the list of accounts the product belongs to. Next, in order: the proto, the field-by-field data structures per layer, and the mappers.La lista llega en un container MarginCalculator (lastSyncAt generado en el mapper + products[]); cada ítem es un MarginCalculatorProduct de 10 campos — nombre, sticks por pack y los seis precios (costo, RRP, RRSP × stick/pack) + la lista de cuentas a las que pertenece el producto. A continuación, en orden: el proto, las estructuras de datos campo a campo por capa, y los mappers.

Proto

MarginCalculatorConectaRep.proto · proto3 · package mn.bat.conectarep.streambridge. Um serviço (MarginCalculatorConectaRepService), um método unário:One service (MarginCalculatorConectaRepService), a single unary method:Un servicio (MarginCalculatorConectaRepService), un método unario:

getMarginCalculatorunary
MétodoMethodMétodo

rpc getMarginCalculator(MarginCalculatorRequest) returns (MarginCalculatorReply)

path /mn.bat.conectarep.streambridge.MarginCalculatorConectaRepService/getMarginCalculator

Request · MarginCalculatorRequest
locationHierarchySfid
string · #1 · hierarquia do representante de vendas (resolvida no repository)sales rep hierarchy (resolved in the repository)jerarquía del representante de ventas (resuelta en el repository)
lastModifiedDate
string · #2 · optional
Reply · MarginCalculatorReply

repeated MarginCalculatorProduct productso catálogo. Os 10 campos de MarginCalculatorProduct estão detalhados nas Estruturas de dados abaixo.the catalog. MarginCalculatorProduct's 10 fields are detailed in Data structures below.el catálogo. Los 10 campos de MarginCalculatorProduct 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 accent marca onde o tipo (ou o nome) primeiro diverge lendo Proto→Entity.One dropdown per structure, nested by hierarchy. Each table has one column per layer — Proto · DTO · Model · Entity; the accent text marks where the type (or name) first diverges reading Proto→Entity.Un dropdown por estructura, anidados por jerarquía. Cada tabla tiene una columna por capa — Proto · DTO · Model · Entity; el texto accent marca dónde el tipo (o el nombre) primero diverge leyendo Proto→Entity.

  • MarginCalculator raiz 2 campos
    CampoProtoDTOModelEntity
    lastSyncAtDateTimeDateTimeDateTime
    productsrepeated MarginCalculatorProductList<…DTO>ToMany<…Model>List<…Entity>
    • MarginCalculatorProduct MarginCalculator.products[] 10 campos
      CampoProtoDTOModelEntity
      sfidstringStringString @UniqueString
      accountSfidsaccountSfid repeated stringList<String>List<String>List<String>
      namestringStringStringString
      sticksPerPackint32intintint
      retailerCostPerStickdoubledoubledoubledouble
      retailerCostPerPackdoubledoubledoubledouble
      rrpPerStickdoubledoubledoubledouble
      rrpPerPackdoubledoubledoubledouble
      rrspPerStickdoubledoubledoubledouble
      rrspPerPackdoubledoubledoubledouble

Mappers

As conversões entre camadas, todas como extension (5 direções por tipo). Aqui o método DTO→Entity e Model→Entity chama-se toEntity() (não toDomain()):The conversions between layers, all as extensions (5 directions per type). Here the DTO→Entity and Model→Entity method is named toEntity() (not toDomain()):Las conversiones entre capas, todas como extension (5 direcciones por tipo). Aquí el método DTO→Entity y Model→Entity se llama toEntity() (no toDomain()):

DireçãoDirectionDirecciónMétodoMethodMétodo
JSON → DTOstatic fromMap(Map) (accountSfid aceita String ou List; lastSyncAt = DateTimeUtils.now())(accountSfid accepts String or List; lastSyncAt = DateTimeUtils.now())(accountSfid acepta String o List; lastSyncAt = DateTimeUtils.now())
Proto → DTOtoDTO() (no MarginCalculatorReply; accountSfidaccountSfids; lastSyncAt = DateTimeUtils.now())(on MarginCalculatorReply; accountSfidaccountSfids; lastSyncAt = DateTimeUtils.now())(en MarginCalculatorReply; accountSfidaccountSfids; lastSyncAt = DateTimeUtils.now())
DTO → EntitytoEntity()
Entity → ModeltoModel() (popula o ToMany de products)(fills the products ToMany)(llena el ToMany de products)
Model → EntitytoEntity()

Os únicos deltasThe only deltasLos únicos deltas

  • rename accountSfidaccountSfids (no Proto)rename accountSfidaccountSfids (in the Proto)rename accountSfidaccountSfids (en el Proto)
  • a relação products vira ToMany no Modelthe products relation becomes ToMany in the Modella relación products pasa a ToMany en el Model
  • lastSyncAt gerado no mapper com DateTimeUtils.now() (o proto não carrega timestamp) — CLAUDE §21generated in the mapper with DateTimeUtils.now() (the proto carries no timestamp) — CLAUDE §21generado en el mapper con DateTimeUtils.now() (el proto no lleva timestamp) — CLAUDE §21
  • nenhum enum tipado — todos os campos permanecem primitivos nas quatro camadasno typed enum — every field stays primitive across all four layersningún enum tipado — cada campo permanece primitivo en las cuatro capas
08

Repository

MarginCalculatorRepositoryImpl implementaimplementsimplementa MarginCalculatorRepositoryInterface e injeta os 3 datasources (mock/local/remote) + ConnectivityService + a flag useMock + Ref. Método a método:and injects the 3 datasources (mock/local/remote) + ConnectivityService + the useMock flag + Ref. Method by method:e inyecta los 3 datasources (mock/local/remote) + ConnectivityService + la flag useMock + Ref. Método a método:

getMarginCalculator({source = local}) mock / local / remote

RetornaReturnsDevuelve Result<MarginCalculatorEntity, Failure>

Ponto de entrada do catálogo: decide a fonte pela source + flags, mapeia e grava no cache (write-through).Catalog entry point: picks the source from source + flags, maps and writes to cache (write-through).Punto de entrada del catálogo: elige la fuente por source + flags, mapea y graba en caché (write-through).

Árvore de decisão de fonteSource decision treeÁrbol de decisión de fuente

  1. _useMock == true ouoro source == mock_fetchFromMock(): lê o mock, mapeia, grava no cache._fetchFromMock(): reads the mock, maps, writes to cache._fetchFromMock(): lee el mock, mapea, graba en caché.
  2. source == local ou offlineor offlineu offline_fetchFromCacheOrFail(): só cache; vazio → Error(NetworkFailure)._fetchFromCacheOrFail(): cache only; empty → Error(NetworkFailure)._fetchFromCacheOrFail(): solo caché; vacío → Error(NetworkFailure).
  3. senão (remoto + conectado)otherwise (remote + connected)si no (remoto + conectado)_fetchFromRemoteWithFallback(): lê currentResourceProvider; se null cai pro cache; senão chama o remoto com resource.locationHierarchyId, mapeia, grava; em erro, fallback pro cache._fetchFromRemoteWithFallback(): reads currentResourceProvider; if null falls back to cache; else calls remote with resource.locationHierarchyId, maps, writes; on error, falls back to cache._fetchFromRemoteWithFallback(): lee currentResourceProvider; si null cae al caché; si no llama al remoto con resource.locationHierarchyId, mapea, graba; en error, fallback al caché.
getCachedMarginCalculator() local

RetornaReturnsDevuelve Result<MarginCalculatorEntity?, Failure>

Só cache. null vira Success(null), não erro.Cache only. null becomes Success(null), not an error.Solo caché. null es Success(null), no error.

getCachedMarginCalculatorLastSyncAt() local

RetornaReturnsDevuelve DateTime? (sem Result)(no Result)(sin Result)

Timestamp da última sincronização do container, para o DataLoadInfo. Erro → null.The container's last-sync timestamp, for DataLoadInfo. Error → null.Timestamp de última sincronización del container, para DataLoadInfo. Error → null.

saveMarginCalculator({entity}) local

RetornaReturnsDevuelve Result<void, Failure>

Destrutivo: clearMarginCalculator() + _box.put(entity.toModel()). É o cache-writer chamado após cada fetch bem-sucedido (mock/remoto).Destructive: clearMarginCalculator() + _box.put(entity.toModel()). It's the cache-writer called after each successful fetch (mock/remote).Destructivo: clearMarginCalculator() + _box.put(entity.toModel()). Es el cache-writer llamado tras cada fetch exitoso (mock/remoto).

09

Datasources

Um card por datasource (dropdown). No corpo: método, envio, retorno, fluxo de uso e tratamento de erro.One card per datasource (dropdown). In the body: method, what it sends, return, usage flow and error handling.Un card por datasource (dropdown). En el cuerpo: método, envío, retorno, flujo de uso y manejo de errores.

Remote MarginCalculatorRemoteDataSource gRPC
getMarginCalculator({locationHierarchySfid, lastModifiedDate?})
EnvioSendsEnvío
monta MarginCalculatorRequest (só seta lastModifiedDate se não-vazio) e chama _client.getMarginCalculator(request) no MarginCalculatorConectaRepServiceClient.builds MarginCalculatorRequest (only sets lastModifiedDate if non-empty) and calls _client.getMarginCalculator(request) on MarginCalculatorConectaRepServiceClient.arma MarginCalculatorRequest (solo setea lastModifiedDate si no está vacío) y llama _client.getMarginCalculator(request) en MarginCalculatorConectaRepServiceClient.
RetornoReturnRetorno
MarginCalculatorDTO (via response.toDTO())(via response.toDTO())(vía response.toDTO())
Fluxo de usoUsage flowFlujo de uso
chamado pelo caminho remoto do repository (_fetchFromRemoteWithFallback), quando online e sem mock; o resultado é gravado no cache.called by the repository's remote path (_fetchFromRemoteWithFallback), when online and not mocking; the result is written to cache.llamado por el camino remoto del repository (_fetchFromRemoteWithFallback), online y sin mock; el resultado se graba en caché.
Tratamento de erroError handlingManejo de errores
GrpcErrorGrpcExceptionHandler; outros → ServerException. Em erro, o repository faz fallback pro cache.GrpcErrorGrpcExceptionHandler; others → ServerException. On error, the repository falls back to cache.GrpcErrorGrpcExceptionHandler; otros → ServerException. En error, el repository hace fallback al caché.
Local MarginCalculatorLocalDataSource ObjectBox

Envio / fluxo: persistência local via ObjectBox (ObjectBoxDatabase), boxes MarginCalculatorModel e MarginCalculatorProductModel — sem rede. Erro: falhas de persistência propagam como CacheException (não engolidas).Sends / flow: local persistence via ObjectBox (ObjectBoxDatabase), MarginCalculatorModel and MarginCalculatorProductModel boxes — no network. Error: persistence failures propagate as CacheException (not swallowed).Envío / flujo: persistencia local vía ObjectBox (ObjectBoxDatabase), boxes MarginCalculatorModel y MarginCalculatorProductModel — sin red. Error: fallos de persistencia propagan como CacheException (no tragados).

getMarginCalculator()
RetornoReturnRetorno
MarginCalculatorEntity?
ComportamentoBehaviorComportamiento
models.first.toEntity() — o agregado único, ou null se o cache está vazio.models.first.toEntity() — the single aggregate, or null if the cache is empty.models.first.toEntity() — el agregado único, o null si el caché está vacío.
getMarginCalculatorLastSyncAt()
RetornoReturnRetorno
DateTime?
ComportamentoBehaviorComportamiento
models.first.lastSyncAt — timestamp da última sync do container.— the container's last-sync timestamp.— timestamp de última sincronización del container.
saveMarginCalculator({entity})
RetornoReturnRetorno
void
ComportamentoBehaviorComportamiento
destrutivo: clearMarginCalculator() + _box.put(entity.toModel()). Cache-writer após cada fetch.destructive: clearMarginCalculator() + _box.put(entity.toModel()). Cache-writer after each fetch.destructivo: clearMarginCalculator() + _box.put(entity.toModel()). Cache-writer tras cada fetch.
mergeAdhocMarginCalculator({incoming, accountSfid})
RetornoReturnRetorno
void
ComportamentoBehaviorComportamiento
funde produtos incoming ao cache via AdhocMarginCalculatorMerge (adiciona novos por sfid; anexa o accountSfid aos já existentes). Não exposto na interface — usado pelo fluxo Ad Hoc.merges incoming products into cache via AdhocMarginCalculatorMerge (adds new by sfid; appends the accountSfid to existing ones). Not on the interface — used by the Ad Hoc flow.funde productos incoming al caché vía AdhocMarginCalculatorMerge (agrega nuevos por sfid; anexa el accountSfid a los existentes). No expuesto en la interfaz — usado por el flujo Ad Hoc.
clearMarginCalculator()
RetornoReturnRetorno
void
ComportamentoBehaviorComportamiento
limpa as boxes na ordem filhas→raiz (_productBox depois _box).clears boxes children→root (_productBox then _box).limpia las boxes hijas→raíz (_productBox luego _box).
Mock MarginCalculatorMockDataSource JSON
getMarginCalculator()
EnvioSendsEnvío
carrega o asset JSON margin_calculator/margin_calculator (por mercado, real vs sintético via useRealMockData) — sem rede.loads the JSON asset margin_calculator/margin_calculator (per market, real vs synthetic via useRealMockData) — no network.carga el asset JSON margin_calculator/margin_calculator (por mercado, real vs sintético vía useRealMockData) — sin red.
RetornoReturnRetorno
MarginCalculatorDTO (via MarginCalculatorDTOJsonMapper.fromMap)(via MarginCalculatorDTOJsonMapper.fromMap)(vía MarginCalculatorDTOJsonMapper.fromMap)
Fluxo de usoUsage flowFlujo de uso
usado quando useMock está ligado ou source == mock; grava no cache como um fetch normal.used when useMock is on or source == mock; writes to cache like a normal fetch.usado cuando useMock está activo o source == mock; graba en caché como un fetch normal.
Tratamento de erroError handlingManejo de errores
asset ausente ou JSON inválido propaga como CacheException (sem rede envolvida).missing asset or invalid JSON propagates as CacheException (no network involved).asset ausente o JSON inválido propaga como CacheException (sin red involucrada).
10

Enums e labelsEnums & labelsEnums y labels

A feature não tem enums de domínio — todos os campos do modelo são primitivos. O único enum é de UI: a aba ativa da tela. Não trafega no wire nem é persistido.The feature has no domain enums — every model field is primitive. The only enum is a UI one: the screen's active tab. It doesn't travel on the wire and isn't persisted.La feature no tiene enums de dominio — todos los campos del modelo son primitivos. El único enum es de UI: la pestaña activa de la pantalla. No viaja en el wire ni se persiste.

MarginCalculatorTab 2 · UI2 · UI2 · UI
casevalue
stick"stick"
pack"pack"
11

UseCases

Dois UseCases, ambos em usecases/margin_calculator/. O container é compartilhado por conta — cada produto declara a lista de contas (accountSfids) a que pertence — então o notifier consome a variante por conta.Two UseCases, both in usecases/margin_calculator/. The container is shared per account — each product declares the list of accounts (accountSfids) it belongs to — so the notifier consumes the per-account variant.Dos UseCases, ambos en usecases/margin_calculator/. El container es compartido por cuenta — cada producto declara la lista de cuentas (accountSfids) a la que pertenece — así el notifier consume la variante por cuenta.

GetMarginCalculatorUseCase 3
MétodoMethodMétodoRetornaReturnsDevuelveUsoUseUso
execute({source})Result<MarginCalculatorEntity, Failure>Catálogo completo. Roteia por sourcerepository.getMarginCalculator.Full catalog. Routes by sourcerepository.getMarginCalculator.Catálogo completo. Rutea por sourcerepository.getMarginCalculator.
getCached()Result<MarginCalculatorEntity?, Failure>Só cache; null vira Success(null).Cache only; null becomes Success(null).Solo caché; null es Success(null).
getCachedLastSyncAt()DateTime?Timestamp da última sincronização. Alimenta o DataLoadInfo.Last-sync timestamp. Feeds DataLoadInfo.Timestamp de última sincronización. Alimenta DataLoadInfo.
GetMarginCalculatorForAccountUseCase 1 · usado pela telaused by the screenusado por la pantalla
MétodoMethodMétodoRetornaReturnsDevuelveUsoUseUso
execute({accountSfid, source})Result<MarginCalculatorEntity, Failure>Chama repository.getMarginCalculator e filtra os produtos onde accountSfids.contains(accountSfid) (in-memory). É o que o notifier chama, com o sfid do varejo da visita.Calls repository.getMarginCalculator and filters products where accountSfids.contains(accountSfid) (in-memory). This is what the notifier calls, with the visit retailer's sfid.Llama repository.getMarginCalculator y filtra los productos donde accountSfids.contains(accountSfid) (in-memory). Es lo que el notifier llama, con el sfid del punto de venta de la visita.
12

Notifier & State

O MarginCalculatorNotifier (@riverpod, family por visitSfid, with AsyncGuard<MarginCalculatorState>) é o cérebro da tela. O build({visitSfid}) observa os UseCases, resolve a visita pelo sfid (getCachedBySfid) para descobrir o varejo, e busca o catálogo daquela conta. O State (MarginCalculatorState, Freezed) é a fonte única de verdade: guarda os produtos, a seleção, a aba e os quatro campos digitados — e todo o cálculo vive em getters do State, recomputado a cada dígito.The MarginCalculatorNotifier (@riverpod, family by visitSfid, with AsyncGuard<MarginCalculatorState>) is the screen's brain. build({visitSfid}) watches the UseCases, resolves the visit by sfid (getCachedBySfid) to find the retailer, and fetches that account's catalog. The State (MarginCalculatorState, Freezed) is the single source of truth: it holds the products, selection, tab and the four typed fields — and all math lives in State getters, recomputed on every keystroke.El MarginCalculatorNotifier (@riverpod, family por visitSfid, with AsyncGuard<MarginCalculatorState>) es el cerebro de la pantalla. build({visitSfid}) observa los UseCases, resuelve la visita por sfid (getCachedBySfid) para hallar el punto de venta, y busca el catálogo de esa cuenta. El State (MarginCalculatorState, Freezed) es la fuente única de verdad: guarda los productos, la selección, la pestaña y los cuatro campos digitados — y toda la matemática vive en getters del State, recalculada en cada tecla.

MétodosMethodsMétodos

build({visitSfid}) async

RetornoReturnRetorno FutureOr<MarginCalculatorState>

Via guardedBuild: resolve a VisitEntity (getCachedBySfid), busca o catálogo por conta (_fetchForAccount) e monta o State com o primeiro produto selecionado e a aba stick.Via guardedBuild: resolves the VisitEntity (getCachedBySfid), fetches the per-account catalog (_fetchForAccount) and assembles the State with the first product selected and the stick tab.Vía guardedBuild: resuelve la VisitEntity (getCachedBySfid), busca el catálogo por cuenta (_fetchForAccount) y arma el State con el primer producto seleccionado y la pestaña stick.

refresh() pull-to-refresh

RetornoReturnRetorno Future<void>

Null-guard no state.value; via runGuarded, re-busca o catálogo com source: remote, re-resolve o produto selecionado (_resolveSelectedAfterRefresh — mantém pelo sfid, senão o primeiro) e preserva a aba e os campos. Não seta AsyncValue.loading.Null-guards state.value; via runGuarded, re-fetches the catalog with source: remote, re-resolves the selected product (_resolveSelectedAfterRefresh — keeps it by sfid, else the first) and preserves tab and inputs. Doesn't set AsyncValue.loading.Null-guard en state.value; vía runGuarded, re-busca el catálogo con source: remote, re-resuelve el producto seleccionado (_resolveSelectedAfterRefresh — lo mantiene por sfid, si no el primero) y preserva la pestaña y los campos. No setea AsyncValue.loading.

selectProduct({product})

RetornoReturnRetorno void

Fixa o produto selecionado e limpa os quatro campos digitados (preço e quantidade de ambas as abas).Sets the selected product and clears the four typed fields (price and quantity of both tabs).Fija el producto seleccionado y limpia los cuatro campos digitados (precio y cantidad de ambas pestañas).

selectTab({tab})

RetornoReturnRetorno void

Alterna entre stick e pack (no-op se já é a aba ativa). Os campos de cada aba são independentes e preservados.Switches between stick and pack (no-op if already active). Each tab's fields are independent and preserved.Alterna entre stick y pack (no-op si ya es la activa). Los campos de cada pestaña son independientes y se preservan.

updateActualSellingPriceForActiveTab({value}) · updateQtyPacksForActiveTab({value})

RetornoReturnRetorno void

Roteiam o valor digitado para o campo da aba ativa. Por baixo, os quatro setters diretos — updateStickActualSellingPrice, updateStickQtyPacks, updatePackActualSellingPrice, updatePackQtyPacks — só guardam a String crua no State; o parse e o cálculo acontecem nos getters.Route the typed value to the active tab's field. Under the hood, the four direct setters — updateStickActualSellingPrice, updateStickQtyPacks, updatePackActualSellingPrice, updatePackQtyPacks — just store the raw String in State; parsing and math happen in the getters.Rutean el valor digitado al campo de la pestaña activa. Por debajo, los cuatro setters directos — updateStickActualSellingPrice, updateStickQtyPacks, updatePackActualSellingPrice, updatePackQtyPacks — solo guardan el String crudo en el State; el parse y el cálculo ocurren en los getters.

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

MarginCalculatorState campos + getters de cálculofields + compute getterscampos + getters de cálculo
campotipodefault
visitSfidString
accountSfidString
accountNameString
lastSyncAtDateTime?null
productsList<MarginCalculatorProductEntity>[]
selectedProductMarginCalculatorProductEntity?null
activeTabMarginCalculatorTabstick
stickActualSellingPriceInputString""
stickQtyPacksInputString""
packActualSellingPriceInputString""
packQtyPacksInputString""

Getters de cálculo (todo o motor): preços do produto (displayedRetailerCost, displayedRecommendedPrice, rrpForActiveTab); quantidade (activeTabQtyPacks, stickQtyConvertedFromPacks, stickTotalSticks, packTotalPacks); lucro por unidade (stick/packRrspBankableProfitPerUnit, stick/packActualBankableProfitPerUnit); totais (stick/packTotalBankableProfitRRSP, stick/packTotalBankableProfitActual); combinados (combinedTotalRRSP, combinedTotalActual, combinedTotalRetailerCost); margem (marginPercentRRSP, marginPercentActual); linhas da tabela (perUnitRow…, totalRow…, perUnitActualBelowRRSP); flags (hasProducts, hasSelectedProduct, isStick). Preço em branco cai pro RRP; custo total 0 → margem 0%.Compute getters (the whole engine): product prices (displayedRetailerCost, displayedRecommendedPrice, rrpForActiveTab); quantity (activeTabQtyPacks, stickQtyConvertedFromPacks, stickTotalSticks, packTotalPacks); per-unit profit (stick/packRrspBankableProfitPerUnit, stick/packActualBankableProfitPerUnit); totals (stick/packTotalBankableProfitRRSP, stick/packTotalBankableProfitActual); combined (combinedTotalRRSP, combinedTotalActual, combinedTotalRetailerCost); margin (marginPercentRRSP, marginPercentActual); table rows (perUnitRow…, totalRow…, perUnitActualBelowRRSP); flags (hasProducts, hasSelectedProduct, isStick). Blank price falls back to RRP; total cost 0 → 0% margin.Getters de cálculo (todo el motor): precios del producto (displayedRetailerCost, displayedRecommendedPrice, rrpForActiveTab); cantidad (activeTabQtyPacks, stickQtyConvertedFromPacks, stickTotalSticks, packTotalPacks); lucro por unidad (stick/packRrspBankableProfitPerUnit, stick/packActualBankableProfitPerUnit); totales (stick/packTotalBankableProfitRRSP, stick/packTotalBankableProfitActual); combinados (combinedTotalRRSP, combinedTotalActual, combinedTotalRetailerCost); margen (marginPercentRRSP, marginPercentActual); filas de la tabla (perUnitRow…, totalRow…, perUnitActualBelowRRSP); flags (hasProducts, hasSelectedProduct, isStick). Precio en blanco cae al RRP; costo total 0 → margen 0%.

As fórmulas exatasThe exact formulasLas fórmulas exactas

  • bankableProfitPerUnit = sellingPrice − retailerCost (RRSP usa rrsp*; Actual usa o preço digitado, ou rrp* se em branco)(RRSP uses rrsp*; Actual uses the typed price, or rrp* if blank)(RRSP usa rrsp*; Actual usa el precio digitado, o rrp* si está en blanco)
  • stickTotalSticks = activeTabQtyPacks × sticksPerPack (quantidade é digitada em packs)(quantity is typed in packs)(la cantidad se digita en packs)
  • total = bankableProfitPerUnit × totalUnits
  • marginPercent = combinedTotalProfit ÷ combinedTotalRetailerCost × 100 (0 se o custo total ≤ 0)(0 if total cost ≤ 0)(0 si el costo total ≤ 0)
13

Page e widgetsPage & widgetsPage y widgets

A MarginCalculatorPage (ConsumerWidget, recebe só o visitSfid) observa o marginCalculatorProvider(visitSfid:) e monta os widgets filhos. Loading e erro são globais (asyncState.when); o conteúdo existe só no ramo data. Cada card filho é auto-suficiente (lê o provider e retorna SizedBox.shrink() se o State for nulo). Árvore de composição:MarginCalculatorPage (ConsumerWidget, takes only visitSfid) watches marginCalculatorProvider(visitSfid:) and composes the child widgets. Loading and error are global (asyncState.when); content exists only in the data branch. Each child card is self-sufficient (reads the provider and returns SizedBox.shrink() if State is null). Composition tree:MarginCalculatorPage (ConsumerWidget, recibe solo el visitSfid) observa el marginCalculatorProvider(visitSfid:) y compone los widgets hijos. Loading y error son globales (asyncState.when); el contenido existe solo en la rama data. Cada card hijo es autosuficiente (lee el provider y retorna SizedBox.shrink() si el State es nulo). Árbol de composición:

  • MarginCalculatorPage
    • AppPageShell displayBackButton · bg=background
      • CustomLoadingIndicator loading
      • FailureStateView error → invalidate
      • CustomPullToRefresh data → refresh()
        • DataLoadInfo state.lastSyncAt
        • MarginCalculatorHeader ícone + título
        • MarginCalculatorProductDropdown CustomDropdown.single → selectProduct
          • ConectaModal picker de produto (aberto pelo CustomDropdown)
        • MarginCalculatorTabBar Stick (azul) / Pack (verde) → selectTab
        • MarginCalculatorInputValuesCard ConsumerStatefulWidget · controllers espelham o State
          • CustomInput preço → updateActualSellingPriceForActiveTab
          • CustomInput qtd packs → updateQtyPacksForActiveTab
        • MarginCalculatorBankableCard RRSP/Actual por unidade + total; stick: callouts pack→stick + conversão
        • MarginCalculatorCombinedTotalsCard header roxo · stick+pack+total
        • MarginCalculatorIncidenceCard header laranja · margem % (value.toStringAsFixed(2))

O MarginCalculatorInputValuesCard mantém dois TextEditingController e os re-sincroniza com o State quando a aba ou o produto muda (_syncControllersIfNeeded), para os valores de cada aba não vazarem entre si. Valores monetários usam CurrencyUtils.formatAmountOnly; a margem % usa toStringAsFixed(2) por ser percentual, não moeda.The MarginCalculatorInputValuesCard keeps two TextEditingControllers and re-syncs them with State when tab or product changes (_syncControllersIfNeeded), so each tab's values don't bleed across. Money uses CurrencyUtils.formatAmountOnly; margin % uses toStringAsFixed(2) since it's a percentage, not currency.El MarginCalculatorInputValuesCard mantiene dos TextEditingController y los re-sincroniza con el State cuando cambia la pestaña o el producto (_syncControllersIfNeeded), para que los valores de cada pestaña no se filtren entre sí. Los valores monetarios usan CurrencyUtils.formatAmountOnly; el margen % usa toStringAsFixed(2) por ser porcentaje, no moneda.

Notas por mercadoMarket notesNotas por mercado

A Calculadora de margem é uma ferramenta do detalhe da visita, dirigida por End Market Configuration: ela só aparece quando visit_detail_tools_grid lista o detalhe margin_calculator. Hoje isso acontece somente na África do Sul:The Margin calculator is a visit-detail tool, driven by End Market Configuration: it only appears when visit_detail_tools_grid lists the margin_calculator detail. Today that happens only in South Africa:La Calculadora de margen es una herramienta del detalle de la visita, regida por End Market Configuration: solo aparece cuando visit_detail_tools_grid lista el detalle margin_calculator. Hoy eso ocurre solo en Sudáfrica:

BR CL ZAx AR PY PE
disponívelavailabledisponible presente, desligadopresent, offpresente, apagado ausenteabsentausente
ZA

África do SulSouth AfricaSudáfrica Único mercado com a ferramenta ligada. A moeda (ZAR) é formatada no estilo anglo 1,234.56 via CurrencyUtils — o conceito stick/pack e os preços RRP/RRSP são próprios do tabaco na África do Sul. The only market with the tool on. Currency (ZAR) is formatted anglo-style 1,234.56 via CurrencyUtils — the stick/pack concept and RRP/RRSP prices are specific to tobacco in South Africa. El único mercado con la herramienta activa. La moneda (ZAR) se formatea al estilo anglo 1,234.56 vía CurrencyUtils — el concepto stick/pack y los precios RRP/RRSP son propios del tabaco en Sudáfrica.

Pendências / roadmapPending / roadmapPendientes / roadmap

  • Existem mocks JSON para os 6 mercados (br/cl/za/ar/py/pe_margin_calculator.json) e a stack de dados é market-agnóstica, mas o ponto de entrada só está ligado em ZA — BR/CL/AR/PY/PE não têm o detalhe margin_calculator na grade de ferramentas, então a tela é inalcançável neles hoje. Habilitar em outro mercado = adicionar o detalhe ao visit_detail_tools_grid daquele mercado no EMC.There are JSON mocks for all 6 markets (br/cl/za/ar/py/pe_margin_calculator.json) and the data stack is market-agnostic, but the entry point is only wired in ZA — BR/CL/AR/PY/PE lack the margin_calculator detail in the tools grid, so the screen is unreachable there today. Enabling another market = adding the detail to that market's visit_detail_tools_grid in the EMC.Existen mocks JSON para los 6 mercados (br/cl/za/ar/py/pe_margin_calculator.json) y la stack de datos es market-agnóstica, pero el punto de entrada solo está conectado en ZA — BR/CL/AR/PY/PE no tienen el detalle margin_calculator en la grilla de herramientas, así que la pantalla es inalcanzable allí hoy. Habilitar otro mercado = agregar el detalle al visit_detail_tools_grid de ese mercado en el EMC.