From f96480228f4205563594434944487b779ddb610f Mon Sep 17 00:00:00 2001 From: Lucas Siqueira Date: Sun, 9 Jul 2023 17:52:33 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20implementado=20consulta=20por=20codigo?= =?UTF-8?q?=20e=20acrescentado=20na=20documenta=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 64 ++++++++++++++++++- .../migration.sql | 8 +++ prisma/schema.prisma | 3 +- .../infraestructure/pedidos.repository.ts | 35 ++++++++++ .../adapter/driver/pedidos.controller.ts | 5 ++ .../ports/repositories/pedidos.repository.ts | 3 + .../application/services/pedidos.service.ts | 3 + 7 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 prisma/migrations/20230709201029_alter_pedidos_table_set_codigo_pedido_unique/migration.sql diff --git a/README.md b/README.md index 5be28a0..d49f964 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ - Identificação - Pedido - Pagamento +- Exceções e Validações - Testabilidade - Licença - Autores @@ -152,6 +153,7 @@ O sistema efetuará as seguintes validações: - O CPF não pode ter sido cadastrado em outro cliente Caso o cliente já seja cadastrado, ele poderá se identificar pelo endpoint: + [/clientes/{cpf}](http://localhost:3000/api/#/clientes/ClientesController_findByCPF) Também é possível que o cliente prossiga sem se identificar. Dessa forma, o autoatendimento apenas não informará a identificação do mesmo na hora de efetuar o pedido. @@ -159,6 +161,7 @@ Também é possível que o cliente prossiga sem se identificar. Dessa forma, o a ### Pedido Para composição do pedido, primeiramente é necessário consultar as categorias cadastradas no sistema: + [/categorias](http://localhost:3000/api/#/categorias/CategoriasController_findAll) O cliente poderá escolher entre umas das categorias do sistema: @@ -204,6 +207,7 @@ Com a categoria escolhida, o sistema deverá listar os produtos filtrados, os qu ``` Após escolher todos itens necessário, o autoatendimento deve submeter o pedido completo no seguinte endpoint: + [/pedidos](http://localhost:3000/api/#/pedidos/PedidosController_createManyProdutos) ```json @@ -235,6 +239,7 @@ O pedido consiste na identificação do cliente (se estiver identificado), e os O pedido será submetido a uma etapa de processamento do pagamento que será explicada no próximo tópico. Por hora, nesse momento será exemplificado a etapa pós pagamento. Após o processamento do pagamento, o pedido tem o seu STATUS alterado para "RECEBIDO". Dessa forma, ele deve estar disponível para consulta de pedidos pendentes no painel da cozinha, através do seguinte endpoint: + [/pedidos/consultar_pedidos_pendentes](http://localhost:3000/api/#/pedidos/PedidosController_consultarPedidosPendentes) ```json @@ -270,6 +275,7 @@ Após o processamento do pagamento, o pedido tem o seu STATUS alterado para "REC ] ``` Dessa forma, o pedido entra numa fila e fica disponível de acordo com a ordem de criação (o pedido mais antigo para o mais recente) para que algum usuário atuante da Cozinha, inicie a preparação. Ele deve dar algum comando para painel, para que o status do pedido seja atualizado no seguinte endpoint: + [/pedidos/{id}/iniciar_preparacao](http://localhost:3000/api/#/pedidos/PedidosController_iniciarPreparacaoPedido) ```json { @@ -284,6 +290,7 @@ Dessa forma, o pedido entra numa fila e fica disponível de acordo com a ordem d ``` Dessa forma o STATUS do pedido passa para a "EM_PREPARACAO". Ao finalizar a preparação, o usuário da Cozinha deve chamar o endpoint: + [/pedidos/{id}/finalizar_preparacao](http://localhost:3000/api/#/pedidos/PedidosController_finalizarPreparacaoPedido) ```json { @@ -297,6 +304,7 @@ Ao finalizar a preparação, o usuário da Cozinha deve chamar o endpoint: } ``` O STATUS do pedido passa a ser "PRONTO", o que significa ele já pode ser retirado pelo cliente. Quando isso acontecer, deve ser dado o comando para atualizar o STATUS do pedido novamente: + [/pedidos/{id}/finalizar_pedido](http://localhost:3000/api/#/pedidos/PedidosController_finalizarPedido) ```json { @@ -311,6 +319,10 @@ O STATUS do pedido passa a ser "PRONTO", o que significa ele já pode ser retira ``` O pedido passa para "FINALIZADO" e se encerra o fluxo. +Em todo processo é possível consultar pedido atual através do código gerado pelo sistema, através do seguinte endpoint: + +[/pedidos/{codigo_pedido}](http://localhost:3000/api#/pedidos/PedidosController_consultarPedidoPorCodigo) + ### Pagamento ### Antes do pedido prosseguir para cozinha para preparação, ele deve ter seu pagamento processado. Como isso é feito por um sistema externo, foi optado por fazer isso de forma assíncrona. @@ -365,7 +377,57 @@ export class NovoPedidoListener { } ``` -O NovoPedidoListener tenta se comunicar com o cliente de pagamento através da classe PagamentosService do módulo de Pagamentos. A lógica de comunicação com o gateway do Mercado pago está implementada nessa classe. Se por acaso algum erro acontecer durante o processamento do pagamento,o pedido passa ter o STATUS "CANCELADO" e não vai para fila de preparação. Futuramente, lógicas adicionais para tratento de pedidos cancelados podem ser implementadas. +O NovoPedidoListener tenta se comunicar com o cliente de pagamento através da classe PagamentosService do módulo de Pagamentos. A lógica de comunicação com o gateway do Mercado pago está implementada nessa classe. Se por acaso algum erro acontecer durante o processamento do pagamento,o pedido passa ter o STATUS "CANCELADO" e não vai para fila de preparação. Futuramente, lógicas adicionais para tratento de pedidos cancelados podem ser implementadas. + +## :no_entry_sign: Exceções e Validações + +Assim como no DDD, um dos principais objetivos da Arquitetura Hexagonal é separar a complexidade de implementação (camada de infraestrutura) da camada de negócio. Isso deve nortear todo design de código, incluindo as validações e exceções. + +Nesse projeto a validação é feita pela camada de domínio, mas em específico nas classes services. Quando algo não passar por alguma validação, do ponto de vista de negócio isso é uma exceção. Dessa forma, é disparado uma exceção personalizada, conforme exemplo a seguir: +```ts +async create(cliente: Cliente) { + if (!cliente.nome || cliente.nome.trim() === '') + throw new ClienteException('O nome não pode ser vazio'); + + if (!cliente.cpf || cliente.cpf.length != 11) + throw new ClienteException('CPF precisa ter exatamente 11 caracteres'); + + if (cliente.cpf && (await this.clientesRepository.existsByCpf(cliente.cpf))) + throw new ClienteException('CPF já cadastrado.'); + + return this.clientesRepository.create(cliente); + } +``` + +Nessa abordagem, a camada de domínio apenas dispara a exceção e não se preocupa o que fazer com essa exceção. Ou seja, ela informa que houve uma exceção para outra camada que a chamou, agora fica a critério dessa outra camada tratar essa exceção. + +Nesse projeto, foi utilizado um recurso do próprio NestJS para tratamento de exceções chamado "ExceptionFilter". Foi criado uma classe chamada "ValidationFilter" captura automaticamente as exceções de validação, e retorna uma mensagem formatada para API com status code 400: +```ts +@Catch(ClienteException) +export class ValidationFilter implements ExceptionFilter { + catch(exception: Error, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + + response.status(400).json({ + statusCode: 400, + timestamp: new Date().toISOString(), + path: request.url, + error: exception.message, + }); + } +} +``` +```json +{ + "statusCode": 400, + "timestamp": "2023-07-09T20:50:02.056Z", + "path": "/clientes", + "error": "O nome não pode ser vazio" +} +``` +Dessa forma, transferimos a complexidade de implementação de um retorno da API para camada de infraestrutura, mantendo a camada de domínio desacoplada e limpa. ## :microscope: Testabilidade Considerando o uso de portas para a camada de serviços do coração do software se comunicar com serviços externos, torna-se o possível do uso de injeção de dependência para mudar o comportamento padrão do sistema e dessa forma fazer testes de unidades totalmente independentes. Como por exemplo no teste de ClientesService foi injetado um repositório de clientes em memória para que não precisássemos de banco de dados durante os testes de unitários, dessa forma reforçando o conceito de pirâmide testes: diff --git a/prisma/migrations/20230709201029_alter_pedidos_table_set_codigo_pedido_unique/migration.sql b/prisma/migrations/20230709201029_alter_pedidos_table_set_codigo_pedido_unique/migration.sql new file mode 100644 index 0000000..39ca747 --- /dev/null +++ b/prisma/migrations/20230709201029_alter_pedidos_table_set_codigo_pedido_unique/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - A unique constraint covering the columns `[codigo_pedido]` on the table `pedidos` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "pedidos_codigo_pedido_key" ON "pedidos"("codigo_pedido"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3ce40e0..35293ec 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -47,7 +47,7 @@ model Produto { model Pedido { id Int @id @default(autoincrement()) - codigo_pedido String + codigo_pedido String @unique valor_total Float status String @default("INICIADO") createdAt DateTime @default(now()) @@ -94,7 +94,6 @@ model Pagamento { // pedido Pedido @relation(fields: [id_pedido], references: [id]) cliente Cliente? @relation(fields: [id_cliente], references: [id]) - @@map("pagamentos") } diff --git a/src/pedido/adapter/driven/infraestructure/pedidos.repository.ts b/src/pedido/adapter/driven/infraestructure/pedidos.repository.ts index 6432996..abc1632 100644 --- a/src/pedido/adapter/driven/infraestructure/pedidos.repository.ts +++ b/src/pedido/adapter/driven/infraestructure/pedidos.repository.ts @@ -152,4 +152,39 @@ export class PedidosRepository implements IPedidosRepository { return pedido; }); } + findByCodigo(codigo_pedido: string): Promise { + return this.prisma.pedido + .findUnique({ + where: { + codigo_pedido, + }, + include: { + cliente: true, + itens: { + include: { + produto: true, + }, + }, + }, + }) + .then((result) => { + const pedido = new Pedido(); + pedido.id = result.id; + pedido.codigo_pedido = result.codigo_pedido; + pedido.status = + StatusPedido[result.status as keyof typeof StatusPedido]; + pedido.createdAt = result.createdAt; + pedido.updatedAt = result.updatedAt; + pedido.cliente = result.cliente; + pedido.valor_total = result.valor_total; + pedido.itens = result.itens.map((item) => ({ + id: item.id, + quantidade: item.quantidade, + valor: item.valor, + id_produto: item.id_produto, + produto: item.produto, + })); + return pedido; + }); + } } diff --git a/src/pedido/adapter/driver/pedidos.controller.ts b/src/pedido/adapter/driver/pedidos.controller.ts index 3c4be15..87d2808 100644 --- a/src/pedido/adapter/driver/pedidos.controller.ts +++ b/src/pedido/adapter/driver/pedidos.controller.ts @@ -62,4 +62,9 @@ export class PedidosController { return this.pedidosService.finalizarPedido(idAsNumber); } + @Get(':codigo_pedido') + async consultarPedidoPorCodigo(@Param('codigo_pedido') codigo_pedido: string) { + return this.pedidosService.consultarPedidoPorCodigo(codigo_pedido); + } + } diff --git a/src/pedido/core/application/ports/repositories/pedidos.repository.ts b/src/pedido/core/application/ports/repositories/pedidos.repository.ts index 963d1f1..de52db5 100644 --- a/src/pedido/core/application/ports/repositories/pedidos.repository.ts +++ b/src/pedido/core/application/ports/repositories/pedidos.repository.ts @@ -13,4 +13,7 @@ export interface IPedidosRepository { findByStatus(status: StatusPedido); findById(id: number): Promise; + + findByCodigo(codigo_pedido: string): Promise; + } diff --git a/src/pedido/core/application/services/pedidos.service.ts b/src/pedido/core/application/services/pedidos.service.ts index c0694ae..d0d59b2 100644 --- a/src/pedido/core/application/services/pedidos.service.ts +++ b/src/pedido/core/application/services/pedidos.service.ts @@ -109,4 +109,7 @@ export class PedidosService { 0, ); } + consultarPedidoPorCodigo(codigo_pedido: string) { + return this.pedidosRepository.findByCodigo(codigo_pedido); + } }