Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
4 changes: 3 additions & 1 deletion src/application/create-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { assets, upload } from '../config/directory'
import legacyErrors from '../middlewares/erros-middleware'
import { generatePreview, reportPreview } from '../reports/controller'
import { routes as createEstadoRoutes } from './estado'
import { routes as createExpedicoesRoutes } from './expedicao'
import { routes as createFaseSucessionalRoutes } from './fase-sucessional'
import { routes as createPaisRoutes } from './pais'
import { routes as createVegetacaoRoutes } from './vegetacao'
Expand Down Expand Up @@ -59,7 +60,8 @@ export function createApp({
...createPaisRoutes(knex),
...createEstadoRoutes(knex),
...createFaseSucessionalRoutes(knex),
...createVegetacaoRoutes(knex)
...createVegetacaoRoutes(knex),
...createExpedicoesRoutes(knex)
]
const application = new ExpressApplication({ logger })

Expand Down
109 changes: 109 additions & 0 deletions src/application/expedicao/AtualizaExpedicaoController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { AtualizaExpedicaoUseCase } from '@/domain/expedicao/AtualizaExpedicaoUseCase'
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 { NotFoundError } from '@/library/http/error/NotFoundError'
import { NextHandler, RequestHandler } from '@/library/http/Server'

interface Dependencies {
atualizaExpedicaoUseCase: AtualizaExpedicaoUseCase
}

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

export class AtualizaExpedicaoController implements RequestHandler {
private readonly atualizaExpedicaoUseCase: AtualizaExpedicaoUseCase

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

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

if (!expedicaoId || Number.isNaN(id)) {
return new BadRequestError({ message: 'O ID da expedição é inválido.' })
}

const {
descricao, data_inicio, data_fim, cidade_id
} = request.body as {
descricao: string | null
data_inicio: string
data_fim: string
cidade_id: number
}

const isValidDate = (dateString: string) => {
if (!/^\d{4}-\d{2}-\d{2}$/.test(dateString)) return false

const [
year,
month,
day
] = dateString.split('-').map(Number)
const date = new Date(year, month - 1, day)

return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day
}

if (typeof data_inicio !== 'string' || !isValidDate(data_inicio)) {
return new BadRequestError({ message: 'A data_inicio é obrigatória e deve ser uma data válida no calendário (formato YYYY-MM-DD).' })
}
if (typeof data_fim !== 'string' || !isValidDate(data_fim)) {
return new BadRequestError({ message: 'A data_fim é obrigatória e deve ser uma data válida no calendário (formato YYYY-MM-DD).' })
}
if (!Number.isInteger(cidade_id) || cidade_id <= 0) {
return new BadRequestError({ message: 'O cidade_id é obrigatório e deve ser um número inteiro válido.' })
}
if (descricao !== undefined && descricao !== null && typeof descricao !== 'string') {
return new BadRequestError({ message: 'A descricao, se informada, deve ser um texto.' })
}

const updated_by = 10 // MOCK TEMPORÁRIO, trocar por request.usuario.id

const result = await this.atualizaExpedicaoUseCase.execute(id, {
descricao: descricao ?? null,
data_inicio,
data_fim,
cidade_id,
updated_by
})

if (result.left()) {
Comment thread
JordanBonfim marked this conversation as resolved.
const error = result.value

// Não encontrado (404)
if (error.message === 'Expedição não encontrada') {
return new NotFoundError({ message: error.message })
}

// Erro de Infraestrutura / Banco de Dados (500)
if (error.name === 'CollectionError' || error.message.includes('Failed to update')) {
return new InternalServerError({ message: 'Falha interna ao atualizar expedição' })
}

// Erro de validação da Entidade / Regra de Negócio (400)
return new BadRequestError({ message: error.message })
}

return {
statusCode: StatusCode.Ok,
body: result.value
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Erro inesperado ao atualizar expedição'
return new InternalServerError({ message: errorMessage })
}
}
}
59 changes: 59 additions & 0 deletions src/application/expedicao/BuscaExpedicaoController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { BuscaExpedicaoUseCase } from '@/domain/expedicao/BuscaExpedicaoUseCase'
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 { NotFoundError } from '@/library/http/error/NotFoundError'
import { NextHandler, RequestHandler } from '@/library/http/Server'

interface Dependencies {
buscaExpedicaoUseCase: BuscaExpedicaoUseCase
}

interface CustomHttpRequest extends HttpRequest {
params: Record<string, string | undefined>
}

