Pular para o conteúdo principal

Devolucao de Pecas Nao Utilizadas

Em discussao2026-07-15

Problema

Apos uma peca ser aprovada e entregue, o sistema considera o processo concluido. Nao ha como registrar a devolucao de materiais que nao foram utilizados.

Na pratica, e comum que o mecanico solicite pecas que acabam nao sendo utilizadas:

  • Alteracao no diagnostico durante o reparo
  • Troca de estrategia de manutencao
  • Solicitacao em quantidade superior a necessaria

Resultado: o estoque no sistema nao reflete a realidade fisica. Pecas devolvidas ao almoxarifado nao voltam ao saldo.


Fluxo Atual

PENDING ──→ APPROVED ──→ DELIVERED (terminal, sem volta)
│ -qty
└──→ REJECTED (terminal)

Como o estoque funciona hoje:

EtapaEstoqueO que acontece
Mecanico solicita 10 (PENDING)Sem mudancaAguardando aprovacao
Supervisor aprova 10 (APPROVED)-10Reserva de estoque
Almoxarifado entrega 10 (DELIVERED)Sem mudancaConfirmacao fisica
Mecanico usa 7, sobram 3Sem mudanca3 pecas perdidas no sistema

O estoque e decrementado na aprovacao (reserva), nao na entrega. Esse padrao e correto — impede que dois supervisores aprovem a mesma peca simultaneamente. Porem, nao existe caminho de volta.


Bug Existente: Inconsistencia na Aprovacao Individual

Durante a analise, identificamos um problema de seguranca na aprovacao individual de pecas.

Aprovacao em lote (batch) — segura:

$transaction {
update partRequest → APPROVED
update part → stockQuantity { decrement: qty } ← atomico
}

Aprovacao individual — insegura:

1. Le part.stockQuantity = 100 ← leitura
2. Calcula 100 - 10 = 90 ← no codigo JS
3. Grava partRequest.status = APPROVED ← query 1 (sem transaction)
4. Grava part.stockQuantity = 90 ← query 2 (sem transaction)

Riscos:

  • Se duas aprovacoes acontecem ao mesmo tempo, ambas leem 100 e gravam 90. O estoque deveria ser 80.
  • Se a query 4 falha, a peca fica APPROVED mas o estoque nao decrementou.

Correcao proposta: alinhar a aprovacao individual com o mesmo padrao seguro do batch (transaction + atomic decrement). Sera feito junto com a feature de devolucao.


Proposta: Novo Status RETURNED

Fluxo proposto

┌──→ REJECTED (estoque: sem mudanca)

PENDING ──→ APPROVED ──→ DELIVERED ──→ RETURNED
-qty +returnedQty

Novo status: RETURNED

Quando o usuario registra uma devolucao total (todas as pecas devolvidas), o status muda de DELIVERED para RETURNED. Em devolucao parcial, o status permanece DELIVERED e o frontend exibe um indicador visual com a quantidade devolvida.

Campos novos no PartRequest

CampoTipoDescricao
returnedQuantityIntQuantidade devolvida (1 a approvedQuantity)
returnedAtDateTimeData/hora da devolucao
returnedByIdString (FK User)Quem registrou a devolucao
returnReasonStringMotivo predefinido (enum-like)
returnReasonDetailsString?Texto livre quando motivo = OTHER

Regras de negocio

  1. So pecas DELIVERED podem ser devolvidas — nao e possivel devolver pecas pendentes, aprovadas ou rejeitadas
  2. Quantidade validadareturnedQuantity deve ser entre 1 e approvedQuantity
  3. Estoque restauradoPart.stockQuantity incrementado por returnedQuantity (atomico, dentro de transaction)
  4. Uma devolucao por solicitacao — nao e possivel devolver a mesma peca duas vezes
  5. Motivo obrigatorio — campo returnReason e NOT NULL, validado no backend e frontend
  6. Rastreabilidade — usuario, data/hora e motivo registrados. Evento emitido para o historico da OS
  7. Permissao especifica — apenas usuarios com Action.Return em PartRequest podem devolver

Exemplo pratico

Solicitou: 10 cupilhas
Aprovado: 10 cupilhas → estoque -10
Entregue: 10 cupilhas
Usou: 7 cupilhas
Devolveu: 3 cupilhas → estoque +3, status DELIVERED (parcial)

Resultado:
- quantidadeUsada = approvedQuantity - returnedQuantity = 7
- estoque restaurou 3 unidades
- historico registra a devolucao

Cenarios e Garantias

