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).
Transação gRPC · DispatchergRPC transaction · DispatcherTransacción gRPC · Dispatcher

Envio de visitaVisit uploadEnvío de visita

A transação de escrita que envia uma visita ao backend quando o representante de vendas inicia, finaliza ou marca uma visita como improdutiva. É a única transação do Dispatcher com uma fila offline de verdade: sem internet, o envio é enfileirado e reenviado sozinho quando a conexão volta — todas as outras transações apenas registram erro. The write transaction that uploads a visit to the backend when the sales rep starts, finishes or marks a visit as non-productive. It is the only Dispatcher transaction with a real offline queue: while offline, the upload is queued and retried on its own when the connection returns — every other transaction only records an error. La transacción de escritura que envía una visita al backend cuando el representante de ventas inicia, finaliza o marca una visita como improductiva. Es la única transacción del Dispatcher con una cola offline de verdad: sin internet, el envío se encola y se reintenta solo cuando vuelve la conexión — todas las demás transacciones solo registran error.

PúblicoAudiencePúblico
QA · Suporte · Produto · DevQA · Support · Product · DevQA · Soporte · Producto · Dev
CamadaLayerCapa
Escrita · DispatcherWrite · DispatcherEscritura · Dispatcher
RelacionadoRelatedRelacionado
AtualizadoUpdatedActualizado
17/08/20262026-08-17
Disponível emAvailable inDisponible en BR CL ZA
01

O que é e quando aconteceWhat it is and when it happensQué es y cuándo ocurre

Toda vez que o representante de vendas mexe no ciclo de vida de uma visita — inicia, finaliza/encerra ou marca como improdutiva (sem compra) — o app envia essa visita ao backend por esta transação. O caso mais comum é o encerramento: quando o rep conclui o atendimento no varejo e toca em Finalizar visita, é este envio que registra a visita como concluída no sistema. Whenever the sales rep touches a visit's lifecycle — starts it, finishes/closes it, or marks it non-productive (no buy) — the app uploads that visit to the backend through this transaction. The most common case is the close-out: when the rep wraps up at the retail and taps Finish visit, this upload is what records the visit as completed in the system. Cada vez que el representante de ventas toca el ciclo de vida de una visita — la inicia, la finaliza/cierra o la marca como improductiva (sin compra) — la app envía esa visita al backend por esta transacción. El caso más común es el cierre: cuando el rep termina la atención en el punto de venta y toca Finalizar visita, este envío es lo que registra la visita como concluida en el sistema.

Início da visitaVisit startInicio de la visita

O rep chega ao varejo e inicia o atendimento — a visita é registrada como iniciada.The rep arrives at the retail and starts the call — the visit is recorded as started.El rep llega al punto de venta e inicia la atención — la visita se registra como iniciada.

EncerramentoClose-outCierre

O rep finaliza a visita depois de resolver as pendências de encerramento. O caso central desta doc.The rep finishes the visit after clearing the closure checklist. The central case of this doc.El rep finaliza la visita tras resolver los pendientes de cierre. El caso central de este doc.

ImprodutivaNon-productiveImproductiva

A visita termina sem compra (No Buy); o rep informa o motivo, que sobe junto com o envio.The visit ends with no buy; the rep records the reason, which goes up with the upload.La visita termina sin compra; el rep informa el motivo, que sube junto con el envío.

Um gesto, um envioOne gesture, one uploadUn gesto, un envío Para o rep, cada uma dessas ações é um toque simples (iniciar, finalizar, marcar sem compra). O envio da visita acontece por baixo, automaticamente — o rep não precisa "enviar" nada à mão. For the rep, each of these actions is a single tap (start, finish, mark no-buy). The visit upload happens underneath, automatically — the rep never has to "send" anything by hand. Para el rep, cada una de estas acciones es un toque simple (iniciar, finalizar, marcar sin compra). El envío de la visita ocurre por debajo, automáticamente — el rep nunca tiene que "enviar" nada a mano.

02

Fluxo de telas que disparaScreen flow that fires itFlujo de pantallas que lo dispara

A jornada de encerramento vive na feature de Visitas e no Detalhe da visita; aqui só situamos onde o envio acontece:The close-out journey lives in the Visits feature and the Visit detail; here we only place where the upload happens:El recorrido de cierre vive en la feature de Visitas y en el Detalle de la visita; aquí solo situamos dónde ocurre el envío:

  1. Detalhe da visitaVisit detailDetalle de la visitaO rep abre a visita e toca em Iniciar visita — este toque já dispara um envio (início).The rep opens the visit and taps Start visit — this tap already fires an upload (start).El rep abre la visita y toca Iniciar visita — este toque ya dispara un envío (inicio).
  2. Atendimento no varejoIn-store callAtención en el punto de ventaO rep executa as tarefas da visita (pedido, pesquisas, merchandising, etc.). O botão troca para Finalizar visita.The rep runs the visit's tasks (order, surveys, merchandising, etc.). The button switches to Finish visit.El rep ejecuta las tareas de la visita (pedido, encuestas, merchandising, etc.). El botón cambia a Finalizar visita.
  3. Checklist de encerramentoClosure checklistChecklist de cierreAo tocar em Finalizar visita, se houver pendências de encerramento o app leva o rep à tela de fim de visita para resolvê-las; se não houver, mostra uma confirmação.On Finish visit, if there are closure pendings the app takes the rep to the visit-end screen to clear them; if none, it shows a confirmation.Al tocar Finalizar visita, si hay pendientes de cierre la app lleva al rep a la pantalla de fin de visita para resolverlos; si no hay, muestra una confirmación.
  4. Finalizar → esta transaçãoFinish → this transactionFinalizar → esta transacciónConfirmado o encerramento, o app dispara o Envio de visita (finalização). É este passo que aciona a transação.Once the close-out is confirmed, the app fires Visit upload (finish). This step triggers the transaction.Confirmado el cierre, la app dispara el Envío de visita (finalización). Este paso activa la transacción.

