diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml new file mode 100644 index 0000000..993c9a8 --- /dev/null +++ b/.github/workflows/run-unit-tests.yml @@ -0,0 +1,20 @@ +name: Run Unit Tests + +on: [push] + +jobs: + run-unit-tests: + name: Run Unit Tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: 18 + cache: "npm" + + - run: npm ci + + - run: npm run test diff --git a/README.md b/README.md index 6452330..b23879a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # FastFeet API - Desafio Rocketseat +[![Typescript Badge](https://img.shields.io/badge/TypeScript-20232A?style=for-the-badge&logo=typescript&logoColor=007acd&link=https://gist.github.com/bruno-valero/302a8b36f8fb5749bd15866b523b315e)](https://gist.github.com/bruno-valero/302a8b36f8fb5749bd15866b523b315e) +[![NodeJS Badge](https://img.shields.io/badge/Node.js-20232A?style=for-the-badge&logo=node.js&logoColor=68a063&link=https://gist.github.com/bruno-valero/9c4167a53b05049712ee0333c5664904)](https://gist.github.com/bruno-valero/9c4167a53b05049712ee0333c5664904) +[![NestJS Badge](https://img.shields.io/badge/Nest.js-20232A?style=for-the-badge&logo=nestjs&logoColor=f00057&link=https://gist.github.com/bruno-valero/9c790eee84ac5cecbf41962c79098f9d)](https://gist.github.com/bruno-valero/9c790eee84ac5cecbf41962c79098f9d) + Nesse desafio desenvolveremos uma API para controle de encomendas de uma transportadora fictícia, a FastFeet. Confira o enunciado do desafio no [Notion](https://efficient-sloth-d85.notion.site/Desafio-04-a3a2ef9297ad47b1a94f89b197274ffd) diff --git a/src/domain/core/deliveries-and-orders/enterprise/entities/order.ts b/src/domain/core/deliveries-and-orders/enterprise/entities/order.ts index 26d42e3..9f60885 100644 --- a/src/domain/core/deliveries-and-orders/enterprise/entities/order.ts +++ b/src/domain/core/deliveries-and-orders/enterprise/entities/order.ts @@ -17,6 +17,7 @@ import { OrderAttachment } from './order-attachment' import { OrderAlreadyReturnedError } from '@/core/errors/errors/order-errors/order-already-returned-error copy' import { OrderNotAwaitingPickupError } from '@/core/errors/errors/order-errors/order-not-awaiting-for-pickup-error' import { OrderWasNotCollectedError } from '@/core/errors/errors/order-errors/order-was-not-collected-error' +import { OrderCourierCollectedEvent } from '../events/order-courier-collected-event' export interface OrderProps { recipientId: UniqueEntityId @@ -174,7 +175,7 @@ export class Order extends AggregateRoot { const updateOrder = this.update(() => { this.props.collected = new Date() - this.addDomainEvent(new OrderCourierAcceptedEvent(this)) + this.addDomainEvent(new OrderCourierCollectedEvent(this)) }, updatedBy) return { data: updateOrder } @@ -253,14 +254,22 @@ export class Order extends AggregateRoot { get actions() { // adm - const admSetAwaitingPickup = this.admSetAwaitingPickup.bind(this) - const admCollected = this.admCollected.bind(this) - const admReturned = this.admReturned.bind(this) + const admSetAwaitingPickup = this.admSetAwaitingPickup.bind( + this, + ) as Order['admSetAwaitingPickup'] + const admCollected = this.admCollected.bind(this) as Order['admCollected'] + const admReturned = this.admReturned.bind(this) as Order['admReturned'] // courier - const courierAccept = this.courierAccept.bind(this) - const courierReject = this.courierReject.bind(this) - const courierDeliver = this.courierDeliver.bind(this) + const courierAccept = this.courierAccept.bind( + this, + ) as Order['courierAccept'] + const courierReject = this.courierReject.bind( + this, + ) as Order['courierReject'] + const courierDeliver = this.courierDeliver.bind( + this, + ) as Order['courierDeliver'] return { courier: { diff --git a/src/domain/core/deliveries-and-orders/enterprise/events/order-courier-accepted-event.spec.ts b/src/domain/core/deliveries-and-orders/enterprise/events/order-courier-accepted-event.spec.ts index 714a612..6b78b4e 100644 --- a/src/domain/core/deliveries-and-orders/enterprise/events/order-courier-accepted-event.spec.ts +++ b/src/domain/core/deliveries-and-orders/enterprise/events/order-courier-accepted-event.spec.ts @@ -1,5 +1,5 @@ import UniqueEntityId from '@/core/entities/unique-entity-id' -import { OnOrderAwaitingForPickup } from '@/domain/generic/notification/application/subscribers/on-order-awaiting-for-pickup' +import { OnOrderCourierAccepted } from '@/domain/generic/notification/application/subscribers/on-order-courier-accepted' import { SendNotificationUseCaseRequest, SendNotificationUseCaseResponse, @@ -18,7 +18,7 @@ let awaitingPickup = makeMarkOrderAsAwaitingForPickupUseCase({ ordersRepositoryAlt: createOrder.dependencies.ordersRepository, }) let createNotification = makeSendNotificationUseCase() -let sut: OnOrderAwaitingForPickup // eslint-disable-line +let sut: OnOrderCourierAccepted // eslint-disable-line let sendNotificationSpy: MockInstance< [SendNotificationUseCaseRequest], @@ -34,7 +34,7 @@ describe('on courier accept order', () => { }) createNotification = makeSendNotificationUseCase() - sut = new OnOrderAwaitingForPickup( + sut = new OnOrderCourierAccepted( createOrder.dependencies.ordersRepository, createNotification.useCase, ) @@ -52,7 +52,7 @@ describe('on courier accept order', () => { requestResponsibleId: adm.id.value, creationProps: { address: makeAddress(), - courierId: new UniqueEntityId('123'), + courierId: null, recipientId: new UniqueEntityId('1188'), }, }) @@ -61,6 +61,10 @@ describe('on courier accept order', () => { await createOrder.dependencies.ordersRepository.findMany() )[0] + order.actions.courier.courierAccept(new UniqueEntityId('123'), adm.id) + + await createOrder.dependencies.ordersRepository.update(order) + await awaitingPickup.useCase.execute({ orderId: order.id.value, requestResponsibleId: adm.id.value, diff --git a/src/domain/core/deliveries-and-orders/enterprise/events/order-courier-canceled-event.spec.ts b/src/domain/core/deliveries-and-orders/enterprise/events/order-courier-canceled-event.spec.ts new file mode 100644 index 0000000..5fe56b2 --- /dev/null +++ b/src/domain/core/deliveries-and-orders/enterprise/events/order-courier-canceled-event.spec.ts @@ -0,0 +1,77 @@ +import UniqueEntityId from '@/core/entities/unique-entity-id' +import { OnOrderCourierCanceled } from '@/domain/generic/notification/application/subscribers/on-order-courier-canceled' +import { + SendNotificationUseCaseRequest, + SendNotificationUseCaseResponse, +} from '@/domain/generic/notification/application/use-cases/send-notification' +import { makeAdm } from 'test/factories/entities/makeAdm' +import { makeAddress } from 'test/factories/entities/value-objects/makeAddress' +import { makeSendNotificationUseCase } from 'test/factories/use-cases/notification/make-send-notification-use-case' +import { makeCreateOrderUseCase } from 'test/factories/use-cases/order/make-create-order-use-case' +import { makeMarkOrderAsAwaitingForPickupUseCase } from 'test/factories/use-cases/order/make-mark-order-as-awaiting-for-pickup-use-case' +import { waitFor } from 'test/lib/await-for' +import { MockInstance } from 'vitest' + +let createOrder = makeCreateOrderUseCase() +let awaitingPickup = makeMarkOrderAsAwaitingForPickupUseCase({ + admsRepositoryAlt: createOrder.dependencies.admsRepository, + ordersRepositoryAlt: createOrder.dependencies.ordersRepository, +}) +let createNotification = makeSendNotificationUseCase() +let sut: OnOrderCourierCanceled // eslint-disable-line + +let sendNotificationSpy: MockInstance< + [SendNotificationUseCaseRequest], + Promise +> + +describe('on courier cancel order', () => { + beforeEach(() => { + createOrder = makeCreateOrderUseCase() + awaitingPickup = makeMarkOrderAsAwaitingForPickupUseCase({ + admsRepositoryAlt: createOrder.dependencies.admsRepository, + ordersRepositoryAlt: createOrder.dependencies.ordersRepository, + }) + createNotification = makeSendNotificationUseCase() + + sut = new OnOrderCourierCanceled( + createOrder.dependencies.ordersRepository, + createNotification.useCase, + ) + sendNotificationSpy = vi.spyOn(createNotification.useCase, 'execute') + }) + + afterAll(() => {}) + + it('shoud be able to send a notification on courier cancel order', async () => { + await createOrder.dependencies.admsRepository.create(makeAdm()) + + const adm = (await createOrder.dependencies.admsRepository.findMany())[0] + + await createOrder.useCase.execute({ + requestResponsibleId: adm.id.value, + creationProps: { + address: makeAddress(), + courierId: new UniqueEntityId('123'), + recipientId: new UniqueEntityId('1188'), + }, + }) + + const order = ( + await createOrder.dependencies.ordersRepository.findMany() + )[0] + + order.actions.courier.courierReject(adm.id) + + await createOrder.dependencies.ordersRepository.update(order) + + await awaitingPickup.useCase.execute({ + orderId: order.id.value, + requestResponsibleId: adm.id.value, + }) + + await waitFor(() => { + expect(sendNotificationSpy).toHaveBeenCalled() + }) + }) +}) diff --git a/src/domain/core/deliveries-and-orders/enterprise/events/order-courier-collected-event.spec.ts b/src/domain/core/deliveries-and-orders/enterprise/events/order-courier-collected-event.spec.ts new file mode 100644 index 0000000..d1a22f7 --- /dev/null +++ b/src/domain/core/deliveries-and-orders/enterprise/events/order-courier-collected-event.spec.ts @@ -0,0 +1,78 @@ +import UniqueEntityId from '@/core/entities/unique-entity-id' +import { OnOrderCourierCollected } from '@/domain/generic/notification/application/subscribers/on-order-courier-collected' +import { + SendNotificationUseCaseRequest, + SendNotificationUseCaseResponse, +} from '@/domain/generic/notification/application/use-cases/send-notification' +import { makeAdm } from 'test/factories/entities/makeAdm' +import { makeAddress } from 'test/factories/entities/value-objects/makeAddress' +import { makeSendNotificationUseCase } from 'test/factories/use-cases/notification/make-send-notification-use-case' +import { makeCreateOrderUseCase } from 'test/factories/use-cases/order/make-create-order-use-case' +import { makeMarkOrderAsAwaitingForPickupUseCase } from 'test/factories/use-cases/order/make-mark-order-as-awaiting-for-pickup-use-case' +import { waitFor } from 'test/lib/await-for' +import { MockInstance } from 'vitest' + +let createOrder = makeCreateOrderUseCase() +let awaitingPickup = makeMarkOrderAsAwaitingForPickupUseCase({ + admsRepositoryAlt: createOrder.dependencies.admsRepository, + ordersRepositoryAlt: createOrder.dependencies.ordersRepository, +}) +let createNotification = makeSendNotificationUseCase() +let sut: OnOrderCourierCollected // eslint-disable-line + +let sendNotificationSpy: MockInstance< + [SendNotificationUseCaseRequest], + Promise +> + +describe('on courier collect order', () => { + beforeEach(() => { + createOrder = makeCreateOrderUseCase() + awaitingPickup = makeMarkOrderAsAwaitingForPickupUseCase({ + admsRepositoryAlt: createOrder.dependencies.admsRepository, + ordersRepositoryAlt: createOrder.dependencies.ordersRepository, + }) + createNotification = makeSendNotificationUseCase() + + sut = new OnOrderCourierCollected( + createOrder.dependencies.ordersRepository, + createNotification.useCase, + ) + sendNotificationSpy = vi.spyOn(createNotification.useCase, 'execute') + }) + + afterAll(() => {}) + + it('shoud be able to send a notification on courier collect order', async () => { + await createOrder.dependencies.admsRepository.create(makeAdm()) + + const adm = (await createOrder.dependencies.admsRepository.findMany())[0] + + await createOrder.useCase.execute({ + requestResponsibleId: adm.id.value, + creationProps: { + address: makeAddress(), + courierId: new UniqueEntityId('123'), + recipientId: new UniqueEntityId('1188'), + }, + }) + + const order = ( + await createOrder.dependencies.ordersRepository.findMany() + )[0] + + order.actions.adm.admSetAwaitingPickup(adm.id) + order.actions.adm.admCollected(adm.id) + + await createOrder.dependencies.ordersRepository.update(order) + + await awaitingPickup.useCase.execute({ + orderId: order.id.value, + requestResponsibleId: adm.id.value, + }) + + await waitFor(() => { + expect(sendNotificationSpy).toHaveBeenCalled() + }) + }) +}) diff --git a/src/domain/core/deliveries-and-orders/enterprise/events/order-delivered-event.ts b/src/domain/core/deliveries-and-orders/enterprise/events/order-courier-collected-event.ts similarity index 86% rename from src/domain/core/deliveries-and-orders/enterprise/events/order-delivered-event.ts rename to src/domain/core/deliveries-and-orders/enterprise/events/order-courier-collected-event.ts index 2d24986..37589c1 100644 --- a/src/domain/core/deliveries-and-orders/enterprise/events/order-delivered-event.ts +++ b/src/domain/core/deliveries-and-orders/enterprise/events/order-courier-collected-event.ts @@ -2,7 +2,7 @@ import UniqueEntityId from '@/core/entities/unique-entity-id' import { DomainEvent } from '@/core/events/domain-event' import { Order } from '../entities/order' -export class OrderDeliveredEvent implements DomainEvent { +export class OrderCourierCollectedEvent implements DomainEvent { ocurredAt: Date private _order: Order diff --git a/src/domain/core/deliveries-and-orders/enterprise/events/order-courier-deliver-event.spec.ts b/src/domain/core/deliveries-and-orders/enterprise/events/order-courier-deliver-event.spec.ts new file mode 100644 index 0000000..b1e08f6 --- /dev/null +++ b/src/domain/core/deliveries-and-orders/enterprise/events/order-courier-deliver-event.spec.ts @@ -0,0 +1,79 @@ +import UniqueEntityId from '@/core/entities/unique-entity-id' +import { OnOrderCourierDeliver } from '@/domain/generic/notification/application/subscribers/on-order-courier-deliver' +import { + SendNotificationUseCaseRequest, + SendNotificationUseCaseResponse, +} from '@/domain/generic/notification/application/use-cases/send-notification' +import { makeAdm } from 'test/factories/entities/makeAdm' +import { makeAddress } from 'test/factories/entities/value-objects/makeAddress' +import { makeSendNotificationUseCase } from 'test/factories/use-cases/notification/make-send-notification-use-case' +import { makeCreateOrderUseCase } from 'test/factories/use-cases/order/make-create-order-use-case' +import { makeMarkOrderAsAwaitingForPickupUseCase } from 'test/factories/use-cases/order/make-mark-order-as-awaiting-for-pickup-use-case' +import { waitFor } from 'test/lib/await-for' +import { MockInstance } from 'vitest' + +let createOrder = makeCreateOrderUseCase() +let awaitingPickup = makeMarkOrderAsAwaitingForPickupUseCase({ + admsRepositoryAlt: createOrder.dependencies.admsRepository, + ordersRepositoryAlt: createOrder.dependencies.ordersRepository, +}) +let createNotification = makeSendNotificationUseCase() +let sut: OnOrderCourierDeliver // eslint-disable-line + +let sendNotificationSpy: MockInstance< + [SendNotificationUseCaseRequest], + Promise +> + +describe('on courier collect order', () => { + beforeEach(() => { + createOrder = makeCreateOrderUseCase() + awaitingPickup = makeMarkOrderAsAwaitingForPickupUseCase({ + admsRepositoryAlt: createOrder.dependencies.admsRepository, + ordersRepositoryAlt: createOrder.dependencies.ordersRepository, + }) + createNotification = makeSendNotificationUseCase() + + sut = new OnOrderCourierDeliver( + createOrder.dependencies.ordersRepository, + createNotification.useCase, + ) + sendNotificationSpy = vi.spyOn(createNotification.useCase, 'execute') + }) + + afterAll(() => {}) + + it('shoud be able to send a notification on courier collect order', async () => { + await createOrder.dependencies.admsRepository.create(makeAdm()) + + const adm = (await createOrder.dependencies.admsRepository.findMany())[0] + + await createOrder.useCase.execute({ + requestResponsibleId: adm.id.value, + creationProps: { + address: makeAddress(), + courierId: new UniqueEntityId('123'), + recipientId: new UniqueEntityId('1188'), + }, + }) + + const order = ( + await createOrder.dependencies.ordersRepository.findMany() + )[0] + + order.actions.adm.admSetAwaitingPickup(adm.id) + order.actions.adm.admCollected(adm.id) + order.actions.courier.courierDeliver(new UniqueEntityId('123')) + + await createOrder.dependencies.ordersRepository.update(order) + + await awaitingPickup.useCase.execute({ + orderId: order.id.value, + requestResponsibleId: adm.id.value, + }) + + await waitFor(() => { + expect(sendNotificationSpy).toHaveBeenCalled() + }) + }) +}) diff --git a/src/domain/core/deliveries-and-orders/enterprise/events/order-courier-returned-event.spec.ts b/src/domain/core/deliveries-and-orders/enterprise/events/order-courier-returned-event.spec.ts new file mode 100644 index 0000000..422d322 --- /dev/null +++ b/src/domain/core/deliveries-and-orders/enterprise/events/order-courier-returned-event.spec.ts @@ -0,0 +1,80 @@ +import UniqueEntityId from '@/core/entities/unique-entity-id' +import { OnOrderCourierReturned } from '@/domain/generic/notification/application/subscribers/on-order-courier-returned' +import { + SendNotificationUseCaseRequest, + SendNotificationUseCaseResponse, +} from '@/domain/generic/notification/application/use-cases/send-notification' +import { makeAdm } from 'test/factories/entities/makeAdm' +import { makeAddress } from 'test/factories/entities/value-objects/makeAddress' +import { makeSendNotificationUseCase } from 'test/factories/use-cases/notification/make-send-notification-use-case' +import { makeCreateOrderUseCase } from 'test/factories/use-cases/order/make-create-order-use-case' +import { makeMarkOrderAsAwaitingForPickupUseCase } from 'test/factories/use-cases/order/make-mark-order-as-awaiting-for-pickup-use-case' +import { waitFor } from 'test/lib/await-for' +import { MockInstance } from 'vitest' + +let createOrder = makeCreateOrderUseCase() +let awaitingPickup = makeMarkOrderAsAwaitingForPickupUseCase({ + admsRepositoryAlt: createOrder.dependencies.admsRepository, + ordersRepositoryAlt: createOrder.dependencies.ordersRepository, +}) +let createNotification = makeSendNotificationUseCase() +let sut: OnOrderCourierReturned // eslint-disable-line + +let sendNotificationSpy: MockInstance< + [SendNotificationUseCaseRequest], + Promise +> + +describe('on courier collect order', () => { + beforeEach(() => { + createOrder = makeCreateOrderUseCase() + awaitingPickup = makeMarkOrderAsAwaitingForPickupUseCase({ + admsRepositoryAlt: createOrder.dependencies.admsRepository, + ordersRepositoryAlt: createOrder.dependencies.ordersRepository, + }) + createNotification = makeSendNotificationUseCase() + + sut = new OnOrderCourierReturned( + createOrder.dependencies.ordersRepository, + createNotification.useCase, + ) + sendNotificationSpy = vi.spyOn(createNotification.useCase, 'execute') + }) + + afterAll(() => {}) + + it('shoud be able to send a notification on courier collect order', async () => { + await createOrder.dependencies.admsRepository.create(makeAdm()) + + const adm = (await createOrder.dependencies.admsRepository.findMany())[0] + + await createOrder.useCase.execute({ + requestResponsibleId: adm.id.value, + creationProps: { + address: makeAddress(), + courierId: new UniqueEntityId('123'), + recipientId: new UniqueEntityId('1188'), + }, + }) + + const order = ( + await createOrder.dependencies.ordersRepository.findMany() + )[0] + + order.actions.adm.admSetAwaitingPickup(adm.id) + order.actions.adm.admCollected(adm.id) + order.actions.courier.courierDeliver(new UniqueEntityId('123')) + order.actions.adm.admReturned('nao sei', adm.id) + + await createOrder.dependencies.ordersRepository.update(order) + + await awaitingPickup.useCase.execute({ + orderId: order.id.value, + requestResponsibleId: adm.id.value, + }) + + await waitFor(() => { + expect(sendNotificationSpy).toHaveBeenCalled() + }) + }) +}) diff --git a/src/domain/generic/notification/application/subscribers/on-order-courier-collected.ts b/src/domain/generic/notification/application/subscribers/on-order-courier-collected.ts new file mode 100644 index 0000000..cb8e9ec --- /dev/null +++ b/src/domain/generic/notification/application/subscribers/on-order-courier-collected.ts @@ -0,0 +1,39 @@ +import { DomainEvents } from '@/core/events/domain-events' +import { EventHandler } from '@/core/events/event-handler' +import SendNotificationUseCase from '../use-cases/send-notification' +import { left } from '@/core/either' +import { ResourceNotFoundError } from '@/core/errors/errors/resource-not-found-error' +import { Injectable } from '@nestjs/common' +import { OrderCourierCollectedEvent } from '@/domain/core/deliveries-and-orders/enterprise/events/order-courier-collected-event' +import { OrdersRepository } from '@/domain/core/deliveries-and-orders/application/repositories/order-repositories/orders-repository' + +@Injectable() +export class OnOrderCourierCollected implements EventHandler { + constructor( + private ordersRepository: OrdersRepository, + private sendNotificationUseCase: SendNotificationUseCase, + ) { + this.setupSubscriptions() + } + + setupSubscriptions(): void { + DomainEvents.register( + this.sendNewAnswerNotification.bind(this), + OrderCourierCollectedEvent.name, + ) + } + + private async sendNewAnswerNotification({ + order, + }: OrderCourierCollectedEvent) { + const currentOrder = await this.ordersRepository.findById(order.id.value) + + if (!currentOrder) return left(new ResourceNotFoundError()) + + await this.sendNotificationUseCase.execute({ + recipientId: currentOrder.recipientId.value, + title: `Um entregador aceitou realizar seu pedido ${currentOrder.id.value}`, + content: `Em breve o entregador irá retirar seu pedido`, + }) + } +} diff --git a/src/domain/generic/notification/application/subscribers/on-order-courier-deliver.ts b/src/domain/generic/notification/application/subscribers/on-order-courier-deliver.ts index a386942..bd55ba8 100644 --- a/src/domain/generic/notification/application/subscribers/on-order-courier-deliver.ts +++ b/src/domain/generic/notification/application/subscribers/on-order-courier-deliver.ts @@ -24,8 +24,8 @@ export class OnOrderCourierDeliver implements EventHandler { } private async sendNewAnswerNotification({ order }: OrderCourierDeliverEvent) { + console.log('sendNewAnswerNotification - OrderCourierDeliverEvent') const currentOrder = await this.ordersRepository.findById(order.id.value) - if (!currentOrder) return left(new ResourceNotFoundError()) await this.sendNotificationUseCase.execute({