Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
8abfacf
feat: interfaces detalhadas para expedicao, participante, e rota
DimitriGaraluz Sep 21, 2026
1e33c0d
feat: adiciona contratos de participantes e rotas em ExpedicaoCollection
DimitriGaraluz Sep 21, 2026
f700fde
refactor: update nas interfaces de ExpedicaoCollection por consistencia
DimitriGaraluz Sep 22, 2026
cb332b9
feat: adiciona use cases para adicionar e remover participantes, e, s…
DimitriGaraluz Sep 23, 2026
c8c09e7
feat: adciona ConflictError para http 409
DimitriGaraluz Sep 23, 2026
7de2280
feat: adiciona factory para ExpedicaoCollection
DimitriGaraluz Sep 23, 2026
b2dacef
feat: adiciona controllers e rotas para participantes e rotas
DimitriGaraluz Sep 23, 2026
8ee9b9b
feat: adciona metodos para participantes e rotas, e, ajusta findId e …
DimitriGaraluz Sep 23, 2026
6909a15
test: adiciona testes de integracao para participantes e rotas
DimitriGaraluz Sep 23, 2026
b45e95f
test: atualiza teste para refletir interface de retorno paginado
DimitriGaraluz Sep 23, 2026
cee01ea
fix: corrige formatacao e erros do lint
DimitriGaraluz Sep 23, 2026
f534410
fix: resolve conflitos de merge
DimitriGaraluz Sep 24, 2026
ed1d519
fix: lint reafctor
DimitriGaraluz Sep 24, 2026
54724fb
fix: corrige inconsistencia com "SubstitiuiRotasController"
DimitriGaraluz Sep 25, 2026
2f9f3d6
fix: remove interface duplicada ExpedicaoListItem
DimitriGaraluz Sep 25, 2026
3efcf58
refactor: implementa DuplicateParticipantError para tratamento do 409
DimitriGaraluz Sep 25, 2026
66466f3
refactor: altera mapeamento de erro para usar error.name e corrige o …
DimitriGaraluz Sep 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions src/application/expedicao/AdicionaParticipanteController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { AdicionaParticipanteUseCase } from '@/domain/expedicao/AdcionaParticipanteUseCase'
import {
HttpRequest, HttpResponse, StatusCode
} from '@/library/http/common'
import { BadRequestError } from '@/library/http/error/BadRequestError'
import { ConflictError } from '@/library/http/error/ConflictError'
import { HttpError } from '@/library/http/error/HttpError'
import { InternalServerError } from '@/library/http/error/InternalServerError'
import { NextHandler, RequestHandler } from '@/library/http/Server'

interface Dependencies {
adicionaParticipanteUseCase: AdicionaParticipanteUseCase
}

interface CustomHttpRequest extends HttpRequest {
params: Record<string, string | undefined>
body: unknown
usuario?: {
id: number
tipo_usuario_id: number
}
}

