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.
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.
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:
- 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).
- 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.
- 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.
- 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.
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.
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 + visit → pending, reenviado por flush() na volta da conexão. Ver seção 10.The only DispatcherType with an offline queue: offline + visit → pending, retried by flush() when the connection returns. See section 10.Único DispatcherType con cola offline: offline + visit → pending, 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.
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.
sendTransactionunaryrpc sendTransaction(InboxTransactionRequest) returns (InboxTransactionReply)
path /mn.bat.conectarep.dispatcher.DispatcherConectaRepService/sendTransaction
InboxTransactionRequestendpointstring· #1 · endpoint alvo —type.destination.value(Salesforce)target endpoint —type.destination.value(Salesforce)endpoint destino —type.destination.value(Salesforce)serviceNamestring· #2 · discriminador —VisitUploadAPI(nunca com prefixoPromo_: o builder chamaresolveServiceName(hasPromotion: false))discriminator —VisitUploadAPI(neverPromo_-prefixed: the builder callsresolveServiceName(hasPromotion: false))discriminador —VisitUploadAPI(nunca con prefijoPromo_: el builder llamaresolveServiceName(hasPromotion: false))dateReferencestring· #3 ·AAAA-MM-DDdeinput.submittedAtYYYY-MM-DDofinput.submittedAtAAAA-MM-DDdeinput.submittedAttransactionReferencestring· #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)usernamestring· #5messagestring· #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/deviceVersionstring· #7–#10 · dados do dispositivo (deviceUuidliteral provisório — ver Pendências)device data (deviceUuidprovisional literal — see Pending)datos del dispositivo (deviceUuidliteral provisional — ver Pendientes)tidint64· #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)
InboxTransactionReplystatusint32· #1 · status do ack (0/5= sucesso;1= duplicado)ack status (0/5= success;1= duplicate)status del ack (0/5= éxito;1= duplicado)messagestring· #2 · mensagem do backendbackend messagemensaje del backendtransactionIdint32· #3 · id atribuído pelo backend → persistido embackendTransactionId(otiddo próximo replay)backend-assigned id → persisted tobackendTransactionId(thetidof the next replay)id asignado por el backend → persistido enbackendTransactionId(eltiddel 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.
serviceName e tipos de envioserviceName & upload kindsserviceName y tipos de envío
Há um único serviceName — VisitUploadAPI — 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 serviceName — VisitUploadAPI — 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 serviceName — VisitUploadAPI — 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ícioStartInicio | started | start | Not Completed | rep 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ón | completed | finish | Completed | rep 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-productiveImproductiva | cancelled / notStarted | null | Not Completed | No 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 |
| AgendamentoSchedulingAgendamiento | scheduled | null | Not Completed | criaçã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.
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)
- onlineonlineonlineDispatcherRepository → DispatcherGateway
- SubmitVisitUploadUseCaseDispatcherOrchestratordispatch() — bifurca online/offlinedispatch() — online/offline forkdispatch() — bifurca online/offline
- devolvereturnsdevuelveDispatcherEnvelope
- build()BuildVisitUploadDispatcherPayloadUseCasemonta o wireassembles the wirearma el wire
- reúne entities cruas + ambientegathers raw entities + ambientreúne entities crudas + ambienteVisitUploadDispatcherPayloadInput
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()).
| CampoFieldCampo | TipoTypeTipo | PapelRoleRol |
|---|---|---|
resource | ResourceEntity | representante 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) |
market | EndMarket | → marketIso (market.name) |
status | VisitStatus | dirige o status wire e o visitDispatchKinddrives the wire status and the visitDispatchKinddirige el status wire y el visitDispatchKind |
submittedAt | DateTime | relógio — dateReference + fallback de Origdateclock — dateReference + Origdate fallbackreloj — dateReference + fallback de Origdate |
sendsGranularGeolocation | bool · false | EMC visitUploadConfig — liga o bloco de geolocalização granularEMC visitUploadConfig — toggles the granular geolocation blockEMC visitUploadConfig — activa el bloque de geolocalización granular |
visit | VisitEntity? | 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 |
accountSfid | String? | 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) |
plannedDate | DateTime? | data planejada (agendamento) → 1ª fonte de Origdateplanned date (scheduling) → 1st source of Origdatefecha planificada (agendamiento) → 1ª fuente de Origdate |
latitude / longitude | double? | posição (LocationService) — fallback das coordenadas resolvidasposition (LocationService) — fallback for the resolved coordinatesposición (LocationService) — fallback de las coordenadas resueltas |
nonProductiveReason | String? | motivo de improdutividade → NonPrdReason; liga isNonProductivenon-productive reason → NonPrdReason; toggles isNonProductivemotivo de improductividad → NonPrdReason; activa isNonProductive |
Payload (message)
O JSON serializado no campo message do request. Cada tabela tem 4 colunas — Campo 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 columns — JSON 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 columnas — Campo 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 JSON | TipoTypeTipo | Origem do DadoData sourceOrigen del Dato | RegraRuleRegla |
|---|---|---|---|
VisitUploadDetails | array | [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 JSON TipoTypeTipo Origem do DadoData sourceOrigen del Dato RegraRuleRegla visitIdstring visit?.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)retailerIdstring visit?.accountData.sfid ?? input.accountSfidSFID do varejo; ""se ambos nulosretail SFID;""if both nullSFID del punto de venta;""si ambos nulosresourceIdstring resource.primaryResourceSfid/secondaryResourceSfidisPrimaryResource ? primary : secondaryisPrimaryResource ? primary : secondaryisPrimaryResource ? primary : secondaryOrigdatestring originalDateplannedDate ?? tryParse(visit.visitDate) ?? submittedAt, formatoyyyy-MM-ddplannedDate ?? tryParse(visit.visitDate) ?? submittedAt,yyyy-MM-ddformatplannedDate ?? tryParse(visit.visitDate) ?? submittedAt, formatoyyyy-MM-ddnotestring Fixo: ""nunca populado (ver Pendências)never populated (see Pending)nunca poblado (ver Pendientes) statusstring Calculado"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"timeInstring visit?.startedAtimprodutiva → ""; senãoyyyy-MM-dd HH:mm:ss(""se null)non-productive →""; elseyyyy-MM-dd HH:mm:ss(""if null)improductiva →""; si noyyyy-MM-dd HH:mm:ss(""si null)timeOutstring visit?.endedAtimprodutiva → ""; senãoyyyy-MM-dd HH:mm:ss(""se null)non-productive →""; elseyyyy-MM-dd HH:mm:ss(""if null)improductiva →""; si noyyyy-MM-dd HH:mm:ss(""si null)latitudedouble _resolveLatitudegranular → accountData.latitude; senão por ação (ver Regras); fallbackinput.latitude ?? 0granular →accountData.latitude; else by action (see Rules); fallbackinput.latitude ?? 0granular →accountData.latitude; si no por acción (ver Reglas); fallbackinput.latitude ?? 0longitudedouble _resolveLongitudesimétrico a latitudesymmetric tolatitudesimétrico alatitudeNonPrdReasonstring nonProductiveReasonimprodutiva → 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)marketIsostring input.market.nameBR/CL/ZABR/CL/ZABR/CL/ZAgranular geolocation só quando
sendsGranularGeolocationonly whensendsGranularGeolocationsolo cuandosendsGranularGeolocation10 camposfieldscamposBloco espalhado no mesmo objeto
VisitUploadDetailquando o EMC (visitUploadConfig.sendsGranularGeolocation) está ligado. Coordenadas "não aplicáveis" saem como string vazia""; ausentes saem como0(double) — campo de tipo misto.Block spread into the sameVisitUploadDetailobject when the EMC (visitUploadConfig.sendsGranularGeolocation) is on. "Not applicable" coordinates ship as an empty string""; missing ones ship as0(double) — a mixed-type field.Bloque esparcido en el mismo objetoVisitUploadDetailcuando el EMC (visitUploadConfig.sendsGranularGeolocation) está activo. Coordenadas "no aplicables" salen como string vacía""; ausentes salen como0(double) — campo de tipo mixto.Campo JSON TipoTypeTipo Origem do DadoData sourceOrigen del Dato RegraRuleRegla callTypeIdstring visit?.callTypeId""se null""if null""si nullplanIdstring Fixo: ""contrato · inertecontract · inertcontrato · inerte sequenceint visit?.sequence0se null0if null0si nulltypeOfCallstring Fixo: ""contrato · inertecontract · inertcontrato · inerte startLatitudedouble | string visit?.startLatitudeimprodutiva → ""; senãostartLatitude ?? 0non-productive →""; elsestartLatitude ?? 0improductiva →""; si nostartLatitude ?? 0startlongitudedouble | string visit?.startLongitudeimprodutiva → ""; senãostartLongitude ?? 0(chave em minúsculas)non-productive →""; elsestartLongitude ?? 0(lowercase key)improductiva →""; si nostartLongitude ?? 0(clave en minúsculas)endlatitudedouble | string visit?.endLatitudesó se finalizada ( isFinished) →endLatitude ?? 0; senão""only if finished (isFinished) →endLatitude ?? 0; else""solo si finalizada (isFinished) →endLatitude ?? 0; si no""endlongitudedouble | string visit?.endLongitudesó se finalizada → endLongitude ?? 0; senão""only if finished →endLongitude ?? 0; else""solo si finalizada →endLongitude ?? 0; si no""noCalllatitudedouble | string visit?.noCallLatitudesó improdutiva → noCallLatitude ?? 0; senão""non-productive only →noCallLatitude ?? 0; else""solo improductiva →noCallLatitude ?? 0; si no""noCalllongitudedouble | string visit?.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).
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:
sendsGranularGeolocation→visit?.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)- senão, improdutivaelse, non-productivesi no, improductiva→
visit?.noCallLatitude ?? input.latitude ?? 0→visit?.noCallLatitude ?? input.latitude ?? 0→visit?.noCallLatitude ?? input.latitude ?? 0 - senão,
completedelse,completedsi no,completed→visit?.endLatitude ?? input.latitude ?? 0→visit?.endLatitude ?? input.latitude ?? 0→visit?.endLatitude ?? input.latitude ?? 0 - senão,
startedelse,startedsi no,started→visit?.startLatitude ?? input.latitude ?? 0→visit?.startLatitude ?? input.latitude ?? 0→visit?.startLatitude ?? input.latitude ?? 0 - senãootherwisesi no→
input.latitude ?? 0→input.latitude ?? 0→input.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 whensendsGranularGeolocation(EMC flag). Without it, the object has only the 12 base fields.El bloque solo se esparce en el objeto cuandosendsGranularGeolocation(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 quandoisFinished; 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 whenisFinished; 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 cuandoisFinished; 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) vsstartlongitude/endlatitude/noCalllatitude(minúsculas) — mantidas verbatim do backend.Note the contract-key casing inconsistency:startLatitude(camelCase) vsstartlongitude/endlatitude/noCalllatitude(lowercase) — kept verbatim from the backend.Nota la inconsistencia de casing de las claves de contrato:startLatitude(camelCase) vsstartlongitude/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 emyyyy-MM-dd(default deDateTimeUtils.formatDate). Note quevisit.visitDateéString?— parseado portryParse.Origdate:plannedDate ?? DateTimeUtils.tryParse(visit.visitDate) ?? submittedAt, formatted asyyyy-MM-dd(DateTimeUtils.formatDatedefault). Notevisit.visitDateis aString?— parsed viatryParse.Origdate:plannedDate ?? DateTimeUtils.tryParse(visit.visitDate) ?? submittedAt, formateado enyyyy-MM-dd(default deDateTimeUtils.formatDate). Nota quevisit.visitDateesString?— parseado portryParse.timeIn/timeOut:DateFormatType.isoDateTime=yyyy-MM-dd HH:mm:ss(separador espaço, nãoT).""em improdutiva ou quando o timestamp é null.timeIn/timeOut:DateFormatType.isoDateTime=yyyy-MM-dd HH:mm:ss(space separator, notT).""when non-productive or the timestamp is null.timeIn/timeOut:DateFormatType.isoDateTime=yyyy-MM-dd HH:mm:ss(separador espacio, noT).""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 ?? "").
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.dispatchif (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 offline | pending | Sim — flush() drena getPending() (só pending)Yes — flush() drains getPending() (pending only)Sí — flush() drena getPending() (solo pending) |
| qualquer outro tipo offlineany other type offlinecualquier otro tipo offline | error | Nã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 deconnectivityStatusProvider) e uma vez no boot do orchestrator (instanciado emmain_common.dart,keepAlive). LêgetPending()(FIFO porsubmittedAt), re-processa cada registro comattemptCount + 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 (aconnectivityStatusProviderlistener) and once at orchestrator boot (instantiated inmain_common.dart,keepAlive). ReadsgetPending()(FIFO bysubmittedAt), re-processes each record withattemptCount + 1; stops at the 1stNetworkFailure(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 deconnectivityStatusProvider) y una vez en el boot del orchestrator (instanciado enmain_common.dart,keepAlive). LeegetPending()(FIFO porsubmittedAt), reprocesa cada registro conattemptCount + 1; para en el 1erNetworkFailure(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()=pendingOUerrorde rede (ackStatus == 0). É o único caminho que também re-tenta oserrornão-visit.drainUnsent()— broader drain, called at session start (session_service.dart). ReadsgetUnsent()=pendingOR networkerror(ackStatus == 0). It's the only path that also retries non-visiterrorrecords.drainUnsent()— drenado más amplio, llamado al inicio de sesión (session_service.dart). LeegetUnsent()=pendingOerrorde red (ackStatus == 0). Es el único camino que también reintenta loserrorno-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 persisteack.transactionIdembackendTransactionId; no replay, esse id volta comotid, permitindo o backend deduplicar.tid=record.backendTransactionId. On the 1st send,0; on success the orchestrator persistsack.transactionIdtobackendTransactionId; on replay that id returns astid, letting the backend dedupe.tid=record.backendTransactionId. En el 1er envío,0; en el éxito el orchestrator persisteack.transactionIdenbackendTransactionId; en el replay ese id vuelve comotid, 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 dotid. No sucesso, opayloadguardado é nulado.transactionReference=visit.sfid ?? retailerSfid— stable business key alongsidetid. On success the storedpayloadis nulled.transactionReference=visit.sfid ?? retailerSfid— clave de negocio estable junto altid. En el éxito elpayloadguardado 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.
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
NonPrdReasonem visita produtiva sobe como a string literal"null"(não JSONnull) — sentinela de contrato_emptyReason = "null".NonPrdReasonon a productive visit ships as the literal string"null"(not JSONnull) — the_emptyReason = "null"contract sentinel.NonPrdReasonen una visita productiva sube como la string literal"null"(no JSONnull) — 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.planIdetypeOfCall(bloco granular): fixos""— placeholders inertes do contrato.planIdandtypeOfCall(granular block): fixed""— inert contract placeholders.planIdytypeOfCall(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, statusscheduled) reusaVisitUploadAPIcomvisitDispatchKind = null,visitId = ""estatus = "Not Completed"— é uma criação de visita planejada, não um início/fim.The scheduling path (RetailsNotifier.createPlannedVisit, statusscheduled) reusesVisitUploadAPIwithvisitDispatchKind = null,visitId = ""andstatus = "Not Completed"— it's a planned-visit creation, not a start/finish.El camino de agendamiento (RetailsNotifier.createPlannedVisit, statusscheduled) reusaVisitUploadAPIconvisitDispatchKind = null,visitId = ""ystatus = "Not Completed"— es una creación de visita planificada, no un inicio/fin. - Transporte:
deviceUuidvai como literal provisório no gateway (pendência conhecida do Dispatcher, comum a todas as transações).Transport:deviceUuidships as a provisional literal in the gateway (known Dispatcher pending item, common to all transactions).Transporte:deviceUuidva 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).
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 visita — visit 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 dispatcher — visit 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 visita — visit no los lista en enabledMarkets. El envío de visita no se dispara en estos mercados.