Ofertas — Ciclo de Vida Completo
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
HttpRequestExceptionouAuthenticationException: registraApiStatusEnum.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.ListarOfertasAsynccom endpointrpc/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 porCodProgColeta. - Retorna
IReadOnlyList<PayloadItem>.
OfertasCriacaoPreparationService.PrepararOfertasParaCriacaoAsync(payloadItemsPendentes):
- Carrega do banco todas as ofertas cujos
CogProgColetaja existem. - Filtra somente as que NAO existem no banco (novidades).
- Normaliza via
GalileuOfertaNormalizer.NormalizePayloadItem→OfertaGalileuNormalized.
OfertasCriacaoWriter.CriarOfertasAsync(ofertasParaCriar):
- Para cada oferta: se
Origem == null || Destino == null, pula. - Chama
OfertaManager.CreateAsynccomtipo: 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:
ListarProgramacoesAsynccom statusPENDENTE. - 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 == falsecujos 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.
| Condicao | Resultado |
|---|---|
IsCargaAdicional == false && QuantidadeCarros > 0 && periodoValido | podeAceitarCargaNormal = true |
IsCargaAdicional == true && AceitaCargaAdicional == true && CargaAdicionalQuantidadeVeiculos > 0 && periodoValido | podeAceitarCargaAdicional = true |
IsCargaAdicional == true && AceitaCargaAdicional == false | Rejeita silenciosamente (sem marcar CouldNotBeAcceptedByRoute) |
| Periodo invalido ou sem veiculos | Marca CouldNotBeAcceptedByRoute = true (se ainda nao marcado) |
| Numero de aceites por rota ja atingiu limite | Rejeita, 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(): usaPrecoPerna1como preco principal ePrecoPerna2como preco adicional,precisaAdicional: true. - Se nao TNF: usa
Precoda 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: setaTryAceite = true,CouldNotBeAcceptedByRoute = false.- Se TNF e rota tem
PrecoPerna1ePrecoPerna2: setaValor = PrecoPerna1,PrecoAdicional = PrecoPerna2. - Caso contrario:
Valor = Preco ?? 0. - Decrementa contador da rota: se
IsCargaAdicional == truedecrementaCargaAdicionalQuantidadeVeiculos, senao decrementaQuantidadeCarros. PersistirAceiteAsync: salva oferta e rota viaOfertaManager.UpdateAsynceRotaManager.UpdateAsync.GerarNotificacoesEsgotamentoAsync: se algum contador chegou a 0, cria notificacao de alerta.
Para cada oferta recusada:
- Seta
CouldNotBeAcceptedByRoute = truee 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:
- Adquire distributed lock
OfertaLock:InUse:Tenant:{TenantId}com timeout de 30s. - Chama
GalileuApiService.ListarOfertasAsync(endpoint antigo/rpc/transportador-api/listarOfertas/). - Normaliza payload via
GalileuOfertaNormalizer.NormalizePayloadItem. - Verifica carga adicional para todos os codigos em lote via
CargaAdicionalService.VerificarCargasAdicionaisEmLoteAsync. - 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
OfertaRecordcorrespondente, envia WhatsApp. - Se status == "PENDENTE" e
TryAceite == falsee 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.
- Cenario 1: carga adicional valida → chama
- Se status == "ACEITA" e
TryAceite == truee status anterior != "ACEITA": envia WhatsApp via template.
- Atualiza oferta (
CodigoB100,Status) viaOfertaManager.UpdateAsync. - Se aceite e rota existe: atualiza rota via
RotaManager.UpdateAsync.
Diferencas entre Fluxo Novo e Legado
| Aspecto | Fluxo Novo (ProcessarOfertasWorker) | Fluxo Legado (RegistrarOfertasWorker) |
|---|---|---|
| Endpoint de aceite | AceitarProgramacaoAsync (RPC estruturado) | AceitarOfertaAsync (endpoint antigo) |
| Suporte TNF | Sim (precisaAdicional, precoAdicional) | Nao (sem PrecoPerna1/PrecoPerna2 para TNF) |
| Divisao de janela temporal | Sim (recursiva, ate 1h) | Nao (6 intervalos fixos) |
| Autenticacao | Funcionando | Bug invertido (P-02) — sempre falha |
| Verificacao carga adicional | Em lote (batch) | Individual por oferta |
| Lock de concorrencia | Por oferta (OfertaUpdate:{tenantId}:{cod}) | Global por tenant (OfertaLock:InUse:Tenant:{id}) |
| Janela de busca | Ultimas 24h | Ultimos 7 dias |
| Processamento por tenant | Paralelo | Paralelo |
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 eawait, o proximo ciclo so inicia apos o termino do atual.- Delega para
AtualizarOfertasJob.AtualizarOfertasAsync.
AtualizarOfertasJob (cargo_fleet.Application/BackgroundJobs/AtualizarOfertasJob.cs):
-
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.
- Autentica e busca programacoes com status:
-
Processa em batches de 200 ofertas.
-
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
OfertaRecordcomNomeCampo = "DADOS_INICIAIS", preenchendoDthrAceite,DtPlanSMInicial,DtPrevisaoColetaInicial,DtPrevisaoEntregaInicial. - Se status mudou: cria record de "Status". Se novo status e
CANCELADOouDECLINADA: criaOfertaAlteracaoStatusCargaMessagepara WhatsApp. - Se
DataPrevisaoColetamudou: cria record, atualiza oferta, criaOfertaAlteracaoDataColetaMessagepara WhatsApp. - Se
DataRemessamudou: cria record, atualiza oferta (sem WhatsApp). - Se
DataPrevisaoEntregamudou: cria record, atualiza oferta (sem WhatsApp). - Cenario especial: se
Status == "ACEITA"ETryAceite == trueE 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:
oferta.TryAceite == false→ lancaUserFriendlyException.oferta.Status != "AGUARDANDO CONFIRMACAO"→ lancaUserFriendlyException.oferta.PrecoAtualizadoNoGalileu == true→ lancaUserFriendlyException(idempotencia).- Se
IsTnf()e!input.PrecoAdicional.HasValue→ lancaUserFriendlyException.
Fluxo:
- Autentica na Galileu.
- Chama
AceitarProgramacaoAsynccomisReaceite: trueno payload. - Somente apos sucesso da API: atualiza
Valor,PrecoAdicional(se TNF),PrecoAtualizadoNoGalileu = true. - Cria
OfertaRecordcomNomeCampo = "Valor da oferta atualizado".
Cotacao Manual
Arquivo: cargo_fleet.Application/Ofertas/OfertasAppService.Extended.cs
Fluxo:
- Busca oferta por
CogProgColeta. - Autentica na Galileu.
- Adquire distributed lock
OfertaLock:InUse(escopo global, nao por oferta — incoerencia P-05). - Chama
AceitarProgramacaoAsynccom preco manual. - Seta
TryAceite = true,CouldNotBeAcceptedByRoute = false. - Se
IsTnf(): setaValorePrecoAdicional; caso contrario apenasValor.
Incoerencia: O lock e global, nao por oferta ou tenant, bloqueando todos os aceites manuais do sistema simultaneamente.
Incoerencias Identificadas
| ID | Descricao |
|---|---|
| P-01 | Dois workers concorrentes processando as mesmas ofertas |
| P-02 | Logica de autenticacao invertida no legado |
| P-05 | Lock global para cotacao manual |
| P-06 | Count/List mismatch em GetAcceptedAndRejectedListAsync |
Entidades e Propriedades
OfertaBase (abstrata) / Oferta (concreta) Arquivo:
cargo_fleet.Domain/Ofertas/Oferta.cseOferta.Extended.csHerda de
FullAuditedAggregateRoot— possuiCreationTime,CreatorId,LastModificationTime,LastModifierId,IsDeleted,DeletionTime,DeleterId.TenantIdGuid?TipoTipoRotaEnumDataDateTimeDataRemessaDateOnly?DataPrevisaoColetaDateTime?DataPrevisaoEntregaDateTime?CodigoB100string(obrigatorio)Origemstring?"NOME/BRA"Destinostring?"NOME/BRA"TipoCargaTipoCargaEnumTipoOperacaoTipoOperacaoEnumTrocaDeNf(TNF)Valordecimal?Statusstring(obrigatorio)PontoTNFstring?CogProgColetaintTryAceiteboolCouldNotBeAcceptedByRouteboolPrecoAtualizadoNoGalileubool?IsCargaAdicionalbool?null= nao determinado;true/falseapos verificacaoPrecoAdicionaldecimal?OrigemCidadeIdint?CidadesDestinoCidadeIdint?CidadesPontoTnfCidadeIdint?CidadesMetodos 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): retornatrueseTipoOperacao == 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.