14-16 Mar 2026
Logica do Sistema

Ofertas — Ciclo de Vida Completo

Secao 1

Entidades e Propriedades

OfertaBase (abstrata) / Oferta (concreta) Arquivo: cargo_fleet.Domain/Ofertas/Oferta.cs e Oferta.Extended.cs

Herda de FullAuditedAggregateRoot — possui CreationTime, CreatorId, LastModificationTime, LastModifierId, IsDeleted, DeletionTime, DeleterId.

PropriedadeTipoDescricao
TenantIdGuid?Multitenancy ABP
TipoTipoRotaEnumSpot ou Contrato
DataDateTimeData/hora de inclusao na Galileu
DataRemessaDateOnly?Data planejada da remessa
DataPrevisaoColetaDateTime?Previsao de coleta
DataPrevisaoEntregaDateTime?Previsao de entrega
CodigoB100string (obrigatorio)Codigo da nota fiscal (B100)
Origemstring?Cidade no formato "NOME/BRA"
Destinostring?Cidade no formato "NOME/BRA"
TipoCargaTipoCargaEnumTipo da carga
TipoOperacaoTipoOperacaoEnumInclui TrocaDeNf (TNF)
Valordecimal?Preco acordado
Statusstring (obrigatorio)Espelha status da Galileu
PontoTNFstring?Cidade de troca de NF
CogProgColetaintID da programacao de coleta na Galileu
TryAceiteboolSistema tentou aceitar esta oferta
CouldNotBeAcceptedByRouteboolOferta nao pde ser aceita por limitacao de rota
PrecoAtualizadoNoGalileubool?Flag de idempotencia para atualizacao de preco
IsCargaAdicionalbool?null = nao determinado; true/false apos verificacao
PrecoAdicionaldecimal?Preco da perna 2 (TNF)
OrigemCidadeIdint?FK para tabela Cidades
DestinoCidadeIdint?FK para tabela Cidades
PontoTnfCidadeIdint?FK para tabela Cidades

Metodos de dominio em Oferta.Extended.cs:

  • AtualizarParaAceite(tipo, isCargaAdicional, dataPrevisaoColeta, dataRemessa, dataPrevisaoEntrega, codigoB100, status) (linha 87): atualiza multiplas propriedades atomicamente antes do aceite.
  • IsTnf() (linha 105): retorna true se TipoOperacao == TipoOperacaoEnum.TrocaDeNf.

Status possiveis (espelhados da Galileu): PENDENTE, ACEITA, EMBARQUE EMITIDO, CANCELADO, DECLINADA, RECUSA LEILAO, RECUSADO, CANCELADO_POR_ACEITE, CANCELADO_PELA_CENTRAL, RECUSADO_APOS_ACEITE, AGUARDANDO_CONFIRMACAO, EXPIRADA.


Fluxo de Criacao (ProcessarOfertasWorker → CriarOfertasJob)

Worker: cargo_fleet.HttpApi.Host/BackgroundJobs/ProcessarOfertasWorker.cs

Passo 1 — DoWorkAsync:

  • Busca lista de todos os tenants via ITenantRepository.
  • Para cada tenant ativo (ITenantManager.IsActiveAsync), adiciona task em lista.
  • Executa Task.WhenAll(tasks) — todos os tenants em paralelo, sem semaforo de limite.
  • Timer.Period = 1 (1ms) — worker dispara continuamente, mas o delay interno de 5 minutos por tenant controla o ritmo real.

Passo 2 — ProcessarTenant(tenantId, tenantName):

  • Altera contexto para o tenant com ICurrentTenant.Change.
  • Verifica se existem credenciais Galileu (IGalileuCredentialRepository.GetCountAsync). Se zero, pula.
  • Chama ProcessarOfertasJob.ProcessarOfertasAsync().
  • Em caso de sucesso: registra ApiStatusEnum.Online.
  • Em HttpRequestException ou AuthenticationException: registra ApiStatusEnum.Offline.
  • Implementa intervalo minimo de 5 minutos entre ciclos por tenant via Task.Delay.

Passo 3 — ProcessarOfertasJob (Application/BackgroundJobs/ProcessarOfertasJob.cs):

  • Autentica na Galileu via GalileuApiService.AuthenticateWithExistingCredentialsAsync().
  • Executa CriarOfertasJob.ExecuteAsync(token).
  • Executa AceitarOfertasJob.ExecuteAsync(token).

Passo 4 — CriarOfertasJob (Application/BackgroundJobs/CriarOfertasJob.cs):