CenarioComportamentoSeguranca
Devolver 3 de 10 entregues+3 no estoque, DELIVERED (parcial)Validacao: qty nao excede approved
Tentar devolver 11 de 10BloqueadoErro: excede quantidade aprovada
Tentar devolver peca APPROVED (nao entregue)BloqueadoErro: status deve ser DELIVERED
Tentar devolver duas vezesBloqueadoErro: ja devolvido (returnedQuantity != null)
Duas devolucoes simultaneas (race condition)SeguroTransaction + atomic increment
Devolver todas as 10+10 no estoque, RETURNEDquantidadeUsada = 0

Prototipo: Devolucao Parcial vs Total

Como exibir pecas devolvidas na tabela? Clique nas abas para comparar as 3 opcoes visuais.

Opcao A — Status RETURNED unico
Qualquer devolucao (parcial ou total) muda o status para "Devolvida". A quantidade devolvida vs utilizada aparece no card de detalhes e na coluna de quantidade.
Pendente Aguardando aprovacaoEntregue Entregue ao mecanicoDevolvida Devolvida ao almoxarifado
🔧 PN-0007Em Manutencao4 pecas
PecaServicoQtdStatusSolicitante
Cupilha Maior
27054788
Substituir cuica de freio10(6 usadas)Devolvida
Tamires Admin
04/07/2026
Qtd aprovada: 10
Qtd devolvida: 4
Qtd utilizada: 6
Devolvido por: Tamires Admin
Data devolucao: 04/07/2026
Motivo: Diagnostico alterado durante reparo
Cupilha Menor
27059153
Substituir cuica de freio5(0 usadas)Devolvida
Tamires Admin
04/07/2026
Junta 4 furos
27019917
Substituir cuica de freio1Entregue
Tamires Admin
04/07/2026
Filtro de Oleo
27031445
Troca de oleo2Pendente
Carlos Mec.
04/07/2026
Clique nas linhas com devolucao para ver o card de detalhes

Decisoes

1. Visualizacao de devolucao parcial — Opcao B aprovada

Devolucao parcial mantem status "Entregue" com indicador visual ↩ X/Y devolvidas. So muda para "Devolvida" (roxo) quando TODAS as pecas sao devolvidas.

DevolucaoStatusVisual
Parcial (3 de 10)DELIVEREDBadge "Entregue" (azul) + tag ↩ 3/10 devolvidas
Total (10 de 10)RETURNEDBadge "Devolvida" (roxo)
NenhumaDELIVEREDBadge "Entregue" (azul)

Implicacao tecnica: o status RETURNED so e atribuido quando returnedQuantity === approvedQuantity. Para devolucao parcial, o status permanece DELIVERED e o frontend checa returnedQuantity > 0 para exibir o indicador.

2. Motivo da devolucao — Obrigatorio com opcoes predefinidas

Campo returnReason e obrigatorio (NOT NULL). O usuario seleciona de uma lista predefinida de motivos, com opcao "Outro" que habilita campo de texto livre.

Opcoes predefinidas:

ValorLabel
DIAGNOSIS_CHANGEDDiagnostico alterado durante reparo
EXCESS_QUANTITYQuantidade solicitada maior que necessaria
WRONG_PARTPeca incorreta para o servico
STRATEGY_CHANGEDEstrategia de manutencao alterada
OTHEROutro (campo de texto livre)

Campos no modelo:

CampoTipoDescricao
returnReasonString (enum-like)Motivo predefinido selecionado
returnReasonDetailsString?Texto livre quando motivo = OTHER (obrigatorio neste caso)

Isso permite analise agregada dos motivos (ex: "40% das devolucoes sao por diagnostico alterado") sem perder a flexibilidade de motivos nao previstos.

3. OS cancelada com pecas aprovadas (nao entregues) — Em analise

Hoje, quando uma OS e cancelada, as pecas que estao no status APPROVED continuam com o estoque reservado (decrementado). Nao existe mecanismo de "liberar" essa reserva.

OpcaoO que aconteceImpacto
A: Reverter automaticamenteAo cancelar OS, pecas APPROVED tem estoque devolvido e status muda para REJECTED com motivo "OS cancelada"Estoque sempre consistente. Mais complexo de implementar.
B: Reverter manualmenteSupervisor precisa rejeitar as pecas antes de cancelar a OSProcesso manual, risco de esquecimento. Estoque pode ficar inconsistente.
C: Nao tratar agoraManter como esta. Corrigir em momento futuro.Risco de estoque fantasma continua existindo.

