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.
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.
Como acessarHow to openCómo acceder
- 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.
- 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).
- 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.
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).
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:
- 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).
- 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.
- 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.
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.
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
- filter by accountMarginCalculatorNotifier + State
- toEntityMarginCalculatorEntitydomain · cache
- toModelMarginCalculatorModelObjectBox
- toEntityMarginCalculatorEntitydomain
- toDTOMarginCalculatorDTODTO · Freezed
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:
getMarginCalculatorunaryrpc getMarginCalculator(MarginCalculatorRequest) returns (MarginCalculatorReply)
path /mn.bat.conectarep.streambridge.MarginCalculatorConectaRepService/getMarginCalculator
MarginCalculatorRequestlocationHierarchySfidstring· #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)lastModifiedDatestring· #2 · optional
MarginCalculatorReplyrepeated MarginCalculatorProduct products — o 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
Campo Proto DTO Model Entity lastSyncAt—DateTime DateTime DateTime productsrepeated MarginCalculatorProduct List<…DTO> ToMany<…Model>List<…Entity> MarginCalculatorProduct MarginCalculator.products[] 10 campos
Campo Proto DTO Model Entity sfidstring String String @Unique String accountSfidsaccountSfidrepeated stringList<String> List<String> List<String> namestring String String String sticksPerPackint32 int int int retailerCostPerStickdouble double double double retailerCostPerPackdouble double double double rrpPerStickdouble double double double rrpPerPackdouble double double double rrspPerStickdouble double double double rrspPerPackdouble double double double
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ón | MétodoMethodMétodo |
|---|---|
| JSON → DTO | static 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 → DTO | toDTO() (no MarginCalculatorReply; accountSfid→accountSfids; lastSyncAt = DateTimeUtils.now())(on MarginCalculatorReply; accountSfid→accountSfids; lastSyncAt = DateTimeUtils.now())(en MarginCalculatorReply; accountSfid→accountSfids; lastSyncAt = DateTimeUtils.now()) |
| DTO → Entity | toEntity() |
| Entity → Model | toModel() (popula o ToMany de products)(fills the products ToMany)(llena el ToMany de products) |
| Model → Entity | toEntity() |
Os únicos deltasThe only deltasLos únicos deltas
- rename
accountSfid→accountSfids(no Proto)renameaccountSfid→accountSfids(in the Proto)renameaccountSfid→accountSfids(en el Proto) - a relação
productsviraToManyno Modeltheproductsrelation becomesToManyin the Modella relaciónproductspasa aToManyen el Model lastSyncAtgerado no mapper comDateTimeUtils.now()(o proto não carrega timestamp) — CLAUDE §21generated in the mapper withDateTimeUtils.now()(the proto carries no timestamp) — CLAUDE §21generado en el mapper conDateTimeUtils.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
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
_useMock== true ouorosource == 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é.source == localou offlineor offlineu offline→_fetchFromCacheOrFail(): só cache; vazio →Error(NetworkFailure).→_fetchFromCacheOrFail(): cache only; empty →Error(NetworkFailure).→_fetchFromCacheOrFail(): solo caché; vacío →Error(NetworkFailure).- senão (remoto + conectado)otherwise (remote + connected)si no (remoto + conectado)→
_fetchFromRemoteWithFallback(): lêcurrentResourceProvider; senullcai pro cache; senão chama o remoto comresource.locationHierarchyId, mapeia, grava; em erro, fallback pro cache.→_fetchFromRemoteWithFallback(): readscurrentResourceProvider; ifnullfalls back to cache; else calls remote withresource.locationHierarchyId, maps, writes; on error, falls back to cache.→_fetchFromRemoteWithFallback(): leecurrentResourceProvider; sinullcae al caché; si no llama al remoto conresource.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).
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ó setalastModifiedDatese não-vazio) e chama_client.getMarginCalculator(request)noMarginCalculatorConectaRepServiceClient.buildsMarginCalculatorRequest(only setslastModifiedDateif non-empty) and calls_client.getMarginCalculator(request)onMarginCalculatorConectaRepServiceClient.armaMarginCalculatorRequest(solo setealastModifiedDatesi no está vacío) y llama_client.getMarginCalculator(request)enMarginCalculatorConectaRepServiceClient. - RetornoReturnRetorno
MarginCalculatorDTO(viaresponse.toDTO())(viaresponse.toDTO())(víaresponse.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
GrpcError→GrpcExceptionHandler; outros →ServerException. Em erro, o repository faz fallback pro cache.GrpcError→GrpcExceptionHandler; others →ServerException. On error, the repository falls back to cache.GrpcError→GrpcExceptionHandler; 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, ounullse o cache está vazio.models.first.toEntity()— the single aggregate, ornullif the cache is empty.models.first.toEntity()— el agregado único, onullsi 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
incomingao cache viaAdhocMarginCalculatorMerge(adiciona novos porsfid; anexa oaccountSfidaos já existentes). Não exposto na interface — usado pelo fluxo Ad Hoc.mergesincomingproducts into cache viaAdhocMarginCalculatorMerge(adds new bysfid; appends theaccountSfidto existing ones). Not on the interface — used by the Ad Hoc flow.funde productosincomingal caché víaAdhocMarginCalculatorMerge(agrega nuevos porsfid; anexa elaccountSfida los existentes). No expuesto en la interfaz — usado por el flujo Ad Hoc.
clearMarginCalculator()
- RetornoReturnRetorno
void- ComportamentoBehaviorComportamiento
- limpa as boxes na ordem filhas→raiz (
_productBoxdepois_box).clears boxes children→root (_productBoxthen_box).limpia las boxes hijas→raíz (_productBoxluego_box).
Mock MarginCalculatorMockDataSource JSON
getMarginCalculator()
- EnvioSendsEnvío
- carrega o asset JSON
margin_calculator/margin_calculator(por mercado, real vs sintético viauseRealMockData) — sem rede.loads the JSON assetmargin_calculator/margin_calculator(per market, real vs synthetic viauseRealMockData) — no network.carga el asset JSONmargin_calculator/margin_calculator(por mercado, real vs sintético víauseRealMockData) — sin red. - RetornoReturnRetorno
MarginCalculatorDTO(viaMarginCalculatorDTOJsonMapper.fromMap)(viaMarginCalculatorDTOJsonMapper.fromMap)(víaMarginCalculatorDTOJsonMapper.fromMap)- Fluxo de usoUsage flowFlujo de uso
- usado quando
useMockestá ligado ousource == mock; grava no cache como um fetch normal.used whenuseMockis on orsource == mock; writes to cache like a normal fetch.usado cuandouseMockestá activo osource == 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 asCacheException(no network involved).asset ausente o JSON inválido propaga comoCacheException(sin red involucrada).
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
| case | value |
|---|---|
stick | "stick" |
pack | "pack" |
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étodo | RetornaReturnsDevuelve | UsoUseUso |
|---|---|---|
execute({source}) | Result<MarginCalculatorEntity, Failure> | Catálogo completo. Roteia por source → repository.getMarginCalculator.Full catalog. Routes by source → repository.getMarginCalculator.Catálogo completo. Rutea por source → repository.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étodo | RetornaReturnsDevuelve | UsoUseUso |
|---|---|---|
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. |
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
| campo | tipo | default |
|---|---|---|
visitSfid | String | — |
accountSfid | String | — |
accountName | String | — |
lastSyncAt | DateTime? | null |
products | List<MarginCalculatorProductEntity> | [] |
selectedProduct | MarginCalculatorProductEntity? | null |
activeTab | MarginCalculatorTab | stick |
stickActualSellingPriceInput | String | "" |
stickQtyPacksInput | String | "" |
packActualSellingPriceInput | String | "" |
packQtyPacksInput | String | "" |
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 usarrsp*; Actual usa o preço digitado, ourrp*se em branco)(RRSP usesrrsp*; Actual uses the typed price, orrrp*if blank)(RRSP usarrsp*; Actual usa el precio digitado, orrp*si está en blanco)stickTotalSticks = activeTabQtyPacks × sticksPerPack(quantidade é digitada em packs)(quantity is typed in packs)(la cantidad se digita en packs)total = bankableProfitPerUnit × totalUnitsmarginPercent = combinedTotalProfit ÷ combinedTotalRetailerCost × 100(0 se o custo total ≤ 0)(0 if total cost ≤ 0)(0 si el costo total ≤ 0)
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))
- AppPageShell displayBackButton · bg=background
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:
Á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 detalhemargin_calculatorna grade de ferramentas, então a tela é inalcançável neles hoje. Habilitar em outro mercado = adicionar o detalhe aovisit_detail_tools_griddaquele 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 themargin_calculatordetail in the tools grid, so the screen is unreachable there today. Enabling another market = adding the detail to that market'svisit_detail_tools_gridin 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 detallemargin_calculatoren la grilla de herramientas, así que la pantalla es inalcanzable allí hoy. Habilitar otro mercado = agregar el detalle alvisit_detail_tools_gridde ese mercado en el EMC.