diff --git a/src/application/create-app.ts b/src/application/create-app.ts index ca1b188d..847312cc 100644 --- a/src/application/create-app.ts +++ b/src/application/create-app.ts @@ -60,6 +60,7 @@ export function createApp({ const routes: Route[] = [ ...createPaisRoutes(knex), ...createEstadoRoutes(knex), + ...createEventoRoutes(knex), ...createFaseSucessionalRoutes(knex), ...createVegetacaoRoutes(knex), ...createEventoRoutes(knex), diff --git a/src/application/evento/BuscarEventoController.ts b/src/application/evento/BuscarEventoController.ts new file mode 100644 index 00000000..5fdc83d4 --- /dev/null +++ b/src/application/evento/BuscarEventoController.ts @@ -0,0 +1,51 @@ +import { BuscarEventoPorIdUseCase } from '@/domain/evento/BuscarEventoPorIdUseCase' +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 { + buscarEventoPorIdUseCase: BuscarEventoPorIdUseCase +} + +export class BuscarEventoController implements RequestHandler { + private readonly buscarEventoPorIdUseCase: BuscarEventoPorIdUseCase + + constructor(dependencies: Dependencies) { + this.buscarEventoPorIdUseCase = dependencies.buscarEventoPorIdUseCase + } + + async handle(request: HttpRequest, _next: NextHandler): Promise { + const { eventoId } = request.params as { eventoId?: string } + + if ( + eventoId === undefined + || eventoId === null + || eventoId === '' + || !/^\d+$/.test(eventoId) + ) { + return new BadRequestError({ message: 'eventoId inválido' }) + } + + const result = await this.buscarEventoPorIdUseCase.execute({ + id: Number(eventoId) + }) + + if (result.left()) { + return new InternalServerError({ message: result.value.message }) + } + + if (!result.value) { + return new NotFoundError({ message: 'Evento não encontrado' }) + } + + return { + statusCode: StatusCode.Ok, + body: result.value + } + } +} diff --git a/src/application/evento/ListaEventosController.ts b/src/application/evento/ListaEventosController.ts new file mode 100644 index 00000000..af1f8535 --- /dev/null +++ b/src/application/evento/ListaEventosController.ts @@ -0,0 +1,72 @@ +import { EventoTipo } from '@/domain/evento/Evento' +import { ListaEventosUseCase } from '@/domain/evento/ListaEventosUseCase' +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 { + listaEventosUseCase: ListaEventosUseCase +} + +export class ListaEventosController implements RequestHandler { + private readonly listaEventosUseCase: ListaEventosUseCase + + constructor(dependencies: Dependencies) { + this.listaEventosUseCase = dependencies.listaEventosUseCase + } + + async handle(request: HttpRequest, _next: NextHandler): Promise { + const { + expedicaoId, + tipo, + capturado_de, + capturado_ate, + limite, + pagina + } = request.params as { + expedicaoId?: string + tipo?: string + capturado_de?: string + capturado_ate?: string + limite?: string + pagina?: string + } + + if ( + expedicaoId === undefined + || expedicaoId === null + || expedicaoId === '' + || !/^\d+$/.test(expedicaoId) + ) { + return new BadRequestError({ message: 'expedicaoId inválido' }) + } + + const filters = { + expedicao_id: Number(expedicaoId), + ...(limite && !Number.isNaN(Number(limite)) + ? { limite: Number(limite) } + : {}), + ...(pagina && !Number.isNaN(Number(pagina)) + ? { pagina: Number(pagina) } + : {}), + ...(tipo ? { tipo: tipo as EventoTipo } : {}), + ...(capturado_de ? { capturado_de: new Date(capturado_de) } : {}), + ...(capturado_ate ? { capturado_ate: new Date(capturado_ate) } : {}) + } + + const result = await this.listaEventosUseCase.execute(filters) + + if (result.left()) { + return new InternalServerError({ message: result.value.message }) + } + + return { + statusCode: StatusCode.Ok, + body: result.value + } + } +} diff --git a/src/application/evento/index.ts b/src/application/evento/index.ts index 0cd3f0cc..812f056d 100644 --- a/src/application/evento/index.ts +++ b/src/application/evento/index.ts @@ -1,20 +1,35 @@ import { type Knex } from 'knex' import { AtualizarEventoUseCase } from '@/domain/evento/AtualizarEventoUseCase' +import { BuscarEventoPorIdUseCase } from '@/domain/evento/BuscarEventoPorIdUseCase' import { CriarEventoUseCase } from '@/domain/evento/CriarEventoUseCase' +import { ListaEventosUseCase } from '@/domain/evento/ListaEventosUseCase' import { RemoverEventoUseCase } from '@/domain/evento/RemoverEventoUseCase' import { EventoCollectionKnexAdapter } from '@/infrastructure/EventoCollectionKnexAdapter' import { Method } from '@/library/http/common' import { Route } from '@/library/http/Router' import { AtualizarEventoController } from './AtualizarEventoController' +import { BuscarEventoController } from './BuscarEventoController' import { CriarEventoController } from './CriarEventoController' +import { ListaEventosController } from './ListaEventosController' import { RemoverEventoController } from './RemoverEventoController' export function routes(knex: Knex): Route[] { const eventoCollection = new EventoCollectionKnexAdapter({ knex }) + // TODO: adicionar autenticação quando a camada HTTP de autenticação estiver disponível. + return [ + { + handlers: [ + new ListaEventosController({ + listaEventosUseCase: new ListaEventosUseCase({ eventoCollection }) + }) + ], + method: Method.Get, + path: '/v2/expedicoes/:expedicaoId/eventos' + }, { handlers: [ new CriarEventoController({ @@ -24,6 +39,15 @@ export function routes(knex: Knex): Route[] { method: Method.Post, path: '/v2/expedicoes/:expedicaoId/eventos' }, + { + handlers: [ + new BuscarEventoController({ + buscarEventoPorIdUseCase: new BuscarEventoPorIdUseCase({ eventoCollection }) + }) + ], + method: Method.Get, + path: '/v2/eventos/:eventoId' + }, { handlers: [ new AtualizarEventoController({ diff --git a/src/domain/evento/BuscarEventoPorIdUseCase.ts b/src/domain/evento/BuscarEventoPorIdUseCase.ts new file mode 100644 index 00000000..76e08343 --- /dev/null +++ b/src/domain/evento/BuscarEventoPorIdUseCase.ts @@ -0,0 +1,20 @@ +import { Either } from '@/library/either/Either' + +import { Attributes } from './Evento' +import { EventoCollection } from './EventoCollection' + +interface Dependencies { + eventoCollection: EventoCollection +} + +export class BuscarEventoPorIdUseCase { + private readonly eventoCollection: EventoCollection + + constructor(dependencies: Dependencies) { + this.eventoCollection = dependencies.eventoCollection + } + + execute({ id }: { id: number }): Promise> { + return this.eventoCollection.findById(id) + } +} diff --git a/src/domain/evento/EventoCollection.ts b/src/domain/evento/EventoCollection.ts index 1be6a2c4..ccd5b4e1 100644 --- a/src/domain/evento/EventoCollection.ts +++ b/src/domain/evento/EventoCollection.ts @@ -15,6 +15,15 @@ export interface EventoFilters { capturado_de?: Date capturado_ate?: Date order?: EventoOrder + limite?: number + pagina?: number +} + +export interface Paginated { + itens: T[] + total: number + limite: number + pagina: number } export interface AtualizarEventoAttributes { @@ -29,7 +38,7 @@ export interface AtualizarEventoAttributes { } export interface EventoCollection { - findAll(filters: EventoFilters): Promise> + findAll(filters: EventoFilters): Promise>> findById(id: number): Promise> create(attributes: CreateAttributes): Promise> update(id: number, attributes: AtualizarEventoAttributes): Promise> diff --git a/src/domain/evento/ListaEventosUseCase.ts b/src/domain/evento/ListaEventosUseCase.ts new file mode 100644 index 00000000..ac2b3909 --- /dev/null +++ b/src/domain/evento/ListaEventosUseCase.ts @@ -0,0 +1,22 @@ +import { Either } from '@/library/either/Either' + +import { Attributes } from './Evento' +import { + EventoCollection, EventoFilters, Paginated +} from './EventoCollection' + +interface Dependencies { + eventoCollection: EventoCollection +} + +export class ListaEventosUseCase { + private readonly eventoCollection: EventoCollection + + constructor(dependencies: Dependencies) { + this.eventoCollection = dependencies.eventoCollection + } + + execute(filters: EventoFilters): Promise>> { + return this.eventoCollection.findAll(filters) + } +} diff --git a/src/infrastructure/EventoCollectionKnexAdapter.ts b/src/infrastructure/EventoCollectionKnexAdapter.ts index f2ba0477..52eeb109 100644 --- a/src/infrastructure/EventoCollectionKnexAdapter.ts +++ b/src/infrastructure/EventoCollectionKnexAdapter.ts @@ -4,7 +4,7 @@ import { Attributes, COLETA_FIELDS, ColetaAttributes, CreateAttributes, EventoTipo } from '@/domain/evento/Evento' import { - AtualizarEventoAttributes, EventoCollection, EventoFilters + AtualizarEventoAttributes, EventoCollection, EventoFilters, Paginated } from '@/domain/evento/EventoCollection' import { Either } from '@/library/either/Either' @@ -106,7 +106,7 @@ export class EventoCollectionKnexAdapter implements EventoCollection { ]) } - async findAll(filters: EventoFilters): Promise> { + async findAll(filters: EventoFilters): Promise>> { try { const query = this.select() @@ -126,11 +126,28 @@ export class EventoCollectionKnexAdapter implements EventoCollection { query.where('eventos.capturado_em', '<=', filters.capturado_ate) } - const order = filters.order ?? { column: 'capturado_em' as const, direction: 'desc' as const } - query.orderBy(`eventos.${order.column}`, order.direction) + const countQuery = query.clone() + + const countRows = await countQuery + .clearSelect() + .count>('* as count') + const [{ count }] = countRows + + const limite = filters.limite && filters.limite > 0 ? filters.limite : 20 + const pagina = filters.pagina && filters.pagina > 0 ? filters.pagina : 1 + const offset = (pagina - 1) * limite + + query.orderBy('eventos.capturado_em', 'desc') + query.orderBy('eventos.id', 'desc') + query.limit(limite).offset(offset) const rows = await query as Array> - return Either.right(rows.map(toAttributes)) + return Either.right({ + itens: rows.map(toAttributes), + total: Number(count), + limite, + pagina + }) } catch (error) { return Either.left(new CollectionError({ message: 'Failed to list eventos', cause: error })) } diff --git a/test/integration/evento/evento-collection.test.ts b/test/integration/evento/evento-collection.test.ts index 3a515dfb..b0d7c778 100644 --- a/test/integration/evento/evento-collection.test.ts +++ b/test/integration/evento/evento-collection.test.ts @@ -168,7 +168,7 @@ describe('EventoCollectionKnexAdapter', () => { expect(found.right()).toBe(true) if (!found.right()) return - expect(found.value.map(evento => evento.id)).toEqual([tarde.value.id, cedo.value.id]) + expect(found.value.itens.map(evento => evento.id)).toEqual([tarde.value.id, cedo.value.id]) } finally { await knex('eventos').whereIn('id', [cedo.value.id, tarde.value.id]).delete() } @@ -186,13 +186,58 @@ describe('EventoCollectionKnexAdapter', () => { expect(found.right()).toBe(true) if (!found.right()) return - expect(found.value.map(evento => evento.id)).toEqual([coleta.value.id]) - expect(found.value[0].coleta).toEqual(ficha) + expect(found.value.itens.map(evento => evento.id)).toEqual([coleta.value.id]) + expect(found.value.itens[0].coleta).toEqual(ficha) } finally { await knex('eventos').whereIn('id', [diario.value.id, coleta.value.id]).delete() } }) + test('filtra por intervalo de captura e pagina os resultados', async () => { + const antes = await collection.create({ + ...novoDiario(), capturado_em: new Date('2026-02-14T10:00:00.000Z') + }) + const dentro1 = await collection.create({ + ...novoDiario(), capturado_em: new Date('2026-02-15T10:00:00.000Z') + }) + const dentro2 = await collection.create({ + ...novaColeta(), capturado_em: new Date('2026-02-15T14:00:00.000Z') + }) + const depois = await collection.create({ + ...novoDiario(), capturado_em: new Date('2026-02-16T10:00:00.000Z') + }) + + expect(antes.right() && dentro1.right() && dentro2.right() && depois.right()).toBe(true) + if (!antes.right() || !dentro1.right() || !dentro2.right() || !depois.right()) return + + try { + const found = await collection.findAll({ + expedicao_id: expedicaoId, + capturado_de: new Date('2026-02-15T00:00:00.000Z'), + capturado_ate: new Date('2026-02-15T23:59:59.999Z'), + limite: 1, + pagina: 2 + }) + + expect(found.right()).toBe(true) + if (!found.right()) return + + expect(found.value.total).toBe(2) + expect(found.value.limite).toBe(1) + expect(found.value.pagina).toBe(2) + expect(found.value.itens.map(evento => evento.id)).toEqual([dentro1.value.id]) + } finally { + await knex('eventos') + .whereIn('id', [ + antes.value.id, + dentro1.value.id, + dentro2.value.id, + depois.value.id + ]) + .delete() + } + }) + test('apaga eventos e fichas junto com a expedição', async () => { const [expedicao] = await knex('expedicoes') .insert({ diff --git a/test/integration/evento/eventos-routes.test.ts b/test/integration/evento/eventos-routes.test.ts new file mode 100644 index 00000000..8f726b70 --- /dev/null +++ b/test/integration/evento/eventos-routes.test.ts @@ -0,0 +1,106 @@ +import { + afterAll, beforeAll, describe, expect, test +} from 'vitest' + +import { createTestApp } from '../setup/app-factory' +import { + cleanupExpedicaoFixtures, ExpedicaoFixtures, seedExpedicaoFixtures +} from '../setup/seeds/expedicao.seed' + +type ListaEventosResponse = { + itens: Array<{ id: number }> + total: number + limite: number + pagina: number +} + +type EventoResponse = { + id: number + tipo: string +} + +describe('Eventos routes', () => { + const { agent, knex } = createTestApp() + + let fixtures: ExpedicaoFixtures + let expedicaoId: number + + beforeAll(async () => { + fixtures = await seedExpedicaoFixtures(knex) + + const [expedicao] = await knex('expedicoes') + .insert({ + descricao: 'XEXP rotas eventos', + data_inicio: '2026-02-14', + data_fim: '2026-02-18', + cidade_id: fixtures.cidades[0] + }) + .returning>(['id']) + + expedicaoId = expedicao.id + }) + + test('lista os eventos da expedição com paginação e ordenação estável', async () => { + const capturadoEm = '2026-02-15T14:32:00.000Z' + + const inserted = await knex('eventos') + .insert([ + { + expedicao_id: expedicaoId, + tipo: 'DIARIO', + capturado_em: capturadoEm + }, + { + expedicao_id: expedicaoId, + tipo: 'COLETA', + capturado_em: capturadoEm + } + ]) + .returning>(['id']) + + try { + const response = await agent + .get(`/api/v2/expedicoes/${expedicaoId}/eventos`) + .expect(200) + + const body = response.body as ListaEventosResponse + + expect(body.total).toBe(2) + expect(body.limite).toBe(20) + expect(body.pagina).toBe(1) + expect(body.itens.map(evento => evento.id)) + .toEqual([...inserted].map(evento => evento.id).sort((a, b) => b - a)) + } finally { + await knex('eventos').whereIn('id', inserted.map(evento => evento.id)).delete() + } + }) + + test('busca um evento pelo id', async () => { + const [evento] = await knex('eventos') + .insert({ + expedicao_id: expedicaoId, + tipo: 'DIARIO', + capturado_em: '2026-02-16T10:00:00.000Z' + }) + .returning>(['id']) + + try { + const response = await agent + .get(`/api/v2/eventos/${evento.id}`) + .expect(200) + + const body = response.body as EventoResponse + + expect(body.id).toBe(evento.id) + expect(body.tipo).toBe('DIARIO') + } finally { + await knex('eventos').where({ id: evento.id }).delete() + } + }) + + afterAll(async () => { + await knex('expedicoes').where({ id: expedicaoId }).delete() + await cleanupExpedicaoFixtures(knex, fixtures) + await knex.destroy() + }) +}) diff --git a/test/unit/application/evento/BuscarEventoController.test.ts b/test/unit/application/evento/BuscarEventoController.test.ts new file mode 100644 index 00000000..c080e773 --- /dev/null +++ b/test/unit/application/evento/BuscarEventoController.test.ts @@ -0,0 +1,125 @@ +import { + describe, expect, test, vi +} from 'vitest' + +import { BuscarEventoController } from '@/application/evento/BuscarEventoController' +import { BuscarEventoPorIdUseCase } from '@/domain/evento/BuscarEventoPorIdUseCase' +import { Either } from '@/library/either/Either' +import { + Headers, HttpRequest, Method, StatusCode +} from '@/library/http/common' +import { BadRequestError } from '@/library/http/error/BadRequestError' +import { NotFoundError } from '@/library/http/error/NotFoundError' + +describe('BuscarEventoController', () => { + const headers = {} as Headers + + test('returns 200 with body when use case finds event', async () => { + const evento = { + id: 1, + expedicao_id: 10, + tipo: 'DIARIO' as const, + capturado_em: new Date('2026-09-15T10:00:00.000Z'), + latitude: null, + longitude: null, + altitude: null, + observacoes: 'Observação', + coleta: null, + created_at: new Date('2026-09-15T10:00:00.000Z'), + updated_at: new Date('2026-09-15T10:00:00.000Z'), + created_by: null, + updated_by: null + } + + const buscarEventoPorIdUseCase = { + execute: vi.fn().mockResolvedValue(Either.right(evento)) + } as unknown as BuscarEventoPorIdUseCase + + const controller = new BuscarEventoController({ + buscarEventoPorIdUseCase + }) + + const request = { + body: {}, + headers, + method: Method.Get, + params: { eventoId: '1' }, + path: '/eventos/1' + } satisfies HttpRequest + + const response = await controller.handle(request, vi.fn()) + + expect(buscarEventoPorIdUseCase.execute).toHaveBeenCalledWith({ + id: 1 + }) + expect('statusCode' in response ? response.statusCode : undefined).toBe(StatusCode.Ok) + expect('body' in response ? response.body : undefined).toEqual(evento) + }) + + test('returns BadRequest when eventoId is invalid', async () => { + const buscarEventoPorIdUseCase = { + execute: vi.fn() + } as unknown as BuscarEventoPorIdUseCase + + const controller = new BuscarEventoController({ + buscarEventoPorIdUseCase + }) + + const request = { + body: {}, + headers, + method: Method.Get, + params: { eventoId: 'abc' }, + path: '/eventos/abc' + } satisfies HttpRequest + + const response = await controller.handle(request, vi.fn()) + + expect(buscarEventoPorIdUseCase.execute).not.toHaveBeenCalled() + expect(response).toBeInstanceOf(BadRequestError) + }) + + test('returns NotFound when event does not exist', async () => { + const buscarEventoPorIdUseCase = { + execute: vi.fn().mockResolvedValue(Either.right(null)) + } as unknown as BuscarEventoPorIdUseCase + + const controller = new BuscarEventoController({ + buscarEventoPorIdUseCase + }) + + const request = { + body: {}, + headers, + method: Method.Get, + params: { eventoId: '1' }, + path: '/eventos/1' + } satisfies HttpRequest + + const response = await controller.handle(request, vi.fn()) + + expect(response).toBeInstanceOf(NotFoundError) + }) + + test('returns InternalServerError when use case fails', async () => { + const buscarEventoPorIdUseCase = { + execute: vi.fn().mockResolvedValue(Either.left(new Error('boom'))) + } as unknown as BuscarEventoPorIdUseCase + + const controller = new BuscarEventoController({ + buscarEventoPorIdUseCase + }) + + const request = { + body: {}, + headers, + method: Method.Get, + params: { eventoId: '1' }, + path: '/eventos/1' + } satisfies HttpRequest + + const response = await controller.handle(request, vi.fn()) + + expect((response as Error).name).toBe('InternalServerError') + }) +}) diff --git a/test/unit/application/evento/ListaEventosController.test.ts b/test/unit/application/evento/ListaEventosController.test.ts new file mode 100644 index 00000000..40d80593 --- /dev/null +++ b/test/unit/application/evento/ListaEventosController.test.ts @@ -0,0 +1,154 @@ +import { + describe, + expect, + test, + vi +} from 'vitest' + +import { ListaEventosController } from '@/application/evento/ListaEventosController' +import { ListaEventosUseCase } from '@/domain/evento/ListaEventosUseCase' +import { Either } from '@/library/either/Either' +import { + Headers, + HttpRequest, + Method, + StatusCode +} from '@/library/http/common' +import { BadRequestError } from '@/library/http/error/BadRequestError' +import { InternalServerError } from '@/library/http/error/InternalServerError' + +const headers: Headers = { + 'Content-Length': 0, + 'Content-Type': 'application/json' +} + +describe('ListaEventosController', () => { + test('returns 200 with body when use case succeeds', async () => { + const lista = [ + { + id: 1, + expedicao_id: 10, + tipo: 'DIARIO' as const, + capturado_em: new Date('2026-09-01T10:00:00.000Z') + } + ] + + const resultado = { + itens: lista, + total: 1, + limite: 20, + pagina: 1 + } + + const listaEventosUseCase = { + execute: vi.fn().mockResolvedValue(Either.right(resultado)) + } as unknown as ListaEventosUseCase + + const controller = new ListaEventosController({ listaEventosUseCase }) + + const request = { + body: {}, + headers, + method: Method.Get, + params: { + expedicaoId: '10' + }, + path: '/expedicoes/10/eventos' + } satisfies HttpRequest + + const response = await controller.handle(request, vi.fn()) + + expect(response).toEqual({ + statusCode: StatusCode.Ok, + body: resultado + }) + }) + + test('returns BadRequest when expedicaoId is invalid', async () => { + const listaEventosUseCase = { + execute: vi.fn() + } as unknown as ListaEventosUseCase + + const controller = new ListaEventosController({ listaEventosUseCase }) + + const request = { + body: {}, + headers, + method: Method.Get, + params: { + expedicaoId: 'abc' + }, + path: '/expedicoes/abc/eventos' + } satisfies HttpRequest + + const response = await controller.handle(request, vi.fn()) + + expect(response).toBeInstanceOf(BadRequestError) + expect(listaEventosUseCase.execute).not.toHaveBeenCalled() + }) + + test('returns InternalServerError when use case fails', async () => { + const listaEventosUseCase = { + execute: vi.fn().mockResolvedValue( + Either.left(new Error('erro ao listar eventos')) + ) + } as unknown as ListaEventosUseCase + + const controller = new ListaEventosController({ listaEventosUseCase }) + + const request = { + body: {}, + headers, + method: Method.Get, + params: { + expedicaoId: '10' + }, + path: '/expedicoes/10/eventos' + } satisfies HttpRequest + + const response = await controller.handle(request, vi.fn()) + + expect(response).toBeInstanceOf(InternalServerError) + }) + + test('passes filters and pagination to use case', async () => { + const resultado = { + itens: [], + total: 0, + limite: 10, + pagina: 2 + } + + const listaEventosUseCase = { + execute: vi.fn().mockResolvedValue(Either.right(resultado)) + } as unknown as ListaEventosUseCase + + const controller = new ListaEventosController({ listaEventosUseCase }) + + const request = { + body: {}, + headers, + method: Method.Get, + params: { + expedicaoId: '10', + tipo: 'COLETA', + capturado_de: '2026-09-01T00:00:00.000Z', + capturado_ate: '2026-09-15T23:59:59.999Z', + limite: '10', + pagina: '2' + }, + path: '/expedicoes/10/eventos' + } satisfies HttpRequest + + await controller.handle(request, vi.fn()) + + expect(listaEventosUseCase.execute).toHaveBeenCalledWith({ + expedicao_id: 10, + tipo: 'COLETA', + capturado_de: new Date('2026-09-01T00:00:00.000Z'), + capturado_ate: new Date('2026-09-15T23:59:59.999Z'), + limite: 10, + pagina: 2 + }) + }) +})