OfertasCriacaoFetcher.ObterParaCriacaoAsync(token):

  • Janela de busca: ultimas 24h (agora -1 dia ate agora).
  • Chama GalileuApiService.ListarOfertasAsync com endpoint rpc/transportador-api/listarOfertas/.
  • Se a resposta tem 200+ itens (limite da API): divide recursivamente o intervalo em dois subintervalos ate intervalo minimo de 1 hora.
  • Filtra apenas itens com Status == "PENDENTE" e deduplica por CodProgColeta.
  • Retorna IReadOnlyList<PayloadItem>.

OfertasCriacaoPreparationService.PrepararOfertasParaCriacaoAsync(payloadItemsPendentes):

  • Carrega do banco todas as ofertas cujos CogProgColeta ja existem.
  • Filtra somente as que NAO existem no banco (novidades).
  • Normaliza via GalileuOfertaNormalizer.NormalizePayloadItemOfertaGalileuNormalized.

OfertasCriacaoWriter.CriarOfertasAsync(ofertasParaCriar):

  • Para cada oferta: se Origem == null || Destino == null, pula.
  • Chama OfertaManager.CreateAsync com tipo: TipoRotaEnum.Spot (hardcoded), tryAceite: false, valor: null, isCargaAdicional: null.
  • Tudo em uma unica UoW transacional.

Fluxo de Aceite Automatico (AceitarOfertasJob)

Arquivo: cargo_fleet.Application/BackgroundJobs/AceitarOfertasJob.cs

Passo 1 — Buscar ofertas pendentes (OfertasAceiteFetcher):

  • Endpoint: ListarProgramacoesAsync com status PENDENTE.
  • Janela: ultimos 7 dias.
  • Paginacao: pageSize=300, loop com offset ate atingir total.
  • Deduplica por CodProgColeta.

Passo 2 — Preparacao para aceite (OfertasAceitePreparationService):

  • Processa em batches de 20.
  • Busca ofertas existentes no banco que correspondem aos codigos pendentes e tem TryAceite == false.
  • Para cada oferta existente: tenta adquirir distributed lock por 30s (chave: OfertaUpdate:{tenantId}:{cogProgColeta}).
  • Se obtem lock: chama ofertaExistente.AtualizarParaAceite(...) para sincronizar tipo, IsCargaAdicional, datas e status da Galileu.
  • Salva via UoW separada por batch.

Regras de Avaliacao de Aceite

Arquivo: cargo_fleet.Application/Ofertas/Aceite/OfertasAceiteAvaliacaoAppService.cs

  • Filtra ofertas do banco com TryAceite == false cujos codigos estao nas pendentes da Galileu.
  • Constroi RotaKey(origem, destino, tipoCarga, tipo, isTnf) para cada oferta.
  • Busca rotas ativas que cobrem as combinacoes de origem/destino/tipo/carga.
CondicaoResultado
IsCargaAdicional == false && QuantidadeCarros > 0 && periodoValidopodeAceitarCargaNormal = true
IsCargaAdicional == true && AceitaCargaAdicional == true && CargaAdicionalQuantidadeVeiculos > 0 && periodoValidopodeAceitarCargaAdicional = true
IsCargaAdicional == true && AceitaCargaAdicional == falseRejeita silenciosamente (sem marcar CouldNotBeAcceptedByRoute)
Periodo invalido ou sem veiculosMarca CouldNotBeAcceptedByRoute = true (se ainda nao marcado)
Numero de aceites por rota ja atingiu limiteRejeita, marca CouldNotBeAcceptedByRoute

REGRA CRITICA (incoerencia — ver P-01): O limite de aceites por rota e contado somente dentro do ciclo atual (in-memory), nao inclui ofertas ja aceitas em ciclos anteriores. Se a rota tem QuantidadeCarros = 2 mas ja teve 1 aceite em ciclo anterior, pode aceitar mais do que deveria.

Retorna ResultadoAvaliacaoAceite(aceitesPorRota, ofertasRecusadas).


Execucao do Aceite na API Galileu

Arquivo: cargo_fleet.Application/Ofertas/Aceite/OfertasAceiteExecucaoAppService.cs

Para cada oferta aprovada:

  • Se IsTnf(): usa PrecoPerna1 como preco principal e PrecoPerna2 como preco adicional, precisaAdicional: true.
  • Se nao TNF: usa Preco da rota, precisaAdicional: false.
  • Chama GalileuApiService.AceitarProgramacaoAsync(token, request) — endpoint RPC novo.
  • Em caso de falha HTTP: loga erro e continua com proximas ofertas (nao falha o lote).

Aplicacao de Resultados (OfertasAceiteResultApplier)