export class AdicionaParticipanteController implements RequestHandler {
private readonly adicionaParticipanteUseCase: AdicionaParticipanteUseCase

constructor(dependencies: Dependencies) {
this.adicionaParticipanteUseCase = dependencies.adicionaParticipanteUseCase
}

async handle(request: CustomHttpRequest, _next: NextHandler): Promise<HttpResponse | HttpError> {
try {
// TODO: AUTENTICAÇÃO TEMPORARIAMENTE DESABILITADA

const { expedicaoId } = request.params
const { usuarioId } = request.body as { usuarioId?: number }
if (expedicaoId === undefined || expedicaoId === null || expedicaoId === '' || !/^\d+$/.test(expedicaoId)) {
return new BadRequestError({ message: 'expedicaoId inválido' })
}

if (usuarioId === undefined || typeof usuarioId !== 'number' || usuarioId <= 0) {
return new BadRequestError({ message: 'usuarioId inválido' })
}

const result = await this.adicionaParticipanteUseCase.execute(Number(expedicaoId), usuarioId)

if (result.left()) {
const error = result.value
if (error.name === 'DuplicateParticipantError') {
return new ConflictError({ message: error.message })
}
if (error.name === 'CollectionError') {
return new InternalServerError({ message: 'Falha interna ao adicionar participante' })
}
return new BadRequestError({ message: error.message })
}

return {
statusCode: StatusCode.Created,
body: { message: 'Participante adicionado com sucesso.' }
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Erro inesperado ao adicionar participante'
return new InternalServerError({ message: errorMessage })
}
}
}
58 changes: 58 additions & 0 deletions src/application/expedicao/RemoveParticipanteController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { RemoveParticipanteUseCase } from '@/domain/expedicao/RemoveParticipanteUseCase'
import {
HttpRequest, HttpResponse, StatusCode
} from '@/library/http/common'
import { BadRequestError } from '@/library/http/error/BadRequestError'
import { HttpError } from '@/library/http/error/HttpError'
import { InternalServerError } from '@/library/http/error/InternalServerError'
import { NextHandler, RequestHandler } from '@/library/http/Server'

interface Dependencies {
removeParticipanteUseCase: RemoveParticipanteUseCase
}

interface CustomHttpRequest extends HttpRequest {
usuario?: {
id: number
tipo_usuario_id: number
}
}

export class RemoveParticipanteController implements RequestHandler {
private readonly removeParticipanteUseCase: RemoveParticipanteUseCase

constructor(dependencies: Dependencies) {
this.removeParticipanteUseCase = dependencies.removeParticipanteUseCase
}

async handle(request: CustomHttpRequest, _next: NextHandler): Promise<HttpResponse | HttpError> {
try {
const { expedicaoId, usuarioId } = request.params

if (expedicaoId === undefined || expedicaoId === null || expedicaoId === '' || typeof expedicaoId !== 'string' || !/^\d+$/.test(expedicaoId)) {
return new BadRequestError({ message: 'expedicaoId inválido' })
}

if (usuarioId === undefined || usuarioId === null || usuarioId === '' || typeof usuarioId !== 'string' || !/^\d+$/.test(usuarioId)) {
return new BadRequestError({ message: 'usuarioId inválido' })
}

const result = await this.removeParticipanteUseCase.execute(Number(expedicaoId), Number(usuarioId))

if (result.left()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Standards] Mapeamento por string, mesmo padrão frágil. result.value.name === 'CollectionError' || result.value.message.includes('Falha') — funciona hoje porque o adapter só produz CollectionError ou sucesso, mas o branch "resto vira 400" é código morto: não há nenhuma condição de domínio real (expedição inexistente, participante não encontrado) modelada aqui. Mesmo comentário vale para SubstituirRotaController.ts:43.

if (result.value.name === 'CollectionError' || result.value.message.includes('Falha')) {
return new InternalServerError({ message: 'Falha interna ao remover participante' })
}
return new BadRequestError({ message: result.value.message })
}

return {
statusCode: StatusCode.NoContent,
body: undefined
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Erro inesperado ao remover participante'
return new InternalServerError({ message: errorMessage })
}
}
}
60 changes: 60 additions & 0 deletions src/application/expedicao/SubstituiRotaController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { SubstituiRotasUseCase } from '@/domain/expedicao/SubstituiRotaUseCase'
import {
HttpRequest, HttpResponse, StatusCode
} from '@/library/http/common'
import { BadRequestError } from '@/library/http/error/BadRequestError'
import { HttpError } from '@/library/http/error/HttpError'
import { InternalServerError } from '@/library/http/error/InternalServerError'
import { NextHandler, RequestHandler } from '@/library/http/Server'

interface Dependencies {
SubstituiRotasUseCase: SubstituiRotasUseCase
}

interface CustomHttpRequest extends HttpRequest {
usuario?: {
id: number
tipo_usuario_id: number
}
}

export class SubstituiRotasController implements RequestHandler {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Standards] Nome do arquivo não bate com o da classe (smell). O arquivo é SubstituirRotaController.ts, mas a classe exportada é SubstituiRotasController (com S no final e verbo diferente). Pequeno, mas vale alinhar para não confundir quem for importar/procurar isso depois.

private readonly SubstituiRotasUseCase: SubstituiRotasUseCase

constructor(dependencies: Dependencies) {
this.SubstituiRotasUseCase = dependencies.SubstituiRotasUseCase
}

async handle(request: CustomHttpRequest, _next: NextHandler): Promise<HttpResponse | HttpError> {
try {
const { expedicaoId } = request.params
const { rotas } = request.body as { rotas?: number[] }

if (expedicaoId === undefined || expedicaoId === null || expedicaoId === '' || typeof expedicaoId !== 'string' || !/^\d+$/.test(expedicaoId)) {
return new BadRequestError({ message: 'expedicaoId inválido' })
}

if (!rotas || !Array.isArray(rotas) || rotas.some(id => typeof id !== 'number' || id <= 0)) {
return new BadRequestError({ message: 'rotas inválidas' })
}

const result = await this.SubstituiRotasUseCase.execute(Number(expedicaoId), rotas)

if (result.left()) {
const error = result.value
if (error.name === 'CollectionError') {
return new InternalServerError({ message: 'Falha interna ao substituir rotas' })
}
return new BadRequestError({ message: error.message })
}

return {
statusCode: StatusCode.NoContent,
body: undefined
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Erro inesperado ao substituir rotas'
return new InternalServerError({ message: errorMessage })
}
}
}
33 changes: 33 additions & 0 deletions src/application/expedicao/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import { type Knex } from 'knex'

import { AdicionaParticipanteUseCase } from '@/domain/expedicao/AdcionaParticipanteUseCase'
import { AtualizaExpedicaoUseCase } from '@/domain/expedicao/AtualizaExpedicaoUseCase'
import { BuscaExpedicaoUseCase } from '@/domain/expedicao/BuscaExpedicaoUseCase'
import { CadastraExpedicaoUseCase } from '@/domain/expedicao/CadastraExpedicaoUseCase'
import { DeletaExpedicaoUseCase } from '@/domain/expedicao/DeletaExpedicaoUseCase'
import { ListaExpedicoesUseCase } from '@/domain/expedicao/ListaExpedicoesUseCase'
import { RemoveParticipanteUseCase } from '@/domain/expedicao/RemoveParticipanteUseCase'
import { SubstituiRotasUseCase } from '@/domain/expedicao/SubstituiRotaUseCase'
import { createExpedicaoCollection } from '@/factory/ExpedicaoCollectionFactory'
import { Method } from '@/library/http/common'
import { Route } from '@/library/http/Router'

import { AdicionaParticipanteController } from './AdicionaParticipanteController'
import { AtualizaExpedicaoController } from './AtualizaExpedicaoController'
import { BuscaExpedicaoController } from './BuscaExpedicaoController'
import { CadastraExpedicaoController } from './CadastraExpedicaoController'
import { DeletaExpedicaoController } from './DeletaExpedicaoController'
import { ListaExpedicoesController } from './ListaExpedicoesController'
import { RemoveParticipanteController } from './RemoveParticipanteController'
import { SubstituiRotasController } from './SubstituiRotaController'

export function routes(knex: Knex): Route[] {
const expedicaoCollection = createExpedicaoCollection()
Expand Down Expand Up @@ -63,6 +69,33 @@ export function routes(knex: Knex): Route[] {
atualizaExpedicaoUseCase: new AtualizaExpedicaoUseCase({ expedicaoCollection })
})
]
},
{
method: Method.Post,
path: '/v2/expedicoes/:expedicaoId/participantes',
handlers: [
new AdicionaParticipanteController({
adicionaParticipanteUseCase: new AdicionaParticipanteUseCase({ expedicaoCollection })
})
]
},
{
method: Method.Delete,
path: '/v2/expedicoes/:expedicaoId/participantes/:usuarioId',
handlers: [
new RemoveParticipanteController({
removeParticipanteUseCase: new RemoveParticipanteUseCase({ expedicaoCollection })
})
]
},
{
method: Method.Put,
path: '/v2/expedicoes/:expedicaoId/rotas',
handlers: [
new SubstituiRotasController({
SubstituiRotasUseCase: new SubstituiRotasUseCase({ expedicaoCollection })
})
]
}
]
}
19 changes: 19 additions & 0 deletions src/domain/expedicao/AdcionaParticipanteUseCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Either } from '@/library/either/Either'