export class BuscaExpedicaoController implements RequestHandler {
private readonly buscaExpedicaoUseCase: BuscaExpedicaoUseCase

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

// Busca expedição por ID
// GET /v2/expedicoes/:expedicaoId
async handle(request: CustomHttpRequest, _next: NextHandler): Promise<HttpResponse | HttpError> {
try {
const { expedicaoId } = request.params

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

const result = await this.buscaExpedicaoUseCase.execute({ id: Number(expedicaoId) })

// Falha na infra ou exceção de regra de negócio
if (result.left()) {
Comment thread
JordanBonfim marked this conversation as resolved.
return new InternalServerError({ message: result.value.message })
}

// sucesso na querry, mas a expedição não encontrada
if (!result.value) {
return new NotFoundError({ message: 'Expedição não encontrada' })
}

// sucesso
return {
statusCode: StatusCode.Ok,
body: result.value
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Erro inesperado ao buscar expedição por ID'
return new InternalServerError({ message: errorMessage })
}
}
}
101 changes: 101 additions & 0 deletions src/application/expedicao/CadastraExpedicaoController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { CadastraExpedicaoUseCase } from '@/domain/expedicao/CadastraExpedicaoUseCase'
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'
// import { TIPOS_USUARIOS } from '@/middlewares/tokens-middleware'
// import { UnauthorizedError } from '@/library/http/error/UnauthorizedError'

interface Dependencies {
cadastraExpedicaoUseCase: CadastraExpedicaoUseCase
}

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

export class CadastraExpedicaoController implements RequestHandler {
private readonly cadastraExpedicaoUseCase: CadastraExpedicaoUseCase

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

async handle(request: CustomHttpRequest, _next: NextHandler): Promise<HttpResponse | HttpError> {
try {
// TODO: AUTENTICAÇÃO TEMPORARIAMENTE DESABILITADA
// A infraestrutura de request HTTP autenticada está sendo desenvolvida.
// Assim que integrada, descomentar a validação abaixo e remover o mock do 'created_by'.

// if (!request.usuario || ![TIPOS_USUARIOS.CURADOR, TIPOS_USUARIOS.OPERADOR].includes(request.usuario.tipo_usuario_id)) {
// return new UnauthorizedError({ message: 'Não tem permissão para realizar esta ação' })
// }

const {
descricao, data_inicio, data_fim, cidade_id, participantes, rotas
} = request.body as {
descricao?: string | null
data_inicio: string
data_fim: string
cidade_id: number
participantes?: number[]
rotas?: number[]
}

// VALIDAÇÃO MANUAL DE DADOS
if (!data_inicio || Number.isNaN(Date.parse(data_inicio))) {
return new BadRequestError({ message: 'A data_inicio é obrigatória e deve ser uma data válida (ex: YYYY-MM-DD).' })
}

if (!data_fim || Number.isNaN(Date.parse(data_fim))) {
return new BadRequestError({ message: 'A data_fim é obrigatória e deve ser uma data válida (ex: YYYY-MM-DD).' })
}

if (!cidade_id || typeof cidade_id !== 'number' || cidade_id <= 0) {
return new BadRequestError({ message: 'O cidade_id é obrigatório e deve ser um número válido.' })
}

if (descricao !== undefined && descricao !== null && typeof descricao !== 'string') {
return new BadRequestError({ message: 'A descricao, se informada, deve ser um texto.' })
}

// MOCK TEMPORÁRIO: Substituir pelo request.usuario.id quando a autenticação estiver pronta
const created_by = 9 // Id válido de usuário para teste

// EXECUÇÃO DO CASO DE USO
const result = await this.cadastraExpedicaoUseCase.execute({
descricao: descricao ?? null,
data_inicio,
data_fim,
cidade_id,
created_by,
participantes: participantes ?? [],
rotas: rotas ?? []
})

if (result.left()) {
Comment thread
JordanBonfim marked this conversation as resolved.
// Se for um erro que sabemos ser de banco/infraestrutura devolve 500
if (result.value.name === 'CollectionError' || result.value.message.includes('Failed to create')) {
return new InternalServerError({ message: 'Falha interna ao cadastrar expedição' })
}

// Se for um erro de validação de domínio (ex: data_fim antes de data_inicio) devolve 400
return new BadRequestError({ message: result.value.message })
}

return {
statusCode: StatusCode.Created,
body: result.value
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Erro inesperado ao cadastrar expedição'
return new InternalServerError({ message: errorMessage })
}
}
}
52 changes: 52 additions & 0 deletions src/application/expedicao/DeletaExpedicaoController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { DeletaExpedicaoUseCase } from '@/domain/expedicao/DeletaExpedicaoUseCase'
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 { NotFoundError } from '@/library/http/error/NotFoundError'
import { NextHandler, RequestHandler } from '@/library/http/Server'

interface Dependencies {
deletaExpedicaoUseCase: DeletaExpedicaoUseCase
}

interface CustomHttpRequest extends HttpRequest {
params: Record<string, string | undefined>
}

export class DeletaExpedicaoController implements RequestHandler {
private readonly deletaExpedicaoUseCase: DeletaExpedicaoUseCase

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

async handle(request: CustomHttpRequest, _next: NextHandler): Promise<HttpResponse | HttpError> {
Comment thread
JordanBonfim marked this conversation as resolved.
try {
const { expedicaoId } = request.params

if (expedicaoId === undefined || expedicaoId === null || expedicaoId === '' || !/^\d+$/.test(expedicaoId)) {
return new BadRequestError({ message: 'O ID da expedição é inválido.' })
}

const result = await this.deletaExpedicaoUseCase.execute({ id: Number(expedicaoId) })

if (result.left()) {
// Distingue não encontrado (404) de erro no banco (500)
if (result.value.message === 'Expedição não encontrada') {
return new NotFoundError({ message: result.value.message })
}
return new InternalServerError({ message: result.value.message })
}

return {
statusCode: StatusCode.NoContent
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Erro inesperado ao deletar a expedição.'
return new InternalServerError({ message: errorMessage })
}
}
}
Loading
Loading