Arquivo: cargo_fleet.Application/Ofertas/Aceite/OfertasAceiteResultApplier.cs

Para cada aceite bem-sucedido:

  • AplicarEfeitosAceite: seta TryAceite = true, CouldNotBeAcceptedByRoute = false.
  • Se TNF e rota tem PrecoPerna1 e PrecoPerna2: seta Valor = PrecoPerna1, PrecoAdicional = PrecoPerna2.
  • Caso contrario: Valor = Preco ?? 0.
  • Decrementa contador da rota: se IsCargaAdicional == true decrementa CargaAdicionalQuantidadeVeiculos, senao decrementa QuantidadeCarros.
  • PersistirAceiteAsync: salva oferta e rota via OfertaManager.UpdateAsync e RotaManager.UpdateAsync.
  • GerarNotificacoesEsgotamentoAsync: se algum contador chegou a 0, cria notificacao de alerta.

Para cada oferta recusada:

  • Seta CouldNotBeAcceptedByRoute = true e persiste.
  • Cria notificacao informando que a carga ficou disponivel para aceite/cotacao.

Apos avaliacao: INotificationDispatcher.DispatchManyAsync(notificacoes) → Firebase Push.


Fluxo Legado (RegistrarOfertasWorker)

Arquivo: cargo_fleet.HttpApi.Host/BackgroundJobs/RegistrarOfertasWorker.cs

Status atual: Ativo (registrado no DI), mas com bug de autenticacao invertida (P-02) que o faz falhar em todos os ciclos.

  • Periodo: ultimos 7 dias, janela fracionada em 6 intervalos por dia.
  • Delega para RegistrarOfertaService.ProcessarOfertasAsync (740 linhas).

RegistrarOfertaService.ProcessarOfertasAsync (cargo_fleet.Application/BackgroundJobs/RegistrarOfertaService.cs):

Fluxo por intervalo:

  1. Adquire distributed lock OfertaLock:InUse:Tenant:{TenantId} com timeout de 30s.
  2. Chama GalileuApiService.ListarOfertasAsync (endpoint antigo /rpc/transportador-api/listarOfertas/).
  3. Normaliza payload via GalileuOfertaNormalizer.NormalizePayloadItem.
  4. Verifica carga adicional para todos os codigos em lote via CargaAdicionalService.VerificarCargasAdicionaisEmLoteAsync.
  5. Para cada oferta normalizada (ProcessarOfertaAsync):
    • Se nao existe no banco E tem origem/destino validos: cria oferta.
    • Se aceita e tem mudanca de status/datas: cria OfertaRecord correspondente, envia WhatsApp.
    • Se status == "PENDENTE" e TryAceite == false e rota existe:
      • Cenario 1: carga adicional valida → chama GalileuApiService.AceitarOfertaAsync (endpoint antigo).
      • Cenario 2: carga normal valida → chama o mesmo endpoint antigo.
      • Cenarios 3-7: rejeita com log de motivo.
    • Se status == "ACEITA" e TryAceite == true e status anterior != "ACEITA": envia WhatsApp via template.
  6. Atualiza oferta (CodigoB100, Status) via OfertaManager.UpdateAsync.
  7. Se aceite e rota existe: atualiza rota via RotaManager.UpdateAsync.

Diferencas entre Fluxo Novo e Legado

AspectoFluxo Novo (ProcessarOfertasWorker)Fluxo Legado (RegistrarOfertasWorker)
Endpoint de aceiteAceitarProgramacaoAsync (RPC estruturado)AceitarOfertaAsync (endpoint antigo)
Suporte TNFSim (precisaAdicional, precoAdicional)Nao (sem PrecoPerna1/PrecoPerna2 para TNF)
Divisao de janela temporalSim (recursiva, ate 1h)Nao (6 intervalos fixos)
AutenticacaoFuncionandoBug invertido (P-02) — sempre falha
Verificacao carga adicionalEm lote (batch)Individual por oferta
Lock de concorrenciaPor oferta (OfertaUpdate:{tenantId}:{cod})Global por tenant (OfertaLock:InUse:Tenant:{id})
Janela de buscaUltimas 24hUltimos 7 dias
Processamento por tenantParaleloParalelo

Atualizacao de Ofertas (AtualizarOfertasWorker)

Arquivo: cargo_fleet.HttpApi.Host/BackgroundJobs/AtualizarOfertasWorker.cs

  • Processa tenants sequencialmente (ao contrario dos outros workers que usam Task.WhenAll).
  • Timer.Period = 1ms — mas como o loop e await, o proximo ciclo so inicia apos o termino do atual.
  • Delega para AtualizarOfertasJob.AtualizarOfertasAsync.