import { ExpedicaoCollection } from './ExpedicaoCollection'

interface Dependencies {
expedicaoCollection: ExpedicaoCollection
}

export class AdicionaParticipanteUseCase {
private readonly expedicaoCollection: ExpedicaoCollection

constructor(dependencies: Dependencies) {
this.expedicaoCollection = dependencies.expedicaoCollection
}

execute(expedicaoId: number, usuarioId: number): Promise<Either<Error, void>> {
return this.expedicaoCollection.addParticipant(expedicaoId, usuarioId)
}
}
18 changes: 18 additions & 0 deletions src/domain/expedicao/Expedicao.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,24 @@ export interface Attributes {
updated_by: number | null
}

export interface ParticipanteDetalhado {
id: number
nome: string
email: string
}

export interface RotaDetalhada {
cidade_id: number
ordem: number
nome_cidade: string
estado: string
}

export interface ExpedicaoDetalhada extends Attributes {
participantes: ParticipanteDetalhado[]
rotas: RotaDetalhada[]
}

export type CreateAttributes =
Omit<Attributes, 'id' | 'created_at' | 'updated_at' | 'updated_by'>
& {
Expand Down
10 changes: 8 additions & 2 deletions src/domain/expedicao/ExpedicaoCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ export interface ExpedicaoOrder {
}

export interface ExpedicaoFilters {
pagina?: number
limite?: number
cidade_id?: number
usuario_id?: number
data_inicio_de?: string
data_fim_ate?: string
order?: ExpedicaoOrder
limite?: number
pagina?: number
}

export interface Paginated<T> {
Expand All @@ -39,6 +39,12 @@ export interface ExpedicaoCollection {
findAll(filters: ExpedicaoFilters): Promise<Either<Error, Paginated<ExpedicaoListItem>>>
findById(id: number): Promise<Either<Error, Attributes | null>>
create(attributes: CreateAttributes): Promise<Either<Error, Attributes>>

addParticipant(expedicaoId: number, usuarioId: number): Promise<Either<Error, void>>
removeParticipant(expedicaoId: number, usuarioId: number): Promise<Either<Error, void>>

substituteRoute(expedicaoId: number, rotas: number[]): Promise<Either<Error, void>>

delete(id: number): Promise<Either<Error, void>>
update(id: number, attributes: UpdateAttributes): Promise<Either<Error, Attributes>>
}
19 changes: 19 additions & 0 deletions src/domain/expedicao/RemoveParticipanteUseCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Either } from '@/library/either/Either'

import { ExpedicaoCollection } from './ExpedicaoCollection'

interface Dependencies {
expedicaoCollection: ExpedicaoCollection
}

export class RemoveParticipanteUseCase {
private readonly expedicaoCollection: ExpedicaoCollection

constructor(dependencies: Dependencies) {
this.expedicaoCollection = dependencies.expedicaoCollection
}

execute(expedicaoId: number, usuarioId: number): Promise<Either<Error, void>> {
return this.expedicaoCollection.removeParticipant(expedicaoId, usuarioId)
}
}
19 changes: 19 additions & 0 deletions src/domain/expedicao/SubstituiRotaUseCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Either } from '@/library/either/Either'

import { ExpedicaoCollection } from './ExpedicaoCollection'

interface Dependencies {
expedicaoCollection: ExpedicaoCollection
}

export class SubstituiRotasUseCase {
private readonly expedicaoCollection: ExpedicaoCollection

constructor(dependencies: Dependencies) {
this.expedicaoCollection = dependencies.expedicaoCollection
}

execute(expedicaoId: number, rotas: number[]): Promise<Either<Error, void>> {
return this.expedicaoCollection.substituteRoute(expedicaoId, rotas)
}
}
Loading
Loading