diff --git a/.env.template b/.env.template index 2eea0a41..dee4725b 100644 --- a/.env.template +++ b/.env.template @@ -6,6 +6,7 @@ DB_NAME=rms DB_SSL=false # Mercado Pago +ENABLE_MERCADOPAGO=false ACCESS_TOKEN_MERCADOPAGO= USER_ID_MERCADOPAGO= EXTERNAL_POS_ID_MERCADOPAGO= diff --git a/docker-compose.yml b/docker-compose.yml index 2e8a8b52..3d06af88 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -47,6 +47,7 @@ services: DB_PASSWORD: ${DB_PASSWORD:-pgpwd} DB_NAME: ${DB_NAME:-rms} DB_SSL=: ${DB_SSL:-false} + ENABLE_MERCADOPAGO=: ${ENABLE_MERCADOPAGO:-false} ACCESS_TOKEN_MERCADOPAGO: ${ACCESS_TOKEN_MERCADOPAGO:-} USER_ID_MERCADOPAGO: ${USER_ID_MERCADOPAGO:-} EXTERNAL_POS_ID_MERCADOPAGO: ${EXTERNAL_POS_ID_MERCADOPAGO:-} diff --git a/k8s/development/bff/config.yaml b/k8s/development/bff/config.yaml index ecef20d0..2bab93fd 100644 --- a/k8s/development/bff/config.yaml +++ b/k8s/development/bff/config.yaml @@ -12,6 +12,7 @@ data: DB_PASSWORD: "pgpwd" DB_NAME: "rms" DB_SSL: "false" + ENABLE_MERCADOPAGO: "false" ACCESS_TOKEN_MERCADOPAGO: "" USER_ID_MERCADOPAGO: "" EXTERNAL_POS_ID_MERCADOPAGO: "" diff --git a/k8s/production/bff/config.yaml b/k8s/production/bff/config.yaml index 6b3ee5be..4ac5c266 100644 --- a/k8s/production/bff/config.yaml +++ b/k8s/production/bff/config.yaml @@ -12,6 +12,7 @@ data: DB_PASSWORD: "" DB_NAME: "" DB_SSL: "" + ENABLE_MERCADOPAGO: "true" ACCESS_TOKEN_MERCADOPAGO: "" USER_ID_MERCADOPAGO: "" EXTERNAL_POS_ID_MERCADOPAGO: "" diff --git a/src/adapters/inbound/rest/v1/presenters/pedido.dto.ts b/src/adapters/inbound/rest/v1/presenters/pedido.dto.ts index 71966ca5..9532074a 100644 --- a/src/adapters/inbound/rest/v1/presenters/pedido.dto.ts +++ b/src/adapters/inbound/rest/v1/presenters/pedido.dto.ts @@ -44,6 +44,9 @@ export class PedidoDTO { @ApiProperty({ description: 'Status do pedido' }) statusPedido: string; + @ApiProperty({ description: 'Status do pagamento' }) + pago: boolean; + @ApiProperty({ description: 'Cliente associado ao pedido' }) cliente: ClienteDTO; diff --git a/src/adapters/outbound/models/pedido.model.ts b/src/adapters/outbound/models/pedido.model.ts index cbfcb78c..45fcee4c 100644 --- a/src/adapters/outbound/models/pedido.model.ts +++ b/src/adapters/outbound/models/pedido.model.ts @@ -28,6 +28,9 @@ export class PedidoModel { @JoinColumn({ name: 'id_cliente' }) cliente: ClienteModel | null; + @Column({ name: 'pago', nullable: false }) + pago: boolean; + @Column({ name: 'status_pedido', length: 20, nullable: false }) statusPedido: string; diff --git a/src/adapters/outbound/repositories/pedido/pedido.repository.spec.ts b/src/adapters/outbound/repositories/pedido/pedido.repository.spec.ts index f3396244..fefb11c1 100644 --- a/src/adapters/outbound/repositories/pedido/pedido.repository.spec.ts +++ b/src/adapters/outbound/repositories/pedido/pedido.repository.spec.ts @@ -83,6 +83,28 @@ describe('PedidoRepository', () => { expect(result).toBe(pedidoModelMock); }); + it('deve alterar o status de pagamento do pedido', async () => { + const novoStatusPagamento = true; + + pedidoTypeORMMock.findOne.mockResolvedValue( + Promise.resolve(pedidoModelMock), + ); + + const result = await pedidoRepository.editarStatusPagamento( + pedidoId, + novoStatusPagamento, + ); + + expect(pedidoTypeORMMock.update).toHaveBeenCalledWith(pedidoId, { + pago: novoStatusPagamento, + }); + expect(pedidoTypeORMMock.findOne).toHaveBeenCalledWith({ + where: { id: pedidoId }, + relations: relations, + }); + expect(result).toBe(pedidoModelMock); + }); + it('deve editar o status de um pedido', async () => { const novoStatusPedido = 'recebido'; diff --git a/src/adapters/outbound/repositories/pedido/pedido.repository.ts b/src/adapters/outbound/repositories/pedido/pedido.repository.ts index c89c81a7..e969aef7 100644 --- a/src/adapters/outbound/repositories/pedido/pedido.repository.ts +++ b/src/adapters/outbound/repositories/pedido/pedido.repository.ts @@ -58,6 +58,20 @@ export class PedidoRepository implements IPedidoRepository { }); } + async editarStatusPagamento( + pedidoId: string, + statusPagamento: boolean, + ): Promise { + await this.pedidoRepository.update(pedidoId, { + pago: statusPagamento, + }); + + return await this.pedidoRepository.findOne({ + where: { id: pedidoId }, + relations: this.relations, + }); + } + async buscarPedido(pedidoId: string): Promise { return await this.pedidoRepository.findOne({ where: { id: pedidoId }, diff --git a/src/domain/entities/pedido/pedido.entity.spec.ts b/src/domain/entities/pedido/pedido.entity.spec.ts index b03d6488..6343c5e3 100644 --- a/src/domain/entities/pedido/pedido.entity.spec.ts +++ b/src/domain/entities/pedido/pedido.entity.spec.ts @@ -9,6 +9,7 @@ describe('PedidoEntity', () => { let itensPedido: ItemPedidoEntity[]; let statusPedido: StatusPedido; let numeroPedido: string; + let pago: boolean; let cliente: ClienteEntity; let id: string; @@ -17,6 +18,7 @@ describe('PedidoEntity', () => { itensPedido = [itemPedidoEntityMock]; statusPedido = StatusPedido.RECEBIDO; numeroPedido = '05012024'; + pago = true cliente = clienteEntityMock; id = '0a14aa4e-75e7-405f-8301-81f60646c93d'; }); @@ -26,6 +28,7 @@ describe('PedidoEntity', () => { itensPedido, statusPedido, numeroPedido, + pago, cliente, id, ); @@ -33,16 +36,23 @@ describe('PedidoEntity', () => { expect(pedido.itensPedido).toEqual(itensPedido); expect(pedido.statusPedido).toEqual(statusPedido); expect(pedido.numeroPedido).toEqual(numeroPedido); + expect(pedido.pago).toEqual(pago); expect(pedido.cliente).toEqual(cliente); expect(pedido.id).toEqual(id); }); it('deve criar uma instância de PedidoEntity sem cliente e id', () => { - const pedido = new PedidoEntity(itensPedido, statusPedido, numeroPedido); + const pedido = new PedidoEntity( + itensPedido, + statusPedido, + numeroPedido, + pago, + ); expect(pedido.itensPedido).toEqual(itensPedido); expect(pedido.statusPedido).toEqual(statusPedido); expect(pedido.numeroPedido).toEqual(numeroPedido); + expect(pedido.pago).toEqual(pago); expect(pedido.cliente).toBeUndefined(); expect(pedido.id).toBeUndefined(); }); diff --git a/src/domain/entities/pedido/pedido.entity.ts b/src/domain/entities/pedido/pedido.entity.ts index c8f089df..e16edaea 100644 --- a/src/domain/entities/pedido/pedido.entity.ts +++ b/src/domain/entities/pedido/pedido.entity.ts @@ -6,6 +6,7 @@ export class PedidoEntity { private _itensPedido: ItemPedidoEntity[]; private _statusPedido: StatusPedido; private _numeroPedido: string; + private _pago: boolean; private _cliente?: ClienteEntity; private _id?: string; @@ -13,11 +14,13 @@ export class PedidoEntity { itensPedido: ItemPedidoEntity[], statusPedido: StatusPedido, numeroPedido: string, + pago: boolean, cliente?: ClienteEntity, id?: string, ) { this.id = id; this.numeroPedido = numeroPedido; + this.pago = pago; this.itensPedido = itensPedido; this.cliente = cliente; this.statusPedido = statusPedido; @@ -47,6 +50,14 @@ export class PedidoEntity { this._numeroPedido = numeroPedido; } + get pago(): boolean { + return this._pago; + } + + set pago(pago: boolean) { + this._pago = pago; + } + get cliente(): ClienteEntity | undefined { return this._cliente; } diff --git a/src/domain/factories/pedido/pedido.dto.factory.ts b/src/domain/factories/pedido/pedido.dto.factory.ts index fc0dadff..892dc6b9 100644 --- a/src/domain/factories/pedido/pedido.dto.factory.ts +++ b/src/domain/factories/pedido/pedido.dto.factory.ts @@ -30,6 +30,7 @@ export class PedidoDTOFactory implements IPedidoDTOFactory { pedidoDTO.id = pedido.id; pedidoDTO.numeroPedido = pedido.numeroPedido; pedidoDTO.itensPedido = itensPedido; + pedidoDTO.pago = pedido.pago; pedidoDTO.statusPedido = pedido.statusPedido; pedidoDTO.cliente = cliente; return pedidoDTO; @@ -48,6 +49,7 @@ export class PedidoDTOFactory implements IPedidoDTOFactory { pedidoDTO.id = pedido.id; pedidoDTO.numeroPedido = pedido.numeroPedido; pedidoDTO.itensPedido = itensPedido; + pedidoDTO.pago = pedido.pago; pedidoDTO.statusPedido = pedido.statusPedido; pedidoDTO.cliente = cliente; return pedidoDTO; diff --git a/src/domain/factories/pedido/pedido.factory.ts b/src/domain/factories/pedido/pedido.factory.ts index 5f808c4e..26416ea6 100644 --- a/src/domain/factories/pedido/pedido.factory.ts +++ b/src/domain/factories/pedido/pedido.factory.ts @@ -88,6 +88,7 @@ export class PedidoFactory implements IPedidoFactory { itensPedido, StatusPedido.RECEBIDO, numeroPedido, + false, clienteEntity, ); } diff --git a/src/domain/ports/pedido/pedido.repository.port.ts b/src/domain/ports/pedido/pedido.repository.port.ts index 7655799d..f4852017 100644 --- a/src/domain/ports/pedido/pedido.repository.port.ts +++ b/src/domain/ports/pedido/pedido.repository.port.ts @@ -3,11 +3,15 @@ import { PedidoModel } from 'src/adapters/outbound/models/pedido.model'; export interface IPedidoRepository { criarPedido(pedido: PedidoEntity): Promise; + buscarPedido(pedidoId: string): Promise; editarStatusPedido( pedidoId: string, statusPedido: string, ): Promise; - buscarPedido(pedidoId: string): Promise; + editarStatusPagamento( + pedidoId: string, + statusPagamento: boolean, + ): Promise; listarPedidos(): Promise; listarPedidosRecebido(): Promise; } diff --git a/src/domain/use_cases/pedido/pedido.use_case.spec.ts b/src/domain/use_cases/pedido/pedido.use_case.spec.ts index ce0f81b8..b88b6de7 100644 --- a/src/domain/use_cases/pedido/pedido.use_case.spec.ts +++ b/src/domain/use_cases/pedido/pedido.use_case.spec.ts @@ -13,9 +13,13 @@ import { atualizaPedidoDTOMock, pedidoDTOMock, pedidoDTOFactoryMock, + configServiceMock, + mensagemGatewayPagamentoDTO, + pedidoGatewayPagamentoDTO, } from 'src/mocks/pedido.mock'; import { IPedidoDTOFactory } from 'src/domain/ports/pedido/pedido.dto.factory.port'; import { PedidoNaoLocalizadoErro } from 'src/domain/exceptions/pedido.exception'; +import { ConfigService } from '@nestjs/config'; describe('PedidoUseCase', () => { let pedidoUseCase: PedidoUseCase; @@ -41,6 +45,10 @@ describe('PedidoUseCase', () => { provide: IPedidoDTOFactory, useValue: pedidoDTOFactoryMock, }, + { + provide: ConfigService, + useValue: configServiceMock, + }, ], }).compile(); @@ -99,6 +107,34 @@ describe('PedidoUseCase', () => { }); }); + it('deve atualizar o status de pagamento do pedido com sucesso', async () => { + const idPedidoMercadoPago = '15171882961'; + const topicMercadoPago = 'merchant_order'; + + pedidoRepositoryMock.buscarPedido.mockReturnValue(pedidoModelMock); + pedidoRepositoryMock.editarStatusPedido.mockReturnValue(pedidoModelMock); + pedidoDTOFactoryMock.criarPedidoDTO.mockReturnValue(pedidoDTOMock); + gatewayPagamentoServiceMock.consultarPedido.mockReturnValue(pedidoGatewayPagamentoDTO); + + const result = await pedidoUseCase.webhookPagamento( + idPedidoMercadoPago, + topicMercadoPago, + mensagemGatewayPagamentoDTO, + ); + + expect(pedidoRepositoryMock.editarStatusPagamento).toHaveBeenCalledWith( + pedidoId, + true, + ); + expect(pedidoRepositoryMock.editarStatusPedido).toHaveBeenCalledWith( + pedidoId, + 'em preparacao', + ); + expect(result).toStrictEqual({ + mensagem: 'Mensagem consumida com sucesso' + }); + }); + it('deve retornar erro ao editar um pedido não existe', async () => { pedidoRepositoryMock.buscarPedido.mockReturnValue(null); diff --git a/src/domain/use_cases/pedido/pedido.use_case.ts b/src/domain/use_cases/pedido/pedido.use_case.ts index c5bb8396..53ffc41e 100644 --- a/src/domain/use_cases/pedido/pedido.use_case.ts +++ b/src/domain/use_cases/pedido/pedido.use_case.ts @@ -16,6 +16,7 @@ import { IPedidoUseCase } from 'src/domain/ports/pedido/pedido.use_case.port'; import { IGatewayPagamentoService } from 'src/domain/ports/pedido/gatewaypag.service.port'; import { HTTPResponse } from 'src/utils/HTTPResponse'; import { PedidoModel } from 'src/adapters/outbound/models/pedido.model'; +import { ConfigService } from '@nestjs/config'; @Injectable() export class PedidoUseCase implements IPedidoUseCase { @@ -26,6 +27,7 @@ export class PedidoUseCase implements IPedidoUseCase { private readonly pedidoFactory: IPedidoFactory, @Inject(IGatewayPagamentoService) private readonly gatewayPagamentoService: IGatewayPagamentoService, + private configService: ConfigService, @Inject(IPedidoDTOFactory) private readonly pedidoDTOFactory: IPedidoDTOFactory, ) {} @@ -46,10 +48,14 @@ export class PedidoUseCase implements IPedidoUseCase { const result = await this.pedidoRepository.criarPedido(pedidoEntity); pedidoEntity.id = result.id; - const qrData = await this.gatewayPagamentoService.criarPedido(pedidoEntity); - const pedidoDTO = this.pedidoDTOFactory.criarPedidoDTO(result); - pedidoDTO.qrCode = qrData; + + const mercadoPagoIsEnabled = this.configService.get('ENABLE_MERCADOPAGO').toLowerCase() === 'true'; + + if (mercadoPagoIsEnabled) { + const qrData = await this.gatewayPagamentoService.criarPedido(pedidoEntity); + pedidoDTO.qrCode = qrData; + } return { mensagem: 'Pedido criado com sucesso', @@ -101,6 +107,7 @@ export class PedidoUseCase implements IPedidoUseCase { mensagem: MensagemGatewayPagamentoDTO, ): Promise { if (id && topic === 'merchant_order') { + console.log(mensagem); const pedidoGatewayPag = await this.gatewayPagamentoService.consultarPedido(id); const idInternoPedido = pedidoGatewayPag.external_reference; @@ -110,13 +117,17 @@ export class PedidoUseCase implements IPedidoUseCase { if (!buscaPedido) { throw new PedidoNaoLocalizadoErro('Pedido não localizado'); } + await this.pedidoRepository.editarStatusPagamento( + idInternoPedido, + true, + ); await this.pedidoRepository.editarStatusPedido( idInternoPedido, 'em preparacao', ); } return { - mensagem: `Mensagem ${mensagem} consumida com sucesso`, + mensagem: 'Mensagem consumida com sucesso', }; } } @@ -125,9 +136,10 @@ export class PedidoUseCase implements IPedidoUseCase { pedidoGatewayPag: PedidoGatewayPagamentoDTO, ): boolean { if ( - pedidoGatewayPag.order_status === 'paid' && + pedidoGatewayPag.status === 'closed' && // closed: Order with payments covering total amount. + pedidoGatewayPag.order_status === 'paid' && // paid: Order with the sum of all payments "approved", "chargeback" or "in_mediation", covers the order total amount. pedidoGatewayPag.payments.every((payment) => { - return payment.status === 'approved'; + return payment.status === 'approved'; // approved: The payment has been approved and accredited. }) ) { return true; diff --git a/src/mocks/pedido.mock.ts b/src/mocks/pedido.mock.ts index 57c441f9..f6227251 100644 --- a/src/mocks/pedido.mock.ts +++ b/src/mocks/pedido.mock.ts @@ -17,6 +17,7 @@ import { itemPedidoEntityMock, itemPedidoModelMock, } from './item_pedido.mock'; +import { MensagemGatewayPagamentoDTO, PaymentDTO, PedidoGatewayPagamentoDTO } from 'src/adapters/inbound/rest/v1/presenters/gatewaypag.dto'; // Mock para simular dados da tabela pedido no banco de dados export const pedidoModelMock = new PedidoModel(); @@ -24,6 +25,7 @@ pedidoModelMock.id = '0a14aa4e-75e7-405f-8301-81f60646c93d'; pedidoModelMock.numeroPedido = '05012024'; pedidoModelMock.itensPedido = [itemPedidoModelMock]; pedidoModelMock.cliente = clienteModelMock; +pedidoModelMock.pago = false; pedidoModelMock.statusPedido = 'recebido'; pedidoModelMock.criadoEm = new Date().toISOString(); pedidoModelMock.atualizadoEm = new Date().toISOString(); @@ -33,6 +35,7 @@ export const pedidoEntityMock = new PedidoEntity( [itemPedidoEntityMock], StatusPedido.RECEBIDO, '05012024', + false, clienteEntityMock, ); @@ -52,10 +55,24 @@ export const pedidoDTOMock = new PedidoDTO(); pedidoDTOMock.id = pedidoModelMock.id; pedidoDTOMock.numeroPedido = pedidoModelMock.numeroPedido; pedidoDTOMock.itensPedido = [itemPedidoDTOMock]; +pedidoDTOMock.pago = false; pedidoDTOMock.statusPedido = pedidoModelMock.statusPedido; pedidoDTOMock.cliente = clienteDTOMock; pedidoDTOMock.qrCode = null; +export const mensagemGatewayPagamentoDTO = new MensagemGatewayPagamentoDTO(); +mensagemGatewayPagamentoDTO.resource = 'https://api.mercadolibre.com/merchant_orders/15171882961'; +mensagemGatewayPagamentoDTO.topic = 'merchant_order' + +export const pedidoGatewayPagamentoDTO = new PedidoGatewayPagamentoDTO(); +pedidoGatewayPagamentoDTO.id = 15171882961; +pedidoGatewayPagamentoDTO.status = 'closed'; +pedidoGatewayPagamentoDTO.external_reference = '0a14aa4e-75e7-405f-8301-81f60646c93d'; +const itemDTO = new PaymentDTO(); +itemDTO.status = "approved"; +pedidoGatewayPagamentoDTO.payments = [itemDTO]; +pedidoGatewayPagamentoDTO.order_status = 'paid'; + // Mock jest das funções do typeORM interagindo com a tabela pedido export const pedidoTypeORMMock: jest.Mocked> = { create: jest.fn(), @@ -67,10 +84,19 @@ export const pedidoTypeORMMock: jest.Mocked> = { Repository >; +export const configServiceMock = { + get: jest.fn((key: string) => { + if (key === 'ENABLE_MERCADOPAGO') { + return 'false'; + } + }) +} + // Mock jest das funções do repository pedido export const pedidoRepositoryMock = { criarPedido: jest.fn(), editarStatusPedido: jest.fn(), + editarStatusPagamento: jest.fn(), buscarPedido: jest.fn(), listarPedidos: jest.fn(), listarPedidosRecebido: jest.fn(),