Dados para decisao: Cada peca APPROVED nao entregue e nao rejeitada representa estoque "preso". Se isso acontece com frequencia, o saldo do almoxarifado vai divergindo da realidade ao longo do tempo.

Aguardando decisao de produto.

4. Quem pode devolver — Permissao especifica Action.Return

Nova action Return no sistema de permissoes, aplicada ao subject PartRequest. Implementada no codigo (hardcoded nos arquivos de ability, mesmo padrao das demais actions).

Por que no codigo e nao no banco: o sistema de permissoes atual e inteiramente hardcoded em TypeScript (40 arquivos de subjects, 13 roles). Mover para banco de dados tem custo de engenharia significativo sem beneficio imediato — a tabela de permissoes seria ~500 rows (por role, nao por user), custo de banco desprezivel, mas a migracao envolve schema novo, cache invalidation e reescrita dos guards. Migrar permissoes para banco e um projeto separado, futuro.

Custo de adicionar Action.Return: ~12 linhas de codigo, zero migrations de banco. A action fica disponivel na tela de configuracao de roles existente.


Visualizacao na Interface

Badge de status

StatusCorLabelQuando
PENDINGAmareloPendenteAguardando aprovacao
APPROVEDVerdeAprovadoAprovado, estoque reservado
REJECTEDVermelhoRejeitadaRejeitado pelo supervisor
DELIVEREDAzulEntregueEntregue ao mecanico
DELIVERED + devolucao parcialAzul + tag Entregue + ↩ 3/10 devolvidasParte devolvida
RETURNEDRoxoDevolvidaTodas devolvidas

Informacao exibida no card de detalhes

Quando uma peca tem devolucao registrada (returnedQuantity > 0):

  • Quantidade aprovada: 10
  • Quantidade devolvida: 3
  • Quantidade utilizada: 7
  • Devolvido por: [nome do usuario]
  • Data da devolucao: [data/hora]
  • Motivo: [opcao selecionada ou texto livre se "Outro"]

Acao no dropdown da tabela

Para pecas com status DELIVERED, o menu de acoes exibe:

┌─────────────────────────┐
│ Acoes │
│─────────────────────────│
│ [>] Ver detalhes │
│ [↩] Devolver Peca │ -- nova acao
└─────────────────────────┘

Clicar em "Devolver Peca" abre um dialog com:

  • Input numerico: quantidade a devolver (max: approvedQuantity)
  • Select: motivo da devolucao (opcoes predefinidas + "Outro")
  • Textarea: detalhes (visivel e obrigatorio apenas quando motivo = "Outro")
  • Botao: "Confirmar Devolucao"

A acao so aparece para usuarios com permissao Action.Return no subject PartRequest.


Impacto Tecnico

Resumo

AreaMudancas
Database1 migration (5 campos novos + 1 enum value)
Backend~16 arquivos (use case, repo, controller, errors, events, ability, testes)
Frontend~9 arquivos (enum, types, api, hook, dialog, table item, card)
Correcao approve2 arquivos (use case + repository — transaction + atomic)
Permissao1 action nova (Return) nos arquivos de ability (~12 linhas)

Logica de status (Opcao B)

if returnedQuantity == null → status = DELIVERED (sem devolucao)
if returnedQuantity < approved → status = DELIVERED (parcial, indicador visual)
if returnedQuantity == approved → status = RETURNED (total)

O que NAO muda

  • Fluxo de aprovacao (exceto correcao de seguranca)
  • Fluxo de entrega
  • Fluxo de rejeicao
  • Queries de analytics
  • Endpoints existentes
  • Schema de permissoes (sem migration, action adicionada no codigo)

Decisao

ItemDecisaoStatus
Visualizacao parcialOpcao B — Entregue + indicadorAprovado
Motivo da devolucaoObrigatorio, select com opcoes + "Outro"Aprovado
OS cancelada com pecas APPROVEDEm analise (3 opcoes acima)Pendente
Permissao de devolucaoAction.Return no codigo (hardcoded)Aprovado

ADRs (Architecture Decision Records)

ADR-1: Status RETURNED vs campos no modelo existente

Contexto: Precisamos registrar devolucoes de pecas. Tres opcoes avaliadas: (A) apenas campos no PartRequest sem novo status, (B) novo status RETURNED, (C) entidade separada PartReturn.

Decisao: Opcao B — novo status RETURNED para devolucao total, campos de devolucao no PartRequest.