Sem compraNo buySin compra Se a visita termina sem compra, o rep usa o botão No Buy (ou resolve a visita como improdutiva na tela de fim de jornada) e informa o motivo — o mesmo Envio de visita sobe, agora marcado como improdutivo. If the visit ends with no buy, the rep uses the No Buy button (or resolves it as non-productive on the end-of-journey screen) and records the reason — the same Visit upload goes up, now flagged non-productive. Si la visita termina sin compra, el rep usa el botón No Buy (o la resuelve como improductiva en la pantalla de fin de jornada) e informa el motivo — el mismo Envío de visita sube, ahora marcado como improductivo.

03

Depois do envioAfter sendingDespués del envío

Confirmação ao repConfirmation to the repConfirmación al rep
Quando o backend aceita, a visita fica registrada com o horário de entrada e saída e a posição do rep. A visita passa a constar como iniciada / concluída / improdutiva na lista de visitas.When the backend accepts it, the visit is recorded with the check-in/check-out time and the rep's position. The visit then shows as started / completed / non-productive in the visit list.Cuando el backend lo acepta, la visita queda registrada con la hora de entrada/salida y la posición del rep. La visita pasa a constar como iniciada / concluida / improductiva en la lista de visitas.
Sem internet (a diferença desta transação)Offline (this transaction's difference)Sin internet (la diferencia de esta transacción)
O Envio de visita é o único que entra em uma fila offline de verdade: sem conexão, a visita fica guardada e é reenviada sozinha assim que a internet volta (ou na próxima abertura do app). O rep não perde a visita e não precisa reenviar à mão. As demais transações, offline, só registram erro.Visit upload is the only one that enters a real offline queue: with no connection, the visit is stored and retried on its own as soon as the internet returns (or on the next app launch). The rep loses nothing and never has to resend by hand. The other transactions, offline, only record an error.El Envío de visita es el único que entra en una cola offline de verdad: sin conexión, la visita se guarda y se reintenta sola en cuanto vuelve internet (o al abrir la app de nuevo). El rep no pierde nada y nunca tiene que reenviar a mano. Las demás transacciones, offline, solo registran error.
Acompanhar o envioTracking the sendSeguir el envío
O status técnico do despacho (enviado, em fila, com erro) pode ser acompanhado na Central de dados / tracking de despachos do app; de lá também é possível reenviar manualmente um envio que ficou com erro — útil para suporte investigar.The dispatch's technical status (sent, queued, errored) can be followed in the app's Data Center / dispatch tracking; from there it's also possible to manually resend an errored dispatch — useful for support to investigate.El estado técnico del despacho (enviado, en cola, con error) puede seguirse en el Centro de datos / tracking de despachos de la app; desde ahí también se puede reenviar manualmente un envío que quedó con error — útil para que soporte investigue.
04

Visão técnicaTechnical overviewVisión técnica

Envio de visita (DispatcherType.visit, serviceName VisitUploadAPI) é a transação de saída que persiste o ciclo de vida de uma visita no backend Salesforce. É disparada por um único funil — VisitContext._updateVisitStatus — quando o rep inicia, finaliza ou torna improdutiva uma visita. O payload é enxuto: um único objeto VisitUploadDetail com identificadores, horários, coordenadas e status. Visit upload (DispatcherType.visit, serviceName VisitUploadAPI) is the outbound transaction that persists a visit's lifecycle in the Salesforce backend. It's fired by a single funnel — VisitContext._updateVisitStatus — when the rep starts, finishes or turns a visit non-productive. The payload is lean: a single VisitUploadDetail object with identifiers, timestamps, coordinates and status. Envío de visita (DispatcherType.visit, serviceName VisitUploadAPI) es la transacción de salida que persiste el ciclo de vida de una visita en el backend Salesforce. Se dispara desde un único embudo — VisitContext._updateVisitStatus — cuando el rep inicia, finaliza o vuelve improductiva una visita. El payload es escueto: un único objeto VisitUploadDetail con identificadores, horarios, coordenadas y status.

Payload de um objetoSingle-object payloadPayload de un objeto

VisitUploadDetails é um array de um único VisitUploadDetail — 12 campos base, mais 10 de geolocalização granular quando o EMC liga a flag.VisitUploadDetails is a single-element array of one VisitUploadDetail — 12 base fields, plus 10 granular-geolocation fields when the EMC turns the flag on.VisitUploadDetails es un array de un solo VisitUploadDetail — 12 campos base, más 10 de geolocalización granular cuando el EMC activa la flag.

Fila offline realReal offline queueCola offline real

Único DispatcherType com fila offline: offline + visitpending, reenviado por flush() na volta da conexão. Ver seção 10.The only DispatcherType with an offline queue: offline + visitpending, retried by flush() when the connection returns. See section 10.Único DispatcherType con cola offline: offline + visitpending, reintentado por flush() al volver la conexión. Ver sección 10.

RPC genéricoGeneric RPCRPC genérico

Não há RPC por visita: tudo passa pelo mesmo sendTransaction, com o JSON serializado no campo message e serviceName = VisitUploadAPI como discriminador.There's no per-visit RPC: everything goes through the same sendTransaction, with the JSON serialized into message and serviceName = VisitUploadAPI as the discriminator.No hay RPC por visita: todo pasa por el mismo sendTransaction, con el JSON serializado en message y serviceName = VisitUploadAPI como discriminador.

FontesSourcesFuentes BuildVisitUploadDispatcherPayloadUseCase + VisitUploadDispatcherPayloadInput + DispatcherOrchestrator + DispatcherType + DispatcherConectaRep.proto. O input carrega entities cruas de domínio (CLAUDE.md §36); o build() constrói todo o wire. BuildVisitUploadDispatcherPayloadUseCase + VisitUploadDispatcherPayloadInput + DispatcherOrchestrator + DispatcherType + DispatcherConectaRep.proto. The input carries raw domain entities (CLAUDE.md §36); build() constructs the entire wire. BuildVisitUploadDispatcherPayloadUseCase + VisitUploadDispatcherPayloadInput + DispatcherOrchestrator + DispatcherType + DispatcherConectaRep.proto. El input lleva entities crudas de dominio (CLAUDE.md §36); build() construye todo el wire.

05

Transporte gRPCgRPC transportTransporte gRPC

DispatcherConectaRep.proto · proto3 · package mn.bat.conectarep.dispatcher. O serviço expõe um único RPC genérico — não existe mensagem por transação. TODA transação de escrita do app usa este mesmo sendTransaction; o que muda é o serviceName (aqui VisitUploadAPI) e o JSON dentro de message.The service exposes a single generic RPC — there's no per-transaction message. EVERY write transaction in the app uses this same sendTransaction; what changes is the serviceName (here VisitUploadAPI) and the JSON inside message.El servicio expone un único RPC genérico — no existe mensaje por transacción. TODA transacción de escritura de la app usa este mismo sendTransaction; lo que cambia es el serviceName (aquí VisitUploadAPI) y el JSON dentro de message.

sendTransactionunary
MétodoMethodMétodo

rpc sendTransaction(InboxTransactionRequest) returns (InboxTransactionReply)

path /mn.bat.conectarep.dispatcher.DispatcherConectaRepService/sendTransaction

Request · InboxTransactionRequest
endpoint
string · #1 · endpoint alvo — type.destination.value (Salesforce)target endpoint — type.destination.value (Salesforce)endpoint destino — type.destination.value (Salesforce)
serviceName
string · #2 · discriminadorVisitUploadAPI (nunca com prefixo Promo_: o builder chama resolveServiceName(hasPromotion: false))discriminatorVisitUploadAPI (never Promo_-prefixed: the builder calls resolveServiceName(hasPromotion: false))discriminadorVisitUploadAPI (nunca con prefijo Promo_: el builder llama resolveServiceName(hasPromotion: false))
dateReference
string · #3 · AAAA-MM-DD de input.submittedAtYYYY-MM-DD of input.submittedAtAAAA-MM-DD de input.submittedAt
transactionReference
string · #4 · visit?.sfid ?? retailerSfid (chave de negócio, correlação/replay)visit?.sfid ?? retailerSfid (business key, correlation/replay)visit?.sfid ?? retailerSfid (clave de negocio, correlación/replay)
username
string · #5
message
string · #6 · o payload JSON serializado (a tabela da seção 08)the JSON payload serialized (the table in section 08)el payload JSON serializado (la tabla de la sección 08)
manufacturer / model / deviceUuid / deviceVersion
string · #7–#10 · dados do dispositivo (deviceUuid literal provisório — ver Pendências)device data (deviceUuid provisional literal — see Pending)datos del dispositivo (deviceUuid literal provisional — ver Pendientes)
tid
int64 · #11 · id de transação para idempotência/replay — Int64(envelope.tid) (ver seção 10)transaction id for idempotency/replay — Int64(envelope.tid) (see section 10)id de transacción para idempotencia/replay — Int64(envelope.tid) (ver sección 10)
Reply · InboxTransactionReply
status
int32 · #1 · status do ack (0/5 = sucesso; 1 = duplicado)ack status (0/5 = success; 1 = duplicate)status del ack (0/5 = éxito; 1 = duplicado)
message
string · #2 · mensagem do backendbackend messagemensaje del backend
transactionId
int32 · #3 · id atribuído pelo backend → persistido em backendTransactionId (o tid do próximo replay)backend-assigned id → persisted to backendTransactionId (the tid of the next replay)id asignado por el backend → persistido en backendTransactionId (el tid del próximo replay)

Envelope → Request O builder devolve um DispatcherEnvelope (type, serviceName, payload, account, visitDispatchKind, transactionReference, dateReference). O DispatcherGateway serializa payload em JSON para message, copia os campos de correlação, preenche dispositivo + bearer token de auth (retry único em unauthenticated) e chama o RPC. The builder returns a DispatcherEnvelope (type, serviceName, payload, account, visitDispatchKind, transactionReference, dateReference). The DispatcherGateway serializes payload to JSON into message, copies the correlation fields, fills in device + the auth bearer token (single retry on unauthenticated) and calls the RPC. El builder devuelve un DispatcherEnvelope (type, serviceName, payload, account, visitDispatchKind, transactionReference, dateReference). El DispatcherGateway serializa payload a JSON en message, copia los campos de correlación, completa dispositivo + el bearer token de auth (retry único en unauthenticated) y llama al RPC.

Para visit, resendMayDuplicate == true: um reenvio manual pode duplicar a visita no backend; a idempotência via tid (e o ack 1 = duplicado) é o que mitiga isso.For visit, resendMayDuplicate == true: a manual resend may duplicate the visit on the backend; idempotency via tid (and ack 1 = duplicate) is what mitigates it.Para visit, resendMayDuplicate == true: un reenvío manual puede duplicar la visita en el backend; la idempotencia vía tid (y el ack 1 = duplicado) es lo que lo mitiga.

06

serviceName e tipos de envioserviceName & upload kindsserviceName y tipos de envío

Há um único serviceNameVisitUploadAPI — e um único DispatcherType.visit. Não há variantes de serviceName (nem prefixo Promo_). O que varia é o tipo de envio, derivado de input.status + nonProductiveReason: ele define o visitDispatchKind (rótulo do envio na Central de dados) e o status textual do payload.There's a single serviceNameVisitUploadAPI — and a single DispatcherType.visit. There are no serviceName variants (nor a Promo_ prefix). What varies is the upload kind, derived from input.status + nonProductiveReason: it sets the visitDispatchKind (the upload's label in the Data Center) and the payload's textual status.Hay un único serviceNameVisitUploadAPI — y un único DispatcherType.visit. No hay variantes de serviceName (ni prefijo Promo_). Lo que varía es el tipo de envío, derivado de input.status + nonProductiveReason: define el visitDispatchKind (la etiqueta del envío en el Centro de datos) y el status textual del payload.