AtualizarOfertasJob (cargo_fleet.Application/BackgroundJobs/AtualizarOfertasJob.cs):

  1. OfertasAtualizacaoFetcher.ObterParaAtualizacaoAsync():

    • Autentica e busca programacoes com status: ACEITA, RECUSADO, CANCELADO, EMBARQUE_EMITIDO, CANCELADO_POR_ACEITE, CANCELADO_PELA_CENTRAL, RECUSADO_APOS_ACEITE, AGUARDANDO_CONFIRMACAO, EXPIRADA, DECLINADA, RECUSA_LEILAO.
    • Janela: ultimos 7 dias, paginacao 300/vez.
  2. Processa em batches de 200 ofertas.

  3. OfertasAtualizacaoProcessor.ProcessarAsync:

    • Busca ofertas existentes no banco com os codigos do lote.
    • Verifica quais ja tem OfertaRecord (para saber se DADOS_INICIAIS ja foi criado).
    • Para cada oferta: tenta distributed lock (OfertaUpdate:{tenantId}:{cod}, 30s).
    • Delega para OfertaAtualizacaoService.Atualizar.

OfertaAtualizacaoService.Atualizar (cargo_fleet.Application/Ofertas/Atualizacao/OfertaAtualizacaoService.cs):

Regras de atualizacao (so para ofertas aceitas, status ACEITA ou EMBARQUE EMITIDO):

  • Se nao tem record inicial: cria OfertaRecord com NomeCampo = "DADOS_INICIAIS", preenchendo DthrAceite, DtPlanSMInicial, DtPrevisaoColetaInicial, DtPrevisaoEntregaInicial.
  • Se status mudou: cria record de "Status". Se novo status e CANCELADO ou DECLINADA: cria OfertaAlteracaoStatusCargaMessage para WhatsApp.
  • Se DataPrevisaoColeta mudou: cria record, atualiza oferta, cria OfertaAlteracaoDataColetaMessage para WhatsApp.
  • Se DataRemessa mudou: cria record, atualiza oferta (sem WhatsApp).
  • Se DataPrevisaoEntrega mudou: cria record, atualiza oferta (sem WhatsApp).
  • Cenario especial: se Status == "ACEITA" E TryAceite == true E status anterior != "ACEITA": cria mensagem WhatsApp de carga aceita.

Ao final: sempre atualiza ofertaExistente.Status = ofertaAtualizada.Status.

Apos processamento do lote:

  • OfertaRecordWriter.WriteAsync(records) persiste os records.
  • WhatsAppOfertasAtualizacaoMessenger.SendAsync(notifications) envia WhatsApp.

Atualizacao Manual de Preco

Arquivo: cargo_fleet.Application/Ofertas/AtualizacaoPreco/OfertaAtualizacaoPrecoAppService.cs

Validacoes antes de executar:

  1. oferta.TryAceite == false → lanca UserFriendlyException.
  2. oferta.Status != "AGUARDANDO CONFIRMACAO" → lanca UserFriendlyException.
  3. oferta.PrecoAtualizadoNoGalileu == true → lanca UserFriendlyException (idempotencia).
  4. Se IsTnf() e !input.PrecoAdicional.HasValue → lanca UserFriendlyException.

Fluxo:

  • Autentica na Galileu.
  • Chama AceitarProgramacaoAsync com isReaceite: true no payload.
  • Somente apos sucesso da API: atualiza Valor, PrecoAdicional (se TNF), PrecoAtualizadoNoGalileu = true.
  • Cria OfertaRecord com NomeCampo = "Valor da oferta atualizado".

Cotacao Manual

Arquivo: cargo_fleet.Application/Ofertas/OfertasAppService.Extended.cs

Fluxo:

  1. Busca oferta por CogProgColeta.
  2. Autentica na Galileu.
  3. Adquire distributed lock OfertaLock:InUse (escopo global, nao por oferta — incoerencia P-05).
  4. Chama AceitarProgramacaoAsync com preco manual.
  5. Seta TryAceite = true, CouldNotBeAcceptedByRoute = false.
  6. Se IsTnf(): seta Valor e PrecoAdicional; caso contrario apenas Valor.

Incoerencia: O lock e global, nao por oferta ou tenant, bloqueando todos os aceites manuais do sistema simultaneamente.


Incoerencias Identificadas

IDDescricao
P-01Dois workers concorrentes processando as mesmas ofertas
P-02Logica de autenticacao invertida no legado
P-05Lock global para cotacao manual
P-06Count/List mismatch em GetAcceptedAndRejectedListAsync