Justificativa:

  • O produto precisa de badge "Devolvida" na tabela — status proprio permite filtragem direta por countByStatus
  • A Opcao A (sem status) exigiria logica extra em toda query que lista/filtra pecas por status
  • A Opcao C (entidade separada) e over-engineering: o requisito e uma unica devolucao por request, nao multiplas
  • A Opcao B permite que devolucao parcial fique como DELIVERED com returnedQuantity > 0 (indicador visual), e total vire RETURNED

Consequencias:

  • Todo switch/case de RequestStatus precisa tratar RETURNED
  • Queries de contagem (countByStatus) incluem RETURNED automaticamente
  • Devolucao parcial exige checagem de returnedQuantity alem do status

ADR-2: Estoque decrementado na aprovacao (reserva)

Contexto: O estoque poderia ser decrementado na aprovacao (reserva) ou na entrega (confirmacao fisica). Precisamos garantir consistencia quando multiplos supervisores aprovam pecas simultaneamente.

Decisao: Manter decremento na aprovacao (padrao atual).

Justificativa:

  • Decrementar na entrega permitiria que dois supervisores aprovem a mesma peca simultaneamente — ambos veriam estoque disponivel, mas so existiria quantidade para um
  • Aprovacao como reserva impede over-allocation: se estoque tem 10, o segundo supervisor ja ve estoque reduzido
  • Na devolucao, o incremento ocorre no ato do registro (Part.stockQuantity += returnedQuantity)

Consequencias:

  • Pecas APPROVED nao entregues representam estoque "preso" (reservado)
  • Cancelamento de OS com pecas APPROVED precisa liberar reserva (decisao pendente)
  • Transaction + atomic increment obrigatorio na devolucao (mesmo padrao do batch approve)

ADR-3: Correcao de race condition no approve individual

Contexto: A aprovacao individual de pecas usa read-then-write sem transaction. A aprovacao em lote (batch) usa transaction + atomic decrement corretamente. As duas devem ter o mesmo nivel de seguranca.

Decisao: Alinhar approve individual com o padrao do batch — transaction + atomic decrement.

Justificativa:

  • O padrao atual le part.stockQuantity, calcula em JS e grava — se duas aprovacoes ocorrem ao mesmo tempo, ambas leem o mesmo valor e uma sobrescreve a outra
  • O batch approve ja resolve isso com $transaction + { stockQuantity: { decrement: qty } }
  • Nao ha motivo para os dois caminhos terem niveis de seguranca diferentes

Consequencias:

  • Approve individual e batch ficam com mesmo padrao de seguranca
  • Eliminado risco de estoque negativo por race condition
  • Mudanca em 2 arquivos (use case + repository)

ADR-4: Permissao Action.Return no codigo (hardcoded)

Contexto: A devolucao precisa de permissao granular. O sistema de permissoes atual e inteiramente hardcoded em TypeScript (40 subjects, 13 roles, 7 actions). Avaliamos mover permissoes para banco de dados.

Decisao: Adicionar Action.Return no codigo, manter sistema hardcoded.

Justificativa:

  • Custo de DB para permissoes seria desprezivel (~500 rows, por role e nao por user)
  • Porem, a migracao para banco envolve: schema novo (3-4 tabelas), cache invalidation, reescrita dos guards, seed de dados — projeto separado
  • Adicionar uma action no codigo custa ~12 linhas e zero migrations
  • A tela de configuracao de roles no frontend ja existe e continuara funcionando

Consequencias:

  • Nova action disponivel para atribuicao por role
  • Alteracao futura de permissoes continua exigindo deploy
  • Migracao de permissoes para banco fica como projeto separado, quando houver necessidade de customizacao por empresa

ADR-5: Motivo de devolucao com opcoes predefinidas

Contexto: O motivo da devolucao precisa ser obrigatorio para rastreabilidade. Campo de texto livre permite qualquer coisa, dificultando analise agregada. Select fechado perde flexibilidade.

Decisao: Select com opcoes predefinidas + opcao "Outro" com campo de texto livre.

Justificativa:

  • Opcoes predefinidas permitem analise agregada (ex: "40% das devolucoes sao por diagnostico alterado")
  • A opcao "Outro" garante flexibilidade para cenarios nao previstos
  • Dois campos no modelo: returnReason (enum-like, obrigatorio) + returnReasonDetails (texto livre, obrigatorio apenas quando reason = OTHER)
  • Novas opcoes podem ser adicionadas sem migration (enum no codigo)

Consequencias:

  • Dashboard de analytics pode agrupar devolucoes por motivo
  • 5 opcoes iniciais cobrem os cenarios mais comuns do dia-a-dia
  • Campo returnReasonDetails pode ser usado para feedback qualitativo