Tipo de envioUpload kindTipo de envío input.status visitDispatchKind status (payload) Quando é disparadoWhen it firesCuándo se dispara
InícioStartIniciostartedstartNot Completedrep inicia a visita (botão Iniciar visita, VisitStartGuard)rep starts the visit (Start visit button, VisitStartGuard)rep inicia la visita (botón Iniciar visita, VisitStartGuard)
FinalizaçãoFinishFinalizacióncompletedfinishCompletedrep finaliza a visita (pós-checklist/confirmação; tela de fim de visita; auto-fecho do ActiveVisitGuard)rep finishes the visit (post-checklist/confirm; visit-end screen; ActiveVisitGuard auto-close)rep finaliza la visita (post-checklist/confirmación; pantalla de fin de visita; auto-cierre del ActiveVisitGuard)
ImprodutivaNon-productiveImproductivacancelled / notStartednullNot CompletedNo Buy ou resolução de fim de jornada; nonProductiveReason preenchido → NonPrdReasonNo Buy or end-of-journey resolution; nonProductiveReason set → NonPrdReasonNo Buy o resolución de fin de jornada; nonProductiveReason lleno → NonPrdReason
AgendamentoSchedulingAgendamientoschedulednullNot Completedcriação de visita planejada em Retails (RetailsNotifier.createPlannedVisit) — sem visit, usa accountSfid/plannedDateplanned-visit creation in Retails (RetailsNotifier.createPlannedVisit) — no visit, uses accountSfid/plannedDatecreación de visita planificada en Retails (RetailsNotifier.createPlannedVisit) — sin visit, usa accountSfid/plannedDate

Regra do kind e do statusKind & status ruleRegla del kind y del status visitDispatchKind: started → start, completed → finish, _ → null. status do payload: "Completed" só quando !isNonProductive && status == completed; em qualquer outro caso, "Not Completed". isNonProductive = nonProductiveReason.trim().isNotEmpty. visitDispatchKind: started → start, completed → finish, _ → null. Payload status: "Completed" only when !isNonProductive && status == completed; in any other case, "Not Completed". isNonProductive = nonProductiveReason.trim().isNotEmpty. visitDispatchKind: started → start, completed → finish, _ → null. status del payload: "Completed" solo cuando !isNonProductive && status == completed; en cualquier otro caso, "Not Completed". isNonProductive = nonProductiveReason.trim().isNotEmpty.

07

Como é disparadoHow it's firedCómo se dispara

Início, finalização e improdutiva convergem para VisitContext._updateVisitStatus, que persiste o novo status da visita e monta o input. O notifier apenas reúne entities cruas + valores de ambiente; o builder é o dono único de todo rename, formatação de data e derivação wire. A cascata:Start, finish and non-productive all converge on VisitContext._updateVisitStatus, which persists the visit's new status and assembles the input. The notifier only gathers raw entities + ambient values; the builder is the sole owner of every rename, date formatting and wire derivation. The cascade:Inicio, finalización e improductiva convergen en VisitContext._updateVisitStatus, que persiste el nuevo status de la visita y arma el input. El notifier solo reúne entities crudas + valores de ambiente; el builder es el dueño único de todo rename, formateo de fecha y derivación wire. La cascada:

  • VisitContextnotifier · _updateVisitStatus
    • reúne entities cruas + ambientegathers raw entities + ambientreúne entities crudas + ambienteVisitUploadDispatcherPayloadInput
      • build()BuildVisitUploadDispatcherPayloadUseCasemonta o wireassembles the wirearma el wire
        • devolvereturnsdevuelveDispatcherEnvelope
          • SubmitVisitUploadUseCaseDispatcherOrchestratordispatch() — bifurca online/offlinedispatch() — online/offline forkdispatch() — bifurca online/offline
            • onlineonlineonlineDispatcherRepository → DispatcherGateway
              • sendTransactionBackendgRPC · Salesforce
            • offline (só visit)offline (visit only)offline (solo visit)_recordPending → fila pendingreenviado por flush() (seção 10)retried by flush() (section 10)reintentado por flush() (sección 10)

O input (entities cruas)The input (raw entities)El input (entities crudas)

VisitUploadDispatcherPayloadInput (Freezed). Carrega o dado como existe no domínio; nada de formato wire. O relógio chega como submittedAt (via DateTimeUtils.now()).VisitUploadDispatcherPayloadInput (Freezed). Carries data as it exists in the domain; no wire shaping. The clock arrives as submittedAt (via DateTimeUtils.now()).VisitUploadDispatcherPayloadInput (Freezed). Lleva el dato como existe en el dominio; nada de formato wire. El reloj llega como submittedAt (vía DateTimeUtils.now()).

CampoFieldCampoTipoTypeTipoPapelRoleRol
resourceResourceEntityrepresentante de vendas (cru — o builder deriva resourceId primary/secondary)sales rep (raw — the builder derives resourceId primary/secondary)representante de ventas (crudo — el builder deriva resourceId primary/secondary)
marketEndMarketmarketIso (market.name)
statusVisitStatusdirige o status wire e o visitDispatchKinddrives the wire status and the visitDispatchKinddirige el status wire y el visitDispatchKind
submittedAtDateTimerelógio — dateReference + fallback de Origdateclock — dateReference + Origdate fallbackreloj — dateReference + fallback de Origdate
sendsGranularGeolocationbool · falseEMC visitUploadConfig — liga o bloco de geolocalização granularEMC visitUploadConfig — toggles the granular geolocation blockEMC visitUploadConfig — activa el bloque de geolocalización granular
visitVisitEntity?a visita: sfid, accountData, startedAt/endedAt, visitDate, coordenadas, callTypeId, sequencethe visit: sfid, accountData, startedAt/endedAt, visitDate, coordinates, callTypeId, sequencela visita: sfid, accountData, startedAt/endedAt, visitDate, coordenadas, callTypeId, sequence
accountSfidString?varejo (fallback de retailerId quando não há visit — agendamento)retail (retailerId fallback when there's no visit — scheduling)punto de venta (fallback de retailerId cuando no hay visit — agendamiento)
plannedDateDateTime?data planejada (agendamento) → 1ª fonte de Origdateplanned date (scheduling) → 1st source of Origdatefecha planificada (agendamiento) → 1ª fuente de Origdate
latitude / longitudedouble?posição (LocationService) — fallback das coordenadas resolvidasposition (LocationService) — fallback for the resolved coordinatesposición (LocationService) — fallback de las coordenadas resueltas
nonProductiveReasonString?motivo de improdutividade → NonPrdReason; liga isNonProductivenon-productive reason → NonPrdReason; toggles isNonProductivemotivo de improductividad → NonPrdReason; activa isNonProductive
08

Payload (message)

O JSON serializado no campo message do request. Cada tabela tem 4 colunasCampo JSON · Tipo · Origem do Dado · Regra — e lista toda chave que o build() emite (1 chave raiz + 12 base + 10 granulares = 23). Campo, Tipo e Origem são código cru; só a Regra é prosa. Um exemplo (finalização/BR, geolocalização granular desligada) está em transaction_example.json, ao lado deste doc.The JSON serialized into the request's message field. Each table has 4 columnsJSON field · Type · Data source · Rule — and lists every key build() emits (1 root + 12 base + 10 granular = 23). Field, Type and Source are raw code; only Rule is prose. An example (finish/BR, granular geolocation off) sits in transaction_example.json, next to this doc.El JSON serializado en el campo message del request. Cada tabla tiene 4 columnasCampo JSON · Tipo · Origen del Dato · Regla — y lista toda clave que build() emite (1 raíz + 12 base + 10 granular = 23). Campo, Tipo y Origen son código crudo; solo la Regla es prosa. Un ejemplo (finalización/BR, geolocalización granular desactivada) está en transaction_example.json, junto a este doc.

Raiz do payloadPayload rootRaíz del payload

Campo JSONTipoTypeTipoOrigem do DadoData sourceOrigen del DatoRegraRuleRegla
VisitUploadDetailsarray[visitUploadDetail]array de um único objeto (o detalhe da visita)single-element array (the visit detail)array de un solo objeto (el detalle de la visita)
  • VisitUploadDetail objeto únicosingle objectobjeto único 12 campos basebase fieldscampos base
    Campo JSONTipoTypeTipoOrigem do DadoData sourceOrigen del DatoRegraRuleRegla
    visitIdstringvisit?.sfidSFID da visita; "" quando não há visit (agendamento)visit SFID; "" when there's no visit (scheduling)SFID de la visita; "" cuando no hay visit (agendamiento)
    retailerIdstringvisit?.accountData.sfid ?? input.accountSfidSFID do varejo; "" se ambos nulosretail SFID; "" if both nullSFID del punto de venta; "" si ambos nulos
    resourceIdstringresource.primaryResourceSfid / secondaryResourceSfidisPrimaryResource ? primary : secondaryisPrimaryResource ? primary : secondaryisPrimaryResource ? primary : secondary
    OrigdatestringoriginalDateplannedDate ?? tryParse(visit.visitDate) ?? submittedAt, formato yyyy-MM-ddplannedDate ?? tryParse(visit.visitDate) ?? submittedAt, yyyy-MM-dd formatplannedDate ?? tryParse(visit.visitDate) ?? submittedAt, formato yyyy-MM-dd
    notestringFixo: ""nunca populado (ver Pendências)never populated (see Pending)nunca poblado (ver Pendientes)
    statusstringCalculado"Completed" só se !isNonProductive && status == completed; senão "Not Completed""Completed" only if !isNonProductive && status == completed; else "Not Completed""Completed" solo si !isNonProductive && status == completed; si no "Not Completed"
    timeInstringvisit?.startedAtimprodutiva → ""; senão yyyy-MM-dd HH:mm:ss ("" se null)non-productive → ""; else yyyy-MM-dd HH:mm:ss ("" if null)improductiva → ""; si no yyyy-MM-dd HH:mm:ss ("" si null)
    timeOutstringvisit?.endedAtimprodutiva → ""; senão yyyy-MM-dd HH:mm:ss ("" se null)non-productive → ""; else yyyy-MM-dd HH:mm:ss ("" if null)improductiva → ""; si no yyyy-MM-dd HH:mm:ss ("" si null)
    latitudedouble_resolveLatitudegranular → accountData.latitude; senão por ação (ver Regras); fallback input.latitude ?? 0granular → accountData.latitude; else by action (see Rules); fallback input.latitude ?? 0granular → accountData.latitude; si no por acción (ver Reglas); fallback input.latitude ?? 0
    longitudedouble_resolveLongitudesimétrico a latitudesymmetric to latitudesimétrico a latitude
    NonPrdReasonstringnonProductiveReasonimprodutiva → reason.trim(); senão a string literal "null" (ver Pendências)non-productive → reason.trim(); else the literal string "null" (see Pending)improductiva → reason.trim(); si no la string literal "null" (ver Pendientes)
    marketIsostringinput.market.nameBR / CL / ZABR / CL / ZABR / CL / ZA
  • granular geolocation só quando sendsGranularGeolocationonly when sendsGranularGeolocationsolo cuando sendsGranularGeolocation 10 camposfieldscampos

    Bloco espalhado no mesmo objeto VisitUploadDetail quando o EMC (visitUploadConfig.sendsGranularGeolocation) está ligado. Coordenadas "não aplicáveis" saem como string vazia ""; ausentes saem como 0 (double) — campo de tipo misto.Block spread into the same VisitUploadDetail object when the EMC (visitUploadConfig.sendsGranularGeolocation) is on. "Not applicable" coordinates ship as an empty string ""; missing ones ship as 0 (double) — a mixed-type field.Bloque esparcido en el mismo objeto VisitUploadDetail cuando el EMC (visitUploadConfig.sendsGranularGeolocation) está activo. Coordenadas "no aplicables" salen como string vacía ""; ausentes salen como 0 (double) — campo de tipo mixto.

    Campo JSONTipoTypeTipoOrigem do DadoData sourceOrigen del DatoRegraRuleRegla
    callTypeIdstringvisit?.callTypeId"" se null"" if null"" si null
    planIdstringFixo: ""contrato · inertecontract · inertcontrato · inerte
    sequenceintvisit?.sequence0 se null0 if null0 si null
    typeOfCallstringFixo: ""contrato · inertecontract · inertcontrato · inerte
    startLatitudedouble | stringvisit?.startLatitudeimprodutiva → ""; senão startLatitude ?? 0non-productive → ""; else startLatitude ?? 0improductiva → ""; si no startLatitude ?? 0
    startlongitudedouble | stringvisit?.startLongitudeimprodutiva → ""; senão startLongitude ?? 0 (chave em minúsculas)non-productive → ""; else startLongitude ?? 0 (lowercase key)improductiva → ""; si no startLongitude ?? 0 (clave en minúsculas)
    endlatitudedouble | stringvisit?.endLatitudesó se finalizada (isFinished) → endLatitude ?? 0; senão ""only if finished (isFinished) → endLatitude ?? 0; else ""solo si finalizada (isFinished) → endLatitude ?? 0; si no ""
    endlongitudedouble | stringvisit?.endLongitudesó se finalizada → endLongitude ?? 0; senão ""only if finished → endLongitude ?? 0; else ""solo si finalizada → endLongitude ?? 0; si no ""
    noCalllatitudedouble | stringvisit?.noCallLatitudesó improdutiva → noCallLatitude ?? 0; senão ""non-productive only → noCallLatitude ?? 0; else ""solo improductiva → noCallLatitude ?? 0; si no ""
    noCalllongitudedouble | stringvisit?.noCallLongitudesó improdutiva → noCallLongitude ?? 0; senão ""non-productive only → noCallLongitude ?? 0; else ""solo improductiva → noCallLongitude ?? 0; si no ""

ExemploExampleEjemplo Um payload de finalização (BR, geolocalização granular desligada) está em docs/public/dispatcher/04_visit_upload/transaction_example.json — a forma exata serializada em message (sem wrapper gRPC). A finish payload (BR, granular geolocation off) is in docs/public/dispatcher/04_visit_upload/transaction_example.json — the exact shape serialized into message (no gRPC wrapper). Un payload de finalización (BR, geolocalización granular desactivada) está en docs/public/dispatcher/04_visit_upload/transaction_example.json — la forma exacta serializada en message (sin wrapper gRPC).

09

Regras de negócioBusiness rulesReglas de negocio

Coordenadas de topoTop-level coordinatesCoordenadas de nivel superior latitude · longitude

Os campos latitude/longitude de topo (sempre double) são resolvidos por _resolveLatitude/_resolveLongitude:The top-level latitude/longitude fields (always double) are resolved by _resolveLatitude/_resolveLongitude:Los campos latitude/longitude de nivel superior (siempre double) se resuelven por _resolveLatitude/_resolveLongitude:

  1. sendsGranularGeolocationvisit?.accountData.latitude ?? input.latitude ?? 0 (posição do varejo)visit?.accountData.latitude ?? input.latitude ?? 0 (retail position)visit?.accountData.latitude ?? input.latitude ?? 0 (posición del punto de venta)
  2. senão, improdutivaelse, non-productivesi no, improductivavisit?.noCallLatitude ?? input.latitude ?? 0visit?.noCallLatitude ?? input.latitude ?? 0visit?.noCallLatitude ?? input.latitude ?? 0
  3. senão, completedelse, completedsi no, completedvisit?.endLatitude ?? input.latitude ?? 0visit?.endLatitude ?? input.latitude ?? 0visit?.endLatitude ?? input.latitude ?? 0
  4. senão, startedelse, startedsi no, startedvisit?.startLatitude ?? input.latitude ?? 0visit?.startLatitude ?? input.latitude ?? 0visit?.startLatitude ?? input.latitude ?? 0
  5. senãootherwisesi noinput.latitude ?? 0input.latitude ?? 0input.latitude ?? 0
Geolocalização granularGranular geolocationGeolocalización granular _buildGranularGeolocation
  • O bloco só é espalhado no objeto quando sendsGranularGeolocation (flag do EMC). Sem ele, o objeto tem só os 12 campos base.The block is only spread into the object when sendsGranularGeolocation (EMC flag). Without it, the object has only the 12 base fields.El bloque solo se esparce en el objeto cuando sendsGranularGeolocation (flag del EMC). Sin él, el objeto tiene solo los 12 campos base.
  • isFinished = !isNonProductive && status == completed. As coordenadas de fim (endlatitude/endlongitude) só saem quando isFinished; as de início saem exceto em improdutiva; as de noCall só em improdutiva.isFinished = !isNonProductive && status == completed. The end coordinates (endlatitude/endlongitude) ship only when isFinished; the start ones ship except when non-productive; the noCall ones only when non-productive.isFinished = !isNonProductive && status == completed. Las coordenadas de fin (endlatitude/endlongitude) salen solo cuando isFinished; las de inicio salen excepto en improductiva; las de noCall solo en improductiva.
  • Sentinelas: _notApplicableCoordinate = "" (string vazia) e _missingCoordinate = 0 (double). Por isso as chaves granulares de coordenada são de tipo misto (string ou double) no wire.Sentinels: _notApplicableCoordinate = "" (empty string) and _missingCoordinate = 0 (double). That's why the granular coordinate keys are mixed-type (string or double) on the wire.Centinelas: _notApplicableCoordinate = "" (string vacía) y _missingCoordinate = 0 (double). Por eso las claves granulares de coordenada son de tipo mixto (string o double) en el wire.
  • Note a inconsistência de casing das chaves de contrato: startLatitude (camelCase) vs startlongitude/endlatitude/noCalllatitude (minúsculas) — mantidas verbatim do backend.Note the contract-key casing inconsistency: startLatitude (camelCase) vs startlongitude/endlatitude/noCalllatitude (lowercase) — kept verbatim from the backend.Nota la inconsistencia de casing de las claves de contrato: startLatitude (camelCase) vs startlongitude/endlatitude/noCalllatitude (minúsculas) — mantenidas verbatim del backend.
Datas e horáriosDates & timestampsFechas y horarios Origdate · timeIn/timeOut · dateReference
  • Origdate: plannedDate ?? DateTimeUtils.tryParse(visit.visitDate) ?? submittedAt, formatado em yyyy-MM-dd (default de DateTimeUtils.formatDate). Note que visit.visitDate é String? — parseado por tryParse.Origdate: plannedDate ?? DateTimeUtils.tryParse(visit.visitDate) ?? submittedAt, formatted as yyyy-MM-dd (DateTimeUtils.formatDate default). Note visit.visitDate is a String? — parsed via tryParse.Origdate: plannedDate ?? DateTimeUtils.tryParse(visit.visitDate) ?? submittedAt, formateado en yyyy-MM-dd (default de DateTimeUtils.formatDate). Nota que visit.visitDate es String? — parseado por tryParse.
  • timeIn/timeOut: DateFormatType.isoDateTime = yyyy-MM-dd HH:mm:ss (separador espaço, não T). "" em improdutiva ou quando o timestamp é null.timeIn/timeOut: DateFormatType.isoDateTime = yyyy-MM-dd HH:mm:ss (space separator, not T). "" when non-productive or the timestamp is null.timeIn/timeOut: DateFormatType.isoDateTime = yyyy-MM-dd HH:mm:ss (separador espacio, no T). "" en improductiva o cuando el timestamp es null.
  • dateReference (envelope, não payload): formatDate(submittedAt) = yyyy-MM-dd.dateReference (envelope, not payload): formatDate(submittedAt) = yyyy-MM-dd.dateReference (envelope, no payload): formatDate(submittedAt) = yyyy-MM-dd.
Conta do envelopeEnvelope accountCuenta del envelope DispatchAccountEntity

Além do payload, o builder preenche o account do envelope (usado pelo tracking da Central de dados, não vai no message): DispatchAccountEntity(sfid: retailerSfid, sapCode: visit?.accountData.customerCode ?? "", name: visit?.accountData.name ?? "").Beyond the payload, the builder fills the envelope's account (used by the Data Center tracking, not sent in message): DispatchAccountEntity(sfid: retailerSfid, sapCode: visit?.accountData.customerCode ?? "", name: visit?.accountData.name ?? "").Además del payload, el builder completa el account del envelope (usado por el tracking del Centro de datos, no va en message): DispatchAccountEntity(sfid: retailerSfid, sapCode: visit?.accountData.customerCode ?? "", name: visit?.accountData.name ?? "").

10

Fila offline (o que torna visit especial)Offline queue (what makes visit special)Cola offline (lo que hace especial a visit)

visit é o único DispatcherType com fila offline automática. Não há flag no enum (nenhum queuesOffline) — é um if hardcoded no DispatcherOrchestrator.dispatch:visit is the only DispatcherType with an automatic offline queue. There's no enum flag (no queuesOffline) — it's a hardcoded if in DispatcherOrchestrator.dispatch:visit es el único DispatcherType con cola offline automática. No hay flag en el enum (ningún queuesOffline) — es un if hardcoded en DispatcherOrchestrator.dispatch:

DispatcherOrchestrator.dispatch
O ramoThe branchLa rama

if (isOffline && envelope.type == DispatcherType.visit) { _recordPending(...); return Error(NetworkFailure); }

offline + visit → grava DispatchTransactionStatus.pending (com o payload inteiro guardado para replay) e retorna NetworkFailure sem tentar a rede.offline + visit → stores DispatchTransactionStatus.pending (with the whole payload kept for replay) and returns NetworkFailure without attempting the network.offline + visit → graba DispatchTransactionStatus.pending (con el payload entero guardado para replay) y retorna NetworkFailure sin intentar la red.

SituaçãoSituationSituación Status gravadoStored statusStatus guardado Reenvio automático?Auto-retried?¿Reenvío automático?
visit offlinevisit offlinevisit offlinependingSimflush() drena getPending() (só pending)Yesflush() drains getPending() (pending only)flush() drena getPending() (solo pending)
qualquer outro tipo offlineany other type offlinecualquier otro tipo offlineerrorNão — só reenvio manual ou drainUnsent() no início de sessãoNo — only manual resend or drainUnsent() at session startNo — solo reenvío manual o drainUnsent() al inicio de sesión
Como a fila é drenadaHow the queue is drainedCómo se drena la cola flush · drainUnsent · resend
  • flush() — dreno automático, dirigido por evento (não há timer): dispara na transição offline→online (listener de connectivityStatusProvider) e uma vez no boot do orchestrator (instanciado em main_common.dart, keepAlive). Lê getPending() (FIFO por submittedAt), re-processa cada registro com attemptCount + 1; para no 1º NetworkFailure (rede caiu de novo). Máx 5 tentativas por registro (_maxFlushAttempts).flush() — automatic, event-driven drain (no timer): fires on the offline→online transition (a connectivityStatusProvider listener) and once at orchestrator boot (instantiated in main_common.dart, keepAlive). Reads getPending() (FIFO by submittedAt), re-processes each record with attemptCount + 1; stops at the 1st NetworkFailure (network dropped again). Max 5 attempts per record (_maxFlushAttempts).flush() — drenado automático, dirigido por evento (no hay timer): dispara en la transición offline→online (listener de connectivityStatusProvider) y una vez en el boot del orchestrator (instanciado en main_common.dart, keepAlive). Lee getPending() (FIFO por submittedAt), reprocesa cada registro con attemptCount + 1; para en el 1er NetworkFailure (la red cayó de nuevo). Máx 5 intentos por registro (_maxFlushAttempts).
  • drainUnsent() — dreno mais amplo, chamado no início de sessão (session_service.dart). Lê getUnsent() = pending OU error de rede (ackStatus == 0). É o único caminho que também re-tenta os error não-visit.drainUnsent() — broader drain, called at session start (session_service.dart). Reads getUnsent() = pending OR network error (ackStatus == 0). It's the only path that also retries non-visit error records.drainUnsent() — drenado más amplio, llamado al inicio de sesión (session_service.dart). Lee getUnsent() = pending O error de red (ackStatus == 0). Es el único camino que también reintenta los error no-visit.
  • resend(localId) — reenvio manual, por registro, disparado pela UI da Central de dados.resend(localId) — manual, per-record resend, fired from the Data Center UI.resend(localId) — reenvío manual, por registro, disparado desde la UI del Centro de datos.
Idempotência / replayIdempotency / replayIdempotencia / replay tid · backendTransactionId · ack
  • tid = record.backendTransactionId. No 1º envio, 0; no sucesso, o orchestrator persiste ack.transactionId em backendTransactionId; no replay, esse id volta como tid, permitindo o backend deduplicar.tid = record.backendTransactionId. On the 1st send, 0; on success the orchestrator persists ack.transactionId to backendTransactionId; on replay that id returns as tid, letting the backend dedupe.tid = record.backendTransactionId. En el 1er envío, 0; en el éxito el orchestrator persiste ack.transactionId en backendTransactionId; en el replay ese id vuelve como tid, dejando al backend deduplicar.
  • Ack: status 0 ou 5 → success; status 1 → duplicate (tratado como success-equivalent → sai da fila); qualquer outro → error.Ack: status 0 or 5 → success; status 1 → duplicate (treated as success-equivalent → leaves the queue); anything else → error.Ack: status 0 o 5 → success; status 1 → duplicate (tratado como success-equivalent → sale de la cola); cualquier otro → error.
  • transactionReference = visit.sfid ?? retailerSfid — chave de negócio estável ao lado do tid. No sucesso, o payload guardado é nulado.transactionReference = visit.sfid ?? retailerSfid — stable business key alongside tid. On success the stored payload is nulled.transactionReference = visit.sfid ?? retailerSfid — clave de negocio estable junto al tid. En el éxito el payload guardado se anula.

FontesSourcesFuentes DispatcherOrchestrator (dispatch/flush/drainUnsent/resend/_recordPending) + DispatchTransactionLocalDataSource (getPending/getUnsent) + DispatcherGateway (guard offline + tid) + DispatchTransactionStatus. DispatcherOrchestrator (dispatch/flush/drainUnsent/resend/_recordPending) + DispatchTransactionLocalDataSource (getPending/getUnsent) + DispatcherGateway (offline guard + tid) + DispatchTransactionStatus. DispatcherOrchestrator (dispatch/flush/drainUnsent/resend/_recordPending) + DispatchTransactionLocalDataSource (getPending/getUnsent) + DispatcherGateway (guard offline + tid) + DispatchTransactionStatus.

11

Pendências / roadmapPending / roadmapPendientes / roadmap

O que o builder ainda não preenche, ou envia inerte/quirk, documentado fiel ao estado atual do código (nunca descrito como se já existisse):What the builder does not yet fill, or ships inert/quirky, documented faithfully to the current code state (never described as already existing):Lo que el builder aún no completa, o envía inerte/quirk, documentado fiel al estado actual del código (nunca descrito como si ya existiera):

Não portado / pendente / quirkNot ported / pending / quirkNo portado / pendiente / quirk

  • NonPrdReason em visita produtiva sobe como a string literal "null" (não JSON null) — sentinela de contrato _emptyReason = "null".NonPrdReason on a productive visit ships as the literal string "null" (not JSON null) — the _emptyReason = "null" contract sentinel.NonPrdReason en una visita productiva sube como la string literal "null" (no JSON null) — centinela de contrato _emptyReason = "null".
  • note: sempre "" — nunca populado por nenhum caminho.note: always "" — never populated by any path.note: siempre "" — nunca poblado por ningún camino.
  • planId e typeOfCall (bloco granular): fixos "" — placeholders inertes do contrato.planId and typeOfCall (granular block): fixed "" — inert contract placeholders.planId y typeOfCall (bloque granular): fijos "" — placeholders inertes del contrato.
  • Coordenadas granulares "não aplicáveis" saem como string vazia num campo que também carrega double — tipo misto no wire (backend precisa tolerar ambos)."Not applicable" granular coordinates ship as an empty string in a field that also carries a double — mixed-type on the wire (the backend must tolerate both).Coordenadas granulares "no aplicables" salen como string vacía en un campo que también lleva un double — tipo mixto en el wire (el backend debe tolerar ambos).
  • O caminho de agendamento (RetailsNotifier.createPlannedVisit, status scheduled) reusa VisitUploadAPI com visitDispatchKind = null, visitId = "" e status = "Not Completed" — é uma criação de visita planejada, não um início/fim.The scheduling path (RetailsNotifier.createPlannedVisit, status scheduled) reuses VisitUploadAPI with visitDispatchKind = null, visitId = "" and status = "Not Completed" — it's a planned-visit creation, not a start/finish.El camino de agendamiento (RetailsNotifier.createPlannedVisit, status scheduled) reusa VisitUploadAPI con visitDispatchKind = null, visitId = "" y status = "Not Completed" — es una creación de visita planificada, no un inicio/fin.
  • Transporte: deviceUuid vai como literal provisório no gateway (pendência conhecida do Dispatcher, comum a todas as transações).Transport: deviceUuid ships as a provisional literal in the gateway (known Dispatcher pending item, common to all transactions).Transporte: deviceUuid va como literal provisional en el gateway (pendiente conocido del Dispatcher, común a todas las transacciones).

MercadosMarketsMercados

A disponibilidade vem de DispatcherType.visit.enabledMarkets = [BR, CL, ZA]. AR/PY/PE não têm dispatcher de visita (config PANGEA mínima). A geolocalização granular é dirigida por mercado pelo EMC (visitUploadConfig.sendsGranularGeolocation).Availability comes from DispatcherType.visit.enabledMarkets = [BR, CL, ZA]. AR/PY/PE have no visit dispatcher (minimal PANGEA config). Granular geolocation is driven per market by the EMC (visitUploadConfig.sendsGranularGeolocation).La disponibilidad viene de DispatcherType.visit.enabledMarkets = [BR, CL, ZA]. AR/PY/PE no tienen dispatcher de visita (config PANGEA mínima). La geolocalización granular se dirige por mercado por el EMC (visitUploadConfig.sendsGranularGeolocation).

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

Geolocalização granularGranular geolocationGeolocalización granular O bloco granular (10 campos) sobe apenas nos mercados cujo EMC liga visitUploadConfig.sendsGranularGeolocation. Onde está desligado, o payload leva só os 12 campos base — o exemplo ao lado deste doc reflete esse caso. The granular block (10 fields) ships only in markets whose EMC turns on visitUploadConfig.sendsGranularGeolocation. Where it's off, the payload carries only the 12 base fields — the example next to this doc reflects that case. El bloque granular (10 campos) sube solo en los mercados cuyo EMC activa visitUploadConfig.sendsGranularGeolocation. Donde está desactivado, el payload lleva solo los 12 campos base — el ejemplo junto a este doc refleja ese caso.

AR · PY · PE Existem como mercados do app (config PANGEA mínima), mas não têm dispatcher de visitavisit não os lista em enabledMarkets. O envio de visita não é disparado nesses mercados. They exist as app markets (minimal PANGEA config), but have no visit dispatchervisit doesn't list them in enabledMarkets. Visit upload is not fired in these markets. Existen como mercados de la app (config PANGEA mínima), pero no tienen dispatcher de visitavisit no los lista en enabledMarkets. El envío de visita no se dispara en estos mercados.