├── .husky ├── .gitignore └── pre-commit ├── .prettierrc ├── apps ├── mba-ddd-venda-ingresso │ ├── src │ │ ├── @core │ │ │ ├── events │ │ │ │ ├── application │ │ │ │ │ ├── admin │ │ │ │ │ │ └── AdminEventService.ts │ │ │ │ │ ├── partner │ │ │ │ │ │ └── PartnerEventService.ts │ │ │ │ │ ├── payment.gateway.ts │ │ │ │ │ ├── handlers │ │ │ │ │ │ └── my-handler.handler.ts │ │ │ │ │ ├── customer.service.ts │ │ │ │ │ ├── partner.service.ts │ │ │ │ │ ├── customer.service.spec.ts │ │ │ │ │ ├── order.service.spec.ts │ │ │ │ │ ├── order.service.ts │ │ │ │ │ └── event.service.ts │ │ │ │ ├── domain │ │ │ │ │ ├── repositories │ │ │ │ │ │ ├── event-repository.interface.ts │ │ │ │ │ │ ├── order-repository.interface.ts │ │ │ │ │ │ ├── partner-repository.interface.ts │ │ │ │ │ │ ├── customer-repository.interface.ts │ │ │ │ │ │ └── spot-reservation-repository.interface.ts │ │ │ │ │ ├── events │ │ │ │ │ │ ├── domain-events │ │ │ │ │ │ │ ├── event-changed-date.event.ts │ │ │ │ │ │ │ ├── event-changed-name.event.ts │ │ │ │ │ │ │ ├── partner-created.event.ts │ │ │ │ │ │ │ ├── order-paid.event.ts │ │ │ │ │ │ │ ├── partner-changed-name.event.ts │ │ │ │ │ │ │ ├── customer-changed-name.event.ts │ │ │ │ │ │ │ ├── order-cancelled.event.ts │ │ │ │ │ │ │ ├── event-publish.event.ts │ │ │ │ │ │ │ ├── event-unpublish.event.ts │ │ │ │ │ │ │ ├── event-changed-description.event.ts │ │ │ │ │ │ │ ├── customer-created.event.ts │ │ │ │ │ │ │ ├── event-publish-all.event.ts │ │ │ │ │ │ │ ├── spot-reservation-changed.event.ts │ │ │ │ │ │ │ ├── spot-reservation-created.event.ts │ │ │ │ │ │ │ ├── event-added-section.event.ts │ │ │ │ │ │ │ ├── event-changed-section-information.event.ts │ │ │ │ │ │ │ ├── event-changed-spot-location.event.ts │ │ │ │ │ │ │ ├── event-marked-sport-as-reserved.event.ts │ │ │ │ │ │ │ ├── order-created.event.ts │ │ │ │ │ │ │ └── event-created.event.ts │ │ │ │ │ │ └── integration-events │ │ │ │ │ │ │ └── partner-created.int-events.ts │ │ │ │ │ └── entities │ │ │ │ │ │ ├── __tests__ │ │ │ │ │ │ ├── partner.entity.spec.ts │ │ │ │ │ │ ├── helpers.ts │ │ │ │ │ │ ├── customer.entity.spec.ts │ │ │ │ │ │ └── event.entity.spec.ts │ │ │ │ │ │ ├── event-spot.ts │ │ │ │ │ │ ├── customer.entity.ts │ │ │ │ │ │ ├── partner.entity.ts │ │ │ │ │ │ ├── spot-reservation.entity.ts │ │ │ │ │ │ ├── order.entity.ts │ │ │ │ │ │ ├── event-section.ts │ │ │ │ │ │ └── event.entity.ts │ │ │ │ └── infra │ │ │ │ │ └── db │ │ │ │ │ ├── types │ │ │ │ │ ├── cpf.schema-type.ts │ │ │ │ │ ├── event-id.schema-type.ts │ │ │ │ │ ├── order-id.schema-type.ts │ │ │ │ │ ├── partner-id.schema-type.ts │ │ │ │ │ ├── customer-id.schema-type.ts │ │ │ │ │ ├── event-spot-id.schema-type.ts │ │ │ │ │ └── event-section-id.schema-type.ts │ │ │ │ │ ├── repositories │ │ │ │ │ ├── event-mysql.repository.ts │ │ │ │ │ ├── order-mysql.repository.ts │ │ │ │ │ ├── partner-mysql.repository.ts │ │ │ │ │ ├── customer-mysql.repository.ts │ │ │ │ │ ├── spot-reservation-mysql.repository.ts │ │ │ │ │ └── __tests__ │ │ │ │ │ │ ├── partner-mysql.repository.spec.ts │ │ │ │ │ │ ├── event-mysql.repository.spec.ts │ │ │ │ │ │ └── customer-mysql.repository.spec.ts │ │ │ │ │ ├── schemas.spec.ts │ │ │ │ │ └── schemas.ts │ │ │ ├── common │ │ │ │ ├── domain │ │ │ │ │ ├── integration-event.ts │ │ │ │ │ ├── value-objects │ │ │ │ │ │ ├── name.vo.spec.ts │ │ │ │ │ │ ├── name.vo.ts │ │ │ │ │ │ ├── uuid.vo.ts │ │ │ │ │ │ ├── value-object.ts │ │ │ │ │ │ └── cpf.vo.ts │ │ │ │ │ ├── domain-event.ts │ │ │ │ │ ├── repository-interface.ts │ │ │ │ │ ├── aggregate-root.ts │ │ │ │ │ ├── entity.ts │ │ │ │ │ ├── domain-event-manager.ts │ │ │ │ │ └── my-collection.ts │ │ │ │ ├── application │ │ │ │ │ ├── domain-event-handler.interface.ts │ │ │ │ │ ├── unit-of-work.interface.ts │ │ │ │ │ └── application.service.ts │ │ │ │ └── infra │ │ │ │ │ └── unit-of-work-mikro-orm.ts │ │ │ └── stored-events │ │ │ │ ├── domain │ │ │ │ ├── repositories │ │ │ │ │ └── stored-event.repository.ts │ │ │ │ └── entities │ │ │ │ │ └── stored-event.entity.ts │ │ │ │ └── infra │ │ │ │ └── db │ │ │ │ ├── schemas.ts │ │ │ │ ├── types │ │ │ │ └── stored-event-id.schema-type.ts │ │ │ │ └── repositories │ │ │ │ └── stored-event-mysql.repository.ts │ │ ├── app.service.ts │ │ ├── events │ │ │ ├── events │ │ │ │ ├── event.dto.ts │ │ │ │ ├── events.controller.spec.ts │ │ │ │ ├── event-spots.controller.ts │ │ │ │ ├── event-sections.controller.ts │ │ │ │ └── events.controller.ts │ │ │ ├── partners │ │ │ │ ├── partners.controller.ts │ │ │ │ └── partners.controller.spec.ts │ │ │ ├── customers │ │ │ │ └── customers.controller.ts │ │ │ ├── orders │ │ │ │ └── orders.controller.ts │ │ │ └── events.module.ts │ │ ├── app.controller.ts │ │ ├── main.ts │ │ ├── rabbitmq │ │ │ └── rabbitmq.module.ts │ │ ├── mikro-orm.config.ts │ │ ├── app.controller.spec.ts │ │ ├── domain-events │ │ │ ├── integration-events.publisher.ts │ │ │ └── domain-events.module.ts │ │ ├── application │ │ │ └── application.module.ts │ │ ├── app.module.ts │ │ └── database │ │ │ └── database.module.ts │ ├── test │ │ ├── jest-e2e.json │ │ └── app.e2e-spec.ts │ └── tsconfig.app.json └── emails │ ├── src │ ├── emails.service.ts │ ├── main.ts │ ├── emails.controller.ts │ ├── emails.module.ts │ ├── consumer.service.ts │ └── emails.controller.spec.ts │ ├── test │ ├── jest-e2e.json │ └── app.e2e-spec.ts │ └── tsconfig.app.json ├── .vscode └── settings.json ├── Event Storming.png ├── tsconfig.build.json ├── camadas de responsabilidade.jpg ├── .swcrc ├── .gitignore ├── docker-compose.yaml ├── tsconfig.json ├── .eslintrc.js ├── README.md ├── nest-cli.json ├── ORMs e design patterns.md ├── api.http ├── package.json └── scorecard.md /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/application/admin/AdminEventService.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/application/partner/PartnerEventService.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint 5 | npm run format 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "mikro", 4 | "Mikro" 5 | ] 6 | } -------------------------------------------------------------------------------- /Event Storming.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfullcycle/mba-domain-driven-design/HEAD/Event Storming.png -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /camadas de responsabilidade.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfullcycle/mba-domain-driven-design/HEAD/camadas de responsabilidade.jpg -------------------------------------------------------------------------------- /apps/emails/src/emails.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class EmailsService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/common/domain/integration-event.ts: -------------------------------------------------------------------------------- 1 | export interface IIntegrationEvent { 2 | event_name: string; 3 | payload: T; 4 | event_version: number; 5 | occurred_on: Date; 6 | } 7 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/common/application/domain-event-handler.interface.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '../domain/domain-event'; 2 | 3 | export interface IDomainEventHandler { 4 | handle(event: IDomainEvent): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/common/domain/value-objects/name.vo.spec.ts: -------------------------------------------------------------------------------- 1 | import { Name } from './name.vo'; 2 | 3 | test('deve criar um nome válido', () => { 4 | const name = new Name('aaaaaa'); 5 | expect(name.value).toBe('aaaaaa'); 6 | }); 7 | -------------------------------------------------------------------------------- /apps/emails/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/common/domain/domain-event.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from './value-objects/value-object'; 2 | 3 | export interface IDomainEvent { 4 | aggregate_id: ValueObject; 5 | occurred_on: Date; 6 | event_version: number; 7 | } 8 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/events/events/event.dto.ts: -------------------------------------------------------------------------------- 1 | import { Type } from 'class-transformer'; 2 | 3 | export class EventDto { 4 | name: string; 5 | description: string; 6 | @Type(() => Date) 7 | date: Date; 8 | partner_id: string; 9 | } 10 | -------------------------------------------------------------------------------- /apps/emails/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { EmailsModule } from './emails.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(EmailsModule); 6 | await app.listen(3001); 7 | } 8 | bootstrap(); 9 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "@swc/jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apps/emails/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "outDir": "../../dist/apps/emails" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "outDir": "../../dist/apps/mba-ddd-venda-ingresso" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/common/domain/repository-interface.ts: -------------------------------------------------------------------------------- 1 | import { AggregateRoot } from './aggregate-root'; 2 | 3 | export interface IRepository { 4 | add(entity: E): Promise; 5 | findById(id: any): Promise; 6 | findAll(): Promise; 7 | delete(entity: E): Promise; 8 | } 9 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/common/domain/value-objects/name.vo.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from './value-object'; 2 | 3 | export class Name extends ValueObject { 4 | constructor(name: string) { 5 | super(name); 6 | this.isValid(); 7 | } 8 | 9 | isValid() { 10 | return this.value.length >= 3; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/repositories/event-repository.interface.ts: -------------------------------------------------------------------------------- 1 | import { IRepository } from '../../../common/domain/repository-interface'; 2 | import { Event } from '../entities/event.entity'; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 5 | export interface IEventRepository extends IRepository {} 6 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/repositories/order-repository.interface.ts: -------------------------------------------------------------------------------- 1 | import { IRepository } from '../../../common/domain/repository-interface'; 2 | import { Order } from '../entities/order.entity'; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 5 | export interface IOrderRepository extends IRepository {} 6 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/application/payment.gateway.ts: -------------------------------------------------------------------------------- 1 | export class PaymentGateway { 2 | // eslint-disable-next-line @typescript-eslint/no-empty-function 3 | constructor() {} 4 | //private IPayPal, private IStripe 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-empty-function 7 | async payment({ token, amount }): Promise {} 8 | } 9 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/repositories/partner-repository.interface.ts: -------------------------------------------------------------------------------- 1 | import { IRepository } from '../../../common/domain/repository-interface'; 2 | import { Partner } from '../entities/partner.entity'; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 5 | export interface IPartnerRepository extends IRepository {} 6 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/repositories/customer-repository.interface.ts: -------------------------------------------------------------------------------- 1 | import { IRepository } from '../../../common/domain/repository-interface'; 2 | import { Customer } from '../entities/customer.entity'; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 5 | export interface ICustomerRepository extends IRepository {} 6 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/emails/src/emails.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { EmailsService } from './emails.service'; 3 | 4 | @Controller() 5 | export class EmailsController { 6 | constructor(private readonly emailsService: EmailsService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return this.emailsService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/repositories/spot-reservation-repository.interface.ts: -------------------------------------------------------------------------------- 1 | import { IRepository } from '../../../common/domain/repository-interface'; 2 | import { SpotReservation } from '../entities/spot-reservation.entity'; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 5 | export interface ISpotReservationRepository 6 | extends IRepository {} 7 | -------------------------------------------------------------------------------- /.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/swcrc", 3 | "sourceMaps": true, 4 | "jsc": { 5 | "parser": { 6 | "syntax": "typescript", 7 | "decorators": true, 8 | "dynamicImport": true 9 | }, 10 | "transform": { 11 | "legacyDecorator": true, 12 | "decoratorMetadata": true 13 | }, 14 | "baseUrl": "./" 15 | }, 16 | "minify": false 17 | } -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/common/domain/aggregate-root.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from './domain-event'; 2 | import { Entity } from './entity'; 3 | 4 | export abstract class AggregateRoot extends Entity { 5 | events: Set = new Set(); 6 | 7 | addEvent(event: IDomainEvent) { 8 | this.events.add(event); 9 | } 10 | 11 | clearEvents() { 12 | this.events.clear(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { ValidationPipe } from '@nestjs/common'; 4 | 5 | async function bootstrap() { 6 | const app = await NestFactory.create(AppModule); 7 | 8 | app.useGlobalPipes( 9 | new ValidationPipe({ 10 | transform: true, 11 | }), 12 | ); 13 | 14 | await app.listen(3000); 15 | } 16 | bootstrap(); 17 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/common/application/unit-of-work.interface.ts: -------------------------------------------------------------------------------- 1 | import { AggregateRoot } from '../domain/aggregate-root'; 2 | 3 | export interface IUnitOfWork { 4 | beginTransaction(): Promise; 5 | completeTransaction(): Promise; 6 | rollbackTransaction(): Promise; 7 | runTransaction(callback: () => Promise): Promise; 8 | commit(): Promise; 9 | rollback(): Promise; 10 | getAggregateRoots(): AggregateRoot[]; 11 | } 12 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/events/domain-events/event-changed-date.event.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '../../../../common/domain/domain-event'; 2 | import { EventId } from '../../entities/event.entity'; 3 | 4 | export class EventChangedDate implements IDomainEvent { 5 | readonly event_version: number = 1; 6 | readonly occurred_on: Date; 7 | 8 | constructor(readonly aggregate_id: EventId, readonly date: Date) { 9 | this.occurred_on = new Date(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/rabbitmq/rabbitmq.module.ts: -------------------------------------------------------------------------------- 1 | import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq'; 2 | import { Global, Module } from '@nestjs/common'; 3 | 4 | @Global() 5 | @Module({ 6 | imports: [ 7 | RabbitMQModule.forRoot(RabbitMQModule, { 8 | uri: 'amqp://admin:admin@localhost:5672', 9 | connectionInitOptions: { wait: false }, 10 | }), 11 | RabbitmqModule, 12 | ], 13 | exports: [RabbitMQModule], 14 | }) 15 | export class RabbitmqModule {} 16 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/events/domain-events/event-changed-name.event.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '../../../../common/domain/domain-event'; 2 | import { EventId } from '../../entities/event.entity'; 3 | 4 | export class EventChangedName implements IDomainEvent { 5 | readonly event_version: number = 1; 6 | readonly occurred_on: Date; 7 | 8 | constructor(readonly aggregate_id: EventId, readonly name: string) { 9 | this.occurred_on = new Date(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/events/domain-events/partner-created.event.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '../../../../common/domain/domain-event'; 2 | import { PartnerId } from '../../entities/partner.entity'; 3 | 4 | export class PartnerCreated implements IDomainEvent { 5 | readonly event_version: number = 1; 6 | readonly occurred_on: Date; 7 | 8 | constructor(readonly aggregate_id: PartnerId, readonly name: string) { 9 | this.occurred_on = new Date(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/common/domain/entity.ts: -------------------------------------------------------------------------------- 1 | export abstract class Entity { 2 | readonly id: any; 3 | 4 | abstract toJSON(): any; 5 | 6 | equals(obj: this) { 7 | if (obj === null || obj === undefined) { 8 | return false; 9 | } 10 | 11 | if (obj.id === undefined) { 12 | return false; 13 | } 14 | 15 | if (obj.constructor.name !== this.constructor.name) { 16 | return false; 17 | } 18 | 19 | return obj.id.equals(this.id); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/events/domain-events/order-paid.event.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '../../../../common/domain/domain-event'; 2 | import { OrderId, OrderStatus } from '../../entities/order.entity'; 3 | 4 | export class OrderPaid implements IDomainEvent { 5 | readonly event_version: number = 1; 6 | readonly occurred_on: Date; 7 | 8 | constructor(readonly aggregate_id: OrderId, readonly status: OrderStatus) { 9 | this.occurred_on = new Date(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/events/domain-events/partner-changed-name.event.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '../../../../common/domain/domain-event'; 2 | import { PartnerId } from '../../entities/partner.entity'; 3 | 4 | export class PartnerChangedName implements IDomainEvent { 5 | readonly event_version: number = 1; 6 | readonly occurred_on: Date; 7 | 8 | constructor(readonly aggregate_id: PartnerId, readonly name: string) { 9 | this.occurred_on = new Date(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/events/domain-events/customer-changed-name.event.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '../../../../common/domain/domain-event'; 2 | import { CustomerId } from '../../entities/customer.entity'; 3 | 4 | export class CustomerChangedName implements IDomainEvent { 5 | readonly event_version: number = 1; 6 | readonly occurred_on: Date; 7 | 8 | constructor(readonly aggregate_id: CustomerId, readonly name: string) { 9 | this.occurred_on = new Date(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/events/domain-events/order-cancelled.event.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '../../../../common/domain/domain-event'; 2 | import { OrderId, OrderStatus } from '../../entities/order.entity'; 3 | 4 | export class OrderCancelled implements IDomainEvent { 5 | readonly event_version: number = 1; 6 | readonly occurred_on: Date; 7 | 8 | constructor(readonly aggregate_id: OrderId, readonly status: OrderStatus) { 9 | this.occurred_on = new Date(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/events/domain-events/event-publish.event.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '../../../../common/domain/domain-event'; 2 | import { EventId } from '../../entities/event.entity'; 3 | 4 | export class EventPublish implements IDomainEvent { 5 | readonly event_version: number = 1; 6 | readonly occurred_on: Date; 7 | 8 | readonly is_published: boolean = true; 9 | 10 | constructor(readonly aggregate_id: EventId) { 11 | this.occurred_on = new Date(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/events/domain-events/event-unpublish.event.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '../../../../common/domain/domain-event'; 2 | import { EventId } from '../../entities/event.entity'; 3 | 4 | export class EventUnpublish implements IDomainEvent { 5 | readonly event_version: number = 1; 6 | readonly occurred_on: Date; 7 | 8 | readonly is_published: boolean = false; 9 | 10 | constructor(readonly aggregate_id: EventId) { 11 | this.occurred_on = new Date(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/stored-events/domain/repositories/stored-event.repository.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '../../../common/domain/domain-event'; 2 | import { StoredEvent, StoredEventId } from '../entities/stored-event.entity'; 3 | 4 | export interface IStoredEventRepository { 5 | allBetween( 6 | lowEventId: StoredEventId, 7 | highEventId: StoredEventId, 8 | ): Promise; 9 | 10 | allSince(eventId: StoredEventId): Promise; 11 | 12 | add(domainEvent: IDomainEvent): StoredEvent; 13 | } 14 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/events/domain-events/event-changed-description.event.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '../../../../common/domain/domain-event'; 2 | import { EventId } from '../../entities/event.entity'; 3 | 4 | export class EventChangedDescription implements IDomainEvent { 5 | readonly event_version: number = 1; 6 | readonly occurred_on: Date; 7 | 8 | constructor( 9 | readonly aggregate_id: EventId, 10 | readonly description: string | null, 11 | ) { 12 | this.occurred_on = new Date(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/events/partners/partners.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Post } from '@nestjs/common'; 2 | import { PartnerService } from '../../@core/events/application/partner.service'; 3 | 4 | @Controller('partners') 5 | export class PartnersController { 6 | constructor(private partnerService: PartnerService) {} 7 | 8 | @Get() 9 | list() { 10 | return this.partnerService.list(); 11 | } 12 | 13 | @Post() 14 | create(@Body() body: { name: string }) { 15 | return this.partnerService.create(body); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | 37 | .history/ -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | 5 | mysql: 6 | image: mysql:8.0.30-debian 7 | ports: 8 | - 3306:3306 9 | environment: 10 | - MYSQL_ROOT_PASSWORD=root 11 | - MYSQL_DATABASE=events 12 | 13 | redis: 14 | image: redis:7.0.8-alpine 15 | ports: 16 | - 6379:6379 17 | 18 | rabbitmq: 19 | image: rabbitmq:3.8-management-alpine 20 | hostname: rabbitmq 21 | ports: 22 | - 15672:15672 23 | - 5672:5672 24 | environment: 25 | - RABBITMQ_DEFAULT_USER=admin 26 | - RABBITMQ_DEFAULT_PASS=admin -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/events/customers/customers.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Post } from '@nestjs/common'; 2 | import { CustomerService } from '../../@core/events/application/customer.service'; 3 | 4 | @Controller('customers') 5 | export class CustomersController { 6 | constructor(private customerService: CustomerService) {} 7 | 8 | @Get() 9 | async list() { 10 | return this.customerService.list(); 11 | } 12 | 13 | @Post() 14 | create(@Body() body: { name: string; cpf: string }) { 15 | return this.customerService.register(body); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/events/domain-events/customer-created.event.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '../../../../common/domain/domain-event'; 2 | import Cpf from '../../../../common/domain/value-objects/cpf.vo'; 3 | import { CustomerId } from '../../entities/customer.entity'; 4 | 5 | export class CustomerCreated implements IDomainEvent { 6 | readonly event_version: number = 1; 7 | readonly occurred_on: Date; 8 | 9 | constructor( 10 | readonly aggregate_id: CustomerId, 11 | readonly name: string, 12 | readonly cpf: Cpf, 13 | ) { 14 | this.occurred_on = new Date(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/stored-events/infra/db/schemas.ts: -------------------------------------------------------------------------------- 1 | import { EntitySchema } from '@mikro-orm/core'; 2 | import { StoredEvent } from '../../domain/entities/stored-event.entity'; 3 | import { StoredEventIdSchemaType } from './types/stored-event-id.schema-type'; 4 | 5 | export const StoredEventSchema = new EntitySchema({ 6 | class: StoredEvent, 7 | properties: { 8 | id: { 9 | customType: new StoredEventIdSchemaType(), 10 | primary: true, 11 | }, 12 | body: { type: 'json' }, 13 | type_name: { type: 'string', length: 255 }, 14 | occurred_on: { type: 'date' }, 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/events/events/events.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { EventsController } from './events.controller'; 3 | 4 | describe.skip('EventsController', () => { 5 | let controller: EventsController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [EventsController], 10 | }).compile(); 11 | 12 | controller = module.get(EventsController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /apps/emails/src/emails.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { EmailsController } from './emails.controller'; 3 | import { EmailsService } from './emails.service'; 4 | import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq'; 5 | import { ConsumerService } from './consumer.service'; 6 | 7 | @Module({ 8 | imports: [ 9 | RabbitMQModule.forRoot(RabbitMQModule, { 10 | uri: 'amqp://admin:admin@localhost:5672', 11 | connectionInitOptions: { wait: false }, 12 | }), 13 | ], 14 | controllers: [EmailsController], 15 | providers: [EmailsService, ConsumerService], 16 | }) 17 | export class EmailsModule {} 18 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/events/partners/partners.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { PartnersController } from './partners.controller'; 3 | 4 | describe.skip('PartnersController', () => { 5 | let controller: PartnersController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [PartnersController], 10 | }).compile(); 11 | 12 | controller = module.get(PartnersController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/events/domain-events/event-publish-all.event.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '../../../../common/domain/domain-event'; 2 | import { EventSectionId } from '../../entities/event-section'; 3 | import { EventId } from '../../entities/event.entity'; 4 | 5 | export class EventPublishAll implements IDomainEvent { 6 | readonly event_version: number = 1; 7 | readonly occurred_on: Date; 8 | 9 | readonly is_published: boolean = true; 10 | 11 | constructor( 12 | readonly aggregate_id: EventId, 13 | readonly sections_id: EventSectionId[], 14 | ) { 15 | this.occurred_on = new Date(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/events/domain-events/spot-reservation-changed.event.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '../../../../common/domain/domain-event'; 2 | import { CustomerId } from '../../entities/customer.entity'; 3 | import { EventSpotId } from '../../entities/event-spot'; 4 | 5 | export class SpotReservationChanged implements IDomainEvent { 6 | readonly event_version: number = 1; 7 | readonly occurred_on: Date; 8 | 9 | constructor( 10 | readonly aggregate_id: EventSpotId, 11 | readonly reservation_date: Date, 12 | readonly customer_id: CustomerId, 13 | ) { 14 | this.occurred_on = new Date(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/events/domain-events/spot-reservation-created.event.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '../../../../common/domain/domain-event'; 2 | import { CustomerId } from '../../entities/customer.entity'; 3 | import { EventSpotId } from '../../entities/event-spot'; 4 | 5 | export class SpotReservationCreated implements IDomainEvent { 6 | readonly event_version: number = 1; 7 | readonly occurred_on: Date; 8 | 9 | constructor( 10 | readonly aggregate_id: EventSpotId, 11 | readonly reservation_date: Date, 12 | readonly customer_id: CustomerId, 13 | ) { 14 | this.occurred_on = new Date(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/entities/__tests__/partner.entity.spec.ts: -------------------------------------------------------------------------------- 1 | import { initOrm } from './helpers'; 2 | import { Partner } from '../partner.entity'; 3 | 4 | describe('Partner tests', () => { 5 | initOrm(); 6 | test('deve criar um evento', () => { 7 | const partner = Partner.create({ 8 | name: 'Parceiro 1', 9 | }); 10 | 11 | const event = partner.initEvent({ 12 | name: 'Evento 1', 13 | description: 'Descrição do evento 1', 14 | date: new Date(), 15 | }); 16 | 17 | partner.changeName('Parceiro 1 alterado'); 18 | 19 | console.log(event); 20 | console.log(partner); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/emails/src/consumer.service.ts: -------------------------------------------------------------------------------- 1 | import { RabbitSubscribe } from '@golevelup/nestjs-rabbitmq'; 2 | import { Injectable } from '@nestjs/common'; 3 | 4 | @Injectable() 5 | export class ConsumerService { 6 | @RabbitSubscribe({ 7 | exchange: 'amq.direct', 8 | routingKey: 'PartnerCreatedIntegrationEvent', 9 | //routingKey: 'events.fullcycle.com/*', 10 | queue: 'emails', 11 | }) 12 | handle(msg: { event_name: string; [key: string]: any }) { 13 | // switch(msg.event_name) { 14 | // case 'PartnerCreatedIntegrationEvent': 15 | 16 | // case 'PartnerUpdatedIntegrationEvent': 17 | // } 18 | console.log('ConsumerService.handle', msg); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false, 20 | "esModuleInterop": true, 21 | "paths": {} 22 | } 23 | } -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/events/domain-events/event-added-section.event.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '../../../../common/domain/domain-event'; 2 | import { EventId } from '../../entities/event.entity'; 3 | 4 | export class EventAddedSection implements IDomainEvent { 5 | readonly event_version: number = 1; 6 | readonly occurred_on: Date; 7 | 8 | constructor( 9 | readonly aggregate_id: EventId, 10 | readonly section_name: string, 11 | readonly section_description: string | null, 12 | readonly section_total_spots: number, 13 | readonly section_price: number, 14 | readonly event_total_spots: number, 15 | ) { 16 | this.occurred_on = new Date(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/events/domain-events/event-changed-section-information.event.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '../../../../common/domain/domain-event'; 2 | import { EventSectionId } from '../../entities/event-section'; 3 | import { EventId } from '../../entities/event.entity'; 4 | 5 | export class EventChangedSectionSection implements IDomainEvent { 6 | readonly event_version: number = 1; 7 | readonly occurred_on: Date; 8 | 9 | constructor( 10 | readonly aggregate_id: EventId, 11 | readonly section_id: EventSectionId, 12 | readonly section_name: string, 13 | readonly section_description: string | null, 14 | ) { 15 | this.occurred_on = new Date(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/mikro-orm.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CustomerSchema, 3 | EventSchema, 4 | EventSectionSchema, 5 | EventSpotSchema, 6 | OrderSchema, 7 | PartnerSchema, 8 | SpotReservationSchema, 9 | } from './@core/events/infra/db/schemas'; 10 | import { StoredEventSchema } from './@core/stored-events/infra/db/schemas'; 11 | 12 | export default { 13 | entities: [ 14 | PartnerSchema, 15 | CustomerSchema, 16 | EventSchema, 17 | EventSectionSchema, 18 | EventSpotSchema, 19 | OrderSchema, 20 | SpotReservationSchema, 21 | StoredEventSchema, 22 | ], 23 | dbName: 'events', 24 | host: 'localhost', 25 | user: 'root', 26 | password: 'root', 27 | type: 'mysql', 28 | }; 29 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(appController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/events/domain-events/event-changed-spot-location.event.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '../../../../common/domain/domain-event'; 2 | import { EventSectionId } from '../../entities/event-section'; 3 | import { EventSpotId } from '../../entities/event-spot'; 4 | import { EventId } from '../../entities/event.entity'; 5 | 6 | export class EventChangedSpotLocation implements IDomainEvent { 7 | readonly event_version: number = 1; 8 | readonly occurred_on: Date; 9 | 10 | constructor( 11 | readonly aggregate_id: EventId, 12 | readonly section_id: EventSectionId, 13 | readonly spot_id: EventSpotId, 14 | readonly location: string, 15 | ) { 16 | this.occurred_on = new Date(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/emails/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { EmailsModule } from './../src/emails.module'; 5 | 6 | describe('EmailsController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [EmailsModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /apps/emails/src/emails.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { EmailsController } from './emails.controller'; 3 | import { EmailsService } from './emails.service'; 4 | 5 | describe('EmailsController', () => { 6 | let emailsController: EmailsController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [EmailsController], 11 | providers: [EmailsService], 12 | }).compile(); 13 | 14 | emailsController = app.get(EmailsController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(emailsController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/events/domain-events/event-marked-sport-as-reserved.event.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '../../../../common/domain/domain-event'; 2 | import { EventSectionId } from '../../entities/event-section'; 3 | import { EventSpotId } from '../../entities/event-spot'; 4 | import { EventId } from '../../entities/event.entity'; 5 | 6 | export class EventMarkedSportAsReserved implements IDomainEvent { 7 | readonly event_version: number = 1; 8 | readonly occurred_on: Date; 9 | readonly spot_is_reserved: boolean = true; 10 | 11 | constructor( 12 | readonly aggregate_id: EventId, 13 | readonly section_id: EventSectionId, 14 | readonly spot_id: EventSpotId, 15 | ) { 16 | this.occurred_on = new Date(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/events/integration-events/partner-created.int-events.ts: -------------------------------------------------------------------------------- 1 | import { IIntegrationEvent } from '../../../../common/domain/integration-event'; 2 | import { PartnerCreated } from '../domain-events/partner-created.event'; 3 | 4 | export class PartnerCreatedIntegrationEvent implements IIntegrationEvent { 5 | event_name: string; 6 | payload: any; 7 | event_version: number; 8 | occurred_on: Date; 9 | 10 | constructor(domainEvent: PartnerCreated) { 11 | this.event_name = PartnerCreatedIntegrationEvent.name; 12 | this.payload = { 13 | id: domainEvent.aggregate_id.value, 14 | name: domainEvent.name, 15 | }; 16 | this.event_version = 1; 17 | this.occurred_on = domainEvent.occurred_on; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/infra/db/types/cpf.schema-type.ts: -------------------------------------------------------------------------------- 1 | import { Type, Platform, EntityProperty } from '@mikro-orm/core'; 2 | import Cpf from '../../../../common/domain/value-objects/cpf.vo'; 3 | 4 | export class CpfSchemaType extends Type { 5 | convertToDatabaseValue( 6 | valueObject: Cpf | undefined | null, 7 | platform: Platform, 8 | ): string { 9 | return valueObject instanceof Cpf 10 | ? valueObject.value 11 | : (valueObject as string); 12 | } 13 | 14 | //não funciona para relacionamentos 15 | convertToJSValue(value: string, platform: Platform): Cpf { 16 | return new Cpf(value); 17 | } 18 | 19 | getColumnType(prop: EntityProperty, platform: Platform) { 20 | return `VARCHAR(11)`; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/events/domain-events/order-created.event.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '../../../../common/domain/domain-event'; 2 | import { CustomerId } from '../../entities/customer.entity'; 3 | import { EventSpotId } from '../../entities/event-spot'; 4 | import { OrderId, OrderStatus } from '../../entities/order.entity'; 5 | 6 | export class OrderCreated implements IDomainEvent { 7 | readonly event_version: number = 1; 8 | readonly occurred_on: Date; 9 | 10 | constructor( 11 | readonly aggregate_id: OrderId, 12 | readonly customer_id: CustomerId, 13 | readonly amount: number, 14 | readonly event_spot_id: EventSpotId, 15 | readonly status: OrderStatus, 16 | ) { 17 | this.occurred_on = new Date(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/common/domain/value-objects/uuid.vo.ts: -------------------------------------------------------------------------------- 1 | import { validate as uuidValidate } from 'uuid'; 2 | import crypto from 'crypto'; 3 | import { ValueObject } from './value-object'; 4 | 5 | export class Uuid extends ValueObject { 6 | constructor(id?: string) { 7 | super(id || crypto.randomUUID()); 8 | this.validate(); 9 | } 10 | 11 | private validate() { 12 | const isValid = uuidValidate(this.value); 13 | if (!isValid) { 14 | throw new InvalidUuidError(this.value); 15 | } 16 | } 17 | } 18 | 19 | export class InvalidUuidError extends Error { 20 | constructor(invalidValue: any) { 21 | super(`Value ${invalidValue} must be a valid UUID`); 22 | this.name = 'InvalidUuidError'; 23 | } 24 | } 25 | 26 | export default Uuid; 27 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/domain-events/integration-events.publisher.ts: -------------------------------------------------------------------------------- 1 | import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'; 2 | import { Job } from 'bull'; 3 | import { IIntegrationEvent } from '../@core/common/domain/integration-event'; 4 | import { Process, Processor } from '@nestjs/bull'; 5 | 6 | @Processor('integration-events') 7 | export class IntegrationEventsPublisher { 8 | constructor(private amqpConnection: AmqpConnection) {} 9 | 10 | @Process() 11 | async handle(job: Job) { 12 | console.log('IntegrationEventsPublisher.handle', job.data); 13 | await this.amqpConnection.publish( 14 | 'amq.direct', 15 | //events.fullcycle.com/PartnerCreated 16 | job.data.event_name, 17 | job.data, 18 | ); 19 | return {}; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/infra/db/types/event-id.schema-type.ts: -------------------------------------------------------------------------------- 1 | import { Type, Platform, EntityProperty } from '@mikro-orm/core'; 2 | import { EventId } from '../../../domain/entities/event.entity'; 3 | 4 | export class EventIdSchemaType extends Type { 5 | convertToDatabaseValue( 6 | valueObject: EventId | undefined | null, 7 | platform: Platform, 8 | ): string { 9 | return valueObject instanceof EventId 10 | ? valueObject.value 11 | : (valueObject as string); 12 | } 13 | 14 | //não funciona para relacionamentos 15 | convertToJSValue(value: string, platform: Platform): EventId { 16 | return new EventId(value); 17 | } 18 | 19 | getColumnType(prop: EntityProperty, platform: Platform) { 20 | return `varchar(36)`; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/infra/db/types/order-id.schema-type.ts: -------------------------------------------------------------------------------- 1 | import { Type, Platform, EntityProperty } from '@mikro-orm/core'; 2 | import { OrderId } from '../../../domain/entities/order.entity'; 3 | 4 | export class OrderIdSchemaType extends Type { 5 | convertToDatabaseValue( 6 | valueObject: OrderId | undefined | null, 7 | platform: Platform, 8 | ): string { 9 | return valueObject instanceof OrderId 10 | ? valueObject.value 11 | : (valueObject as string); 12 | } 13 | 14 | //não funciona para relacionamentos 15 | convertToJSValue(value: string, platform: Platform): OrderId { 16 | return new OrderId(value); 17 | } 18 | 19 | getColumnType(prop: EntityProperty, platform: Platform) { 20 | return `varchar(36)`; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/application/application.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ApplicationService } from '../@core/common/application/application.service'; 3 | import { DomainEventManager } from '../@core/common/domain/domain-event-manager'; 4 | import { IUnitOfWork } from '../@core/common/application/unit-of-work.interface'; 5 | 6 | @Module({ 7 | providers: [ 8 | { 9 | provide: ApplicationService, 10 | useFactory: ( 11 | uow: IUnitOfWork, 12 | domainEventManager: DomainEventManager, 13 | ) => { 14 | return new ApplicationService(uow, domainEventManager); 15 | }, 16 | inject: ['IUnitOfWork', DomainEventManager], 17 | }, 18 | ], 19 | exports: [ApplicationService], 20 | }) 21 | export class ApplicationModule {} 22 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/events/orders/orders.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Param, Post } from '@nestjs/common'; 2 | import { OrderService } from '../../@core/events/application/order.service'; 3 | 4 | @Controller('events/:event_id/orders') 5 | export class OrdersController { 6 | constructor(private ordersService: OrderService) {} 7 | 8 | @Get() 9 | async list() { 10 | return this.ordersService.list(); 11 | } 12 | 13 | @Post() 14 | create( 15 | @Param('event_id') event_id: string, 16 | @Body() 17 | body: { 18 | section_id: string; 19 | spot_id: string; 20 | customer_id: string; 21 | card_token: string; 22 | }, 23 | ) { 24 | return this.ordersService.create({ 25 | ...body, 26 | event_id: event_id, 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/infra/db/types/partner-id.schema-type.ts: -------------------------------------------------------------------------------- 1 | import { Type, Platform, EntityProperty } from '@mikro-orm/core'; 2 | import { PartnerId } from '../../../domain/entities/partner.entity'; 3 | 4 | export class PartnerIdSchemaType extends Type { 5 | convertToDatabaseValue( 6 | valueObject: PartnerId | undefined | null, 7 | platform: Platform, 8 | ): string { 9 | return valueObject instanceof PartnerId 10 | ? valueObject.value 11 | : (valueObject as string); 12 | } 13 | 14 | //não funciona para relacionamentos 15 | convertToJSValue(value: string, platform: Platform): PartnerId { 16 | return new PartnerId(value); 17 | } 18 | 19 | getColumnType(prop: EntityProperty, platform: Platform) { 20 | return 'varchar(36)'; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/infra/db/types/customer-id.schema-type.ts: -------------------------------------------------------------------------------- 1 | import { Type, Platform, EntityProperty } from '@mikro-orm/core'; 2 | import { CustomerId } from '../../../domain/entities/customer.entity'; 3 | 4 | export class CustomerIdSchemaType extends Type { 5 | convertToDatabaseValue( 6 | valueObject: CustomerId | undefined | null, 7 | platform: Platform, 8 | ): string { 9 | return valueObject instanceof CustomerId 10 | ? valueObject.value 11 | : (valueObject as string); 12 | } 13 | 14 | //não funciona para relacionamentos 15 | convertToJSValue(value: string, platform: Platform): CustomerId { 16 | return new CustomerId(value); 17 | } 18 | 19 | getColumnType(prop: EntityProperty, platform: Platform) { 20 | return `varchar(36)`; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/infra/db/types/event-spot-id.schema-type.ts: -------------------------------------------------------------------------------- 1 | import { Type, Platform, EntityProperty } from '@mikro-orm/core'; 2 | import { EventSpotId } from '../../../domain/entities/event-spot'; 3 | 4 | export class EventSpotIdSchemaType extends Type { 5 | convertToDatabaseValue( 6 | valueObject: EventSpotId | undefined | null, 7 | platform: Platform, 8 | ): string { 9 | return valueObject instanceof EventSpotId 10 | ? valueObject.value 11 | : (valueObject as string); 12 | } 13 | 14 | //não funciona para relacionamentos 15 | convertToJSValue(value: string, platform: Platform): EventSpotId { 16 | return new EventSpotId(value); 17 | } 18 | 19 | getColumnType(prop: EntityProperty, platform: Platform) { 20 | return `varchar(36)`; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/entities/__tests__/helpers.ts: -------------------------------------------------------------------------------- 1 | import { MikroORM } from '@mikro-orm/core'; 2 | import { 3 | CustomerSchema, 4 | EventSchema, 5 | EventSectionSchema, 6 | EventSpotSchema, 7 | OrderSchema, 8 | PartnerSchema, 9 | SpotReservationSchema, 10 | } from '../../../infra/db/schemas'; 11 | 12 | export function initOrm() { 13 | beforeAll(async () => { 14 | await MikroORM.init( 15 | { 16 | allowGlobalContext: true, 17 | entities: [ 18 | PartnerSchema, 19 | CustomerSchema, 20 | EventSchema, 21 | EventSectionSchema, 22 | EventSpotSchema, 23 | OrderSchema, 24 | SpotReservationSchema, 25 | ], 26 | type: 'mysql', 27 | dbName: 'fake', 28 | }, 29 | false, 30 | ); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/infra/db/types/event-section-id.schema-type.ts: -------------------------------------------------------------------------------- 1 | import { Type, Platform, EntityProperty } from '@mikro-orm/core'; 2 | import { EventSectionId } from '../../../domain/entities/event-section'; 3 | 4 | export class EventSectionIdSchemaType extends Type { 5 | convertToDatabaseValue( 6 | valueObject: EventSectionId | undefined | null, 7 | platform: Platform, 8 | ): string { 9 | return valueObject instanceof EventSectionId 10 | ? valueObject.value 11 | : (valueObject as string); 12 | } 13 | 14 | //não funciona para relacionamentos 15 | convertToJSValue(value: string, platform: Platform): EventSectionId { 16 | return new EventSectionId(value); 17 | } 18 | 19 | getColumnType(prop: EntityProperty, platform: Platform) { 20 | return `varchar(36)`; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/stored-events/infra/db/types/stored-event-id.schema-type.ts: -------------------------------------------------------------------------------- 1 | import { Type, Platform, EntityProperty } from '@mikro-orm/core'; 2 | import { StoredEventId } from '../../../domain/entities/stored-event.entity'; 3 | 4 | export class StoredEventIdSchemaType extends Type { 5 | convertToDatabaseValue( 6 | valueObject: StoredEventId | undefined | null, 7 | platform: Platform, 8 | ): string { 9 | return valueObject instanceof StoredEventId 10 | ? valueObject.value 11 | : (valueObject as string); 12 | } 13 | 14 | //não funciona para relacionamentos 15 | convertToJSValue(value: string, platform: Platform): StoredEventId { 16 | return new StoredEventId(value); 17 | } 18 | 19 | getColumnType(prop: EntityProperty, platform: Platform) { 20 | return `varchar(255)`; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/events/domain-events/event-created.event.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '../../../../common/domain/domain-event'; 2 | import Cpf from '../../../../common/domain/value-objects/cpf.vo'; 3 | import { EventId } from '../../entities/event.entity'; 4 | import { PartnerId } from '../../entities/partner.entity'; 5 | 6 | export class EventCreated implements IDomainEvent { 7 | readonly event_version: number = 1; 8 | readonly occurred_on: Date; 9 | 10 | constructor( 11 | readonly aggregate_id: EventId, 12 | readonly name: string, 13 | readonly description: string | null, 14 | readonly date: Date, 15 | readonly is_published: boolean, 16 | readonly total_spots: number, 17 | readonly total_spots_reserved: number, 18 | readonly partner_id: PartnerId, 19 | ) { 20 | this.occurred_on = new Date(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/infra/db/repositories/event-mysql.repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityManager } from '@mikro-orm/mysql'; 2 | import { Event, EventId } from '../../../domain/entities/event.entity'; 3 | import { IEventRepository } from '../../../domain/repositories/event-repository.interface'; 4 | 5 | export class EventMysqlRepository implements IEventRepository { 6 | constructor(private entityManager: EntityManager) {} 7 | 8 | async add(entity: Event): Promise { 9 | this.entityManager.persist(entity); 10 | } 11 | 12 | async findById(id: string | EventId): Promise { 13 | return this.entityManager.findOne(Event, { 14 | id: typeof id === 'string' ? new EventId(id) : id, 15 | }); 16 | } 17 | 18 | async findAll(): Promise { 19 | return this.entityManager.find(Event, {}); 20 | } 21 | 22 | async delete(entity: Event): Promise { 23 | await this.entityManager.remove(entity); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/infra/db/repositories/order-mysql.repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityManager } from '@mikro-orm/mysql'; 2 | import { Order, OrderId } from '../../../domain/entities/order.entity'; 3 | import { IOrderRepository } from '../../../domain/repositories/order-repository.interface'; 4 | 5 | export class OrderMysqlRepository implements IOrderRepository { 6 | constructor(private entityManager: EntityManager) {} 7 | 8 | async add(entity: Order): Promise { 9 | this.entityManager.persist(entity); 10 | } 11 | 12 | async findById(id: string | OrderId): Promise { 13 | return this.entityManager.findOne(Order, { 14 | id: typeof id === 'string' ? new OrderId(id) : id, 15 | }); 16 | } 17 | 18 | async findAll(): Promise { 19 | return this.entityManager.find(Order, {}); 20 | } 21 | 22 | async delete(entity: Order): Promise { 23 | await this.entityManager.remove(entity); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/application/handlers/my-handler.handler.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEventHandler } from '../../../common/application/domain-event-handler.interface'; 2 | import { DomainEventManager } from '../../../common/domain/domain-event-manager'; 3 | import { PartnerCreated } from '../../domain/events/domain-events/partner-created.event'; 4 | import { IPartnerRepository } from '../../domain/repositories/partner-repository.interface'; 5 | 6 | export class MyHandlerHandler implements IDomainEventHandler { 7 | constructor( 8 | private partnerRepo: IPartnerRepository, 9 | private domainEventManager: DomainEventManager, 10 | ) {} 11 | 12 | async handle(event: PartnerCreated): Promise { 13 | console.log(event); 14 | //manipular agregados 15 | //this.partnerRepo.add() 16 | //await this.domainEventManager.publish(agregado) 17 | } 18 | 19 | static listensTo(): string[] { 20 | return [PartnerCreated.name]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { DatabaseModule } from './database/database.module'; 5 | import { EventsModule } from './events/events.module'; 6 | import { DomainEventsModule } from './domain-events/domain-events.module'; 7 | import { ApplicationModule } from './application/application.module'; 8 | import { BullModule } from '@nestjs/bull'; 9 | import { RabbitmqModule } from './rabbitmq/rabbitmq.module'; 10 | 11 | @Module({ 12 | imports: [ 13 | DatabaseModule, 14 | BullModule.forRoot({ 15 | redis: { 16 | host: 'localhost', 17 | port: 6379, 18 | }, 19 | }), 20 | EventsModule, 21 | DomainEventsModule, 22 | ApplicationModule, 23 | RabbitmqModule, 24 | ], 25 | controllers: [AppController], 26 | providers: [AppService], 27 | }) 28 | export class AppModule {} 29 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/infra/db/repositories/partner-mysql.repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityManager } from '@mikro-orm/mysql'; 2 | import { Partner, PartnerId } from '../../../domain/entities/partner.entity'; 3 | import { IPartnerRepository } from '../../../domain/repositories/partner-repository.interface'; 4 | 5 | export class PartnerMysqlRepository implements IPartnerRepository { 6 | constructor(private entityManager: EntityManager) {} 7 | 8 | async add(entity: Partner): Promise { 9 | this.entityManager.persist(entity); 10 | } 11 | 12 | findById(id: string | PartnerId): Promise { 13 | return this.entityManager.findOneOrFail(Partner, { 14 | id: typeof id === 'string' ? new PartnerId(id) : id, 15 | }); 16 | } 17 | 18 | findAll(): Promise { 19 | return this.entityManager.find(Partner, {}); 20 | } 21 | 22 | async delete(entity: Partner): Promise { 23 | this.entityManager.remove(entity); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/infra/db/repositories/customer-mysql.repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityManager } from '@mikro-orm/mysql'; 2 | import { Customer, CustomerId } from '../../../domain/entities/customer.entity'; 3 | import { ICustomerRepository } from '../../../domain/repositories/customer-repository.interface'; 4 | 5 | export class CustomerMysqlRepository implements ICustomerRepository { 6 | constructor(private entityManager: EntityManager) {} 7 | 8 | async add(entity: Customer): Promise { 9 | this.entityManager.persist(entity); 10 | } 11 | 12 | async findById(id: string | CustomerId): Promise { 13 | return this.entityManager.findOne(Customer, { 14 | id: typeof id === 'string' ? new CustomerId(id) : id, 15 | }); 16 | } 17 | 18 | async findAll(): Promise { 19 | return this.entityManager.find(Customer, {}); 20 | } 21 | 22 | async delete(entity: Customer): Promise { 23 | await this.entityManager.remove(entity); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/infra/db/schemas.spec.ts: -------------------------------------------------------------------------------- 1 | import { MikroORM, MySqlDriver } from '@mikro-orm/mysql'; 2 | import { PartnerSchema } from './schemas'; 3 | import { Partner } from '../../domain/entities/partner.entity'; 4 | 5 | test('deve criar um partner', async () => { 6 | const orm = await MikroORM.init({ 7 | entities: [PartnerSchema], 8 | dbName: 'events', 9 | host: 'localhost', 10 | port: 3306, 11 | user: 'root', 12 | password: 'root', 13 | type: 'mysql', 14 | forceEntityConstructor: true, 15 | }); 16 | await orm.schema.refreshDatabase(); 17 | const em = orm.em.fork(); 18 | 19 | const partner = Partner.create({ name: 'Partner 1' }); 20 | console.log(partner.id); 21 | em.persist(partner); 22 | await em.flush(); 23 | await em.clear(); // limpa o cache do entity manager (unit of work) 24 | 25 | const partnerFound = await em.findOne(Partner, { id: partner.id }); 26 | console.log(partnerFound); 27 | 28 | await orm.close(); 29 | }); 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MBA Full Cycle - Domain Driven Design 2 | 3 | Este repositório contém o código-fonte e material didático do curso de Domain Driven Design do MBA Full Cycle. 4 | 5 | O projeto é feito com Nestjs, mas o conteúdo é independente de linguagem ou framework. 6 | 7 | ## Pré-requisitos 8 | 9 | - Node.js 18+ 10 | - Docker 11 | 12 | ## Executar o projeto 13 | 14 | Suba as aplicações MySQL, RabbitMQ e Redis: 15 | 16 | ```bash 17 | docker-compose up -d 18 | ``` 19 | 20 | Instale as dependências do Node.js: 21 | 22 | ```bash 23 | npm install 24 | ``` 25 | 26 | Use o arquivo `api.http` como referência para fazer as requisições HTTP. Este arquivo funciona com a extensão [REST Client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) do VSCode. 27 | 28 | ## Professor 29 | 30 | 31 | 32 |
33 | 34 | Luiz Carlos 35 | 36 |
37 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/entities/__tests__/customer.entity.spec.ts: -------------------------------------------------------------------------------- 1 | import Cpf from '../../../../common/domain/value-objects/cpf.vo'; 2 | import { Customer, CustomerId } from '../customer.entity'; 3 | 4 | test('deve criar um cliente', () => { 5 | const customer = Customer.create({ 6 | name: 'João', 7 | cpf: '99346413050', 8 | }); 9 | console.log(customer); 10 | expect(customer).toBeInstanceOf(Customer); 11 | expect(customer.id).toBeDefined(); 12 | expect(customer.id).toBeInstanceOf(CustomerId); 13 | expect(customer.name).toBe('João'); 14 | expect(customer.cpf.value).toBe('99346413050'); 15 | 16 | const customer2 = new Customer({ 17 | id: new CustomerId(customer.id.value), 18 | name: 'João xpto', 19 | cpf: new Cpf('703.758.870-91'), 20 | }); 21 | 22 | console.log(customer.equals(customer2)); 23 | 24 | // não é valido 25 | // customer = new Customer({ 26 | // id: '123', new CustomerId() || new CustomerId('') 27 | // name: 'João', 28 | // cpf: '99346413050', 29 | // }); 30 | }); 31 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "apps/mba-ddd-venda-ingresso/src", 5 | "compilerOptions": { 6 | "deleteOutDir": true, 7 | "builder": "swc", 8 | "typeCheck": true, 9 | "webpack": true, 10 | "tsConfigPath": "apps/mba-ddd-venda-ingresso/tsconfig.app.json" 11 | }, 12 | "monorepo": true, 13 | "root": "apps/mba-ddd-venda-ingresso", 14 | "projects": { 15 | "mba-ddd-venda-ingresso": { 16 | "type": "application", 17 | "root": "apps/mba-ddd-venda-ingresso", 18 | "entryFile": "main", 19 | "sourceRoot": "apps/mba-ddd-venda-ingresso/src", 20 | "compilerOptions": { 21 | "tsConfigPath": "apps/mba-ddd-venda-ingresso/tsconfig.app.json" 22 | } 23 | }, 24 | "emails": { 25 | "type": "application", 26 | "root": "apps/emails", 27 | "entryFile": "main", 28 | "sourceRoot": "apps/emails/src", 29 | "compilerOptions": { 30 | "tsConfigPath": "apps/emails/tsconfig.app.json" 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /ORMs e design patterns.md: -------------------------------------------------------------------------------- 1 | # Javascript 2 | Sequelize - Active Record 3 | Prisma - Active Record 4 | TypeORM - Active Record / Data Mapper * (Unit of Work) 5 | Knex - queries mais de baixo de nível 6 | Bookshelf - Active Record 7 | Mikro ORM - Data Mapper 8 | 9 | # Python 10 | Django ORM - Active Record 11 | 12 | 13 | # Ruby 14 | Rails - Active Record 15 | 16 | # Java 17 | Hibernate - Data Mapper 18 | 19 | # PHP 20 | Doctrine - Data Mapper 21 | Eloquent - Active Record 22 | 23 | # .Net 24 | Entity Framework - Data Mapper 25 | 26 | 27 | # Active Record vs Data Mapper 28 | 29 | 30 | # Dúvidas gerais 31 | 32 | ## Regras dos agregados 33 | 34 | - Um agregado é uma transação atômica 35 | - Um agregado protege invariantes de consistência 36 | - Um agregado referência outros agregados por identidade 37 | - Somente um agregado deve ser processado por transação 38 | 39 | ## Razões para quebrar as regras dos agregados 40 | 41 | - Conveniência da Interface do Usuário 42 | - A falta de mecanismos técnicos ou restrições de negócios 43 | - Transações globais (legados) 44 | - Desempenho das consultas (referências) -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/events/events/event-spots.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Param, Put } from '@nestjs/common'; 2 | import { EventService } from '../../@core/events/application/event.service'; 3 | 4 | @Controller('events/:event_id/sections/:section_id/spots') 5 | export class EventSpotsController { 6 | constructor(private eventService: EventService) {} 7 | 8 | @Get() 9 | async list( 10 | @Param('event_id') event_id: string, 11 | @Param('section_id') section_id: string, 12 | ) { 13 | return this.eventService.findSpots({ 14 | event_id: event_id, 15 | section_id: section_id, 16 | }); 17 | } 18 | 19 | @Put(':spot_id') 20 | update( 21 | @Param('event_id') event_id: string, 22 | @Param('section_id') section_id: string, 23 | @Param('spot_id') spot_id: string, 24 | @Body() 25 | body: { 26 | location: string; 27 | }, 28 | ) { 29 | return this.eventService.updateLocation({ 30 | ...body, 31 | event_id: event_id, 32 | section_id: section_id, 33 | spot_id: spot_id, 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/common/infra/unit-of-work-mikro-orm.ts: -------------------------------------------------------------------------------- 1 | import { EntityManager } from '@mikro-orm/mysql'; 2 | import { IUnitOfWork } from '../application/unit-of-work.interface'; 3 | import { AggregateRoot } from '../domain/aggregate-root'; 4 | 5 | export class UnitOfWorkMikroOrm implements IUnitOfWork { 6 | constructor(private em: EntityManager) {} 7 | 8 | beginTransaction(): Promise { 9 | return this.em.begin(); 10 | } 11 | completeTransaction(): Promise { 12 | return this.em.commit(); 13 | } 14 | rollbackTransaction(): Promise { 15 | return this.em.rollback(); 16 | } 17 | 18 | runTransaction(callback: () => Promise): Promise { 19 | return this.em.transactional(callback); 20 | } 21 | 22 | commit(): Promise { 23 | return this.em.flush(); 24 | } 25 | 26 | async rollback(): Promise { 27 | this.em.clear(); 28 | } 29 | 30 | getAggregateRoots(): AggregateRoot[] { 31 | return [ 32 | ...this.em.getUnitOfWork().getPersistStack(), 33 | ...this.em.getUnitOfWork().getRemoveStack(), 34 | ] as AggregateRoot[]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/application/customer.service.ts: -------------------------------------------------------------------------------- 1 | import { IUnitOfWork } from '../../common/application/unit-of-work.interface'; 2 | import { Customer } from '../domain/entities/customer.entity'; 3 | import { ICustomerRepository } from '../domain/repositories/customer-repository.interface'; 4 | 5 | export class CustomerService { 6 | constructor( 7 | private customerRepo: ICustomerRepository, 8 | private uow: IUnitOfWork, 9 | ) {} 10 | 11 | list() { 12 | return this.customerRepo.findAll(); 13 | } 14 | 15 | async register(input: { name: string; cpf: string }) { 16 | const customer = Customer.create(input); 17 | this.customerRepo.add(customer); 18 | await this.uow.commit(); 19 | return customer; 20 | } 21 | 22 | async update(id: string, input: { name?: string }) { 23 | const customer = await this.customerRepo.findById(id); 24 | 25 | if (!customer) { 26 | throw new Error('Customer not found'); 27 | } 28 | 29 | input.name && customer.changeName(input.name); 30 | 31 | this.customerRepo.add(customer); 32 | await this.uow.commit(); 33 | 34 | return customer; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/infra/db/repositories/spot-reservation-mysql.repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityManager } from '@mikro-orm/mysql'; 2 | import { SpotReservation } from '../../../domain/entities/spot-reservation.entity'; 3 | import { ISpotReservationRepository } from '../../../domain/repositories/spot-reservation-repository.interface'; 4 | import { EventSpotId } from '../../../domain/entities/event-spot'; 5 | 6 | export class SpotReservationMysqlRepository 7 | implements ISpotReservationRepository 8 | { 9 | constructor(private entityManager: EntityManager) {} 10 | 11 | async add(entity: SpotReservation): Promise { 12 | this.entityManager.persist(entity); 13 | } 14 | 15 | async findById(spot_id: string | EventSpotId): Promise { 16 | return this.entityManager.findOne(SpotReservation, { 17 | spot_id: typeof spot_id === 'string' ? new EventSpotId(spot_id) : spot_id, 18 | }); 19 | } 20 | 21 | async findAll(): Promise { 22 | return this.entityManager.find(SpotReservation, {}); 23 | } 24 | 25 | async delete(entity: SpotReservation): Promise { 26 | await this.entityManager.remove(entity); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/stored-events/infra/db/repositories/stored-event-mysql.repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityManager } from '@mikro-orm/mysql'; 2 | import { IStoredEventRepository } from '../../../domain/repositories/stored-event.repository'; 3 | import { 4 | StoredEventId, 5 | StoredEvent, 6 | } from '../../../domain/entities/stored-event.entity'; 7 | import { IDomainEvent } from '../../../../common/domain/domain-event'; 8 | 9 | export class StoredEventMysqlRepository implements IStoredEventRepository { 10 | constructor(private entityManager: EntityManager) {} 11 | 12 | allBetween( 13 | lowEventId: StoredEventId, 14 | highEventId: StoredEventId, 15 | ): Promise { 16 | return this.entityManager.find(StoredEvent, { 17 | id: { $gte: lowEventId, $lte: highEventId }, 18 | }); 19 | } 20 | 21 | allSince(eventId: StoredEventId): Promise { 22 | return this.entityManager.find(StoredEvent, { id: { $gte: eventId } }); 23 | } 24 | 25 | add(domainEvent: IDomainEvent): StoredEvent { 26 | const storedEvent = StoredEvent.create(domainEvent); 27 | this.entityManager.persist(storedEvent); 28 | return storedEvent; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/application/partner.service.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationService } from '../../common/application/application.service'; 2 | import { Partner } from '../domain/entities/partner.entity'; 3 | import { IPartnerRepository } from '../domain/repositories/partner-repository.interface'; 4 | 5 | export class PartnerService { 6 | constructor( 7 | private partnerRepo: IPartnerRepository, 8 | private applicationService: ApplicationService, 9 | ) {} 10 | 11 | list() { 12 | return this.partnerRepo.findAll(); 13 | } 14 | 15 | async create(input: { name: string }) { 16 | return await this.applicationService.run(async () => { 17 | const partner = Partner.create(input); 18 | await this.partnerRepo.add(partner); 19 | return partner; 20 | }); 21 | } 22 | 23 | async update(id: string, input: { name?: string }) { 24 | return this.applicationService.run(async () => { 25 | const partner = await this.partnerRepo.findById(id); 26 | 27 | if (!partner) { 28 | throw new Error('Partner not found'); 29 | } 30 | 31 | input.name && partner.changeName(input.name); 32 | 33 | await this.partnerRepo.add(partner); 34 | return partner; 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/common/application/application.service.ts: -------------------------------------------------------------------------------- 1 | import { DomainEventManager } from '../domain/domain-event-manager'; 2 | import { IUnitOfWork } from './unit-of-work.interface'; 3 | 4 | export class ApplicationService { 5 | constructor( 6 | private uow: IUnitOfWork, 7 | private domainEventManager: DomainEventManager, 8 | ) {} 9 | 10 | // eslint-disable-next-line @typescript-eslint/no-empty-function 11 | start() {} 12 | 13 | async finish() { 14 | const aggregateRoots = this.uow.getAggregateRoots(); 15 | for (const aggregateRoot of aggregateRoots) { 16 | await this.domainEventManager.publish(aggregateRoot); 17 | } 18 | await this.uow.commit(); 19 | for (const aggregateRoot of aggregateRoots) { 20 | await this.domainEventManager.publishForIntegrationEvent(aggregateRoot); 21 | } 22 | } 23 | 24 | // eslint-disable-next-line @typescript-eslint/no-empty-function 25 | fail() {} 26 | 27 | async run(callback: () => Promise): Promise { 28 | await this.start(); 29 | try { 30 | const result = await callback(); 31 | await this.finish(); 32 | return result; 33 | } catch (e) { 34 | await this.fail(); 35 | throw e; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/events/events/event-sections.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Param, Post, Put } from '@nestjs/common'; 2 | import { EventService } from '../../@core/events/application/event.service'; 3 | 4 | @Controller('events/:event_id/sections') 5 | export class EventSectionsController { 6 | constructor(private eventService: EventService) {} 7 | 8 | @Get() 9 | async list(@Param('event_id') event_id: string) { 10 | return this.eventService.findSections(event_id); 11 | } 12 | 13 | @Post() 14 | create( 15 | @Param('event_id') event_id: string, 16 | @Body() 17 | body: { 18 | name: string; 19 | description?: string | null; 20 | total_spots: number; 21 | price: number; 22 | }, 23 | ) { 24 | return this.eventService.addSection({ 25 | ...body, 26 | event_id: event_id, 27 | }); 28 | } 29 | 30 | @Put(':section_id') 31 | update( 32 | @Param('event_id') event_id: string, 33 | @Param('section_id') section_id: string, 34 | @Body() 35 | body: { 36 | name: string; 37 | description?: string | null; 38 | }, 39 | ) { 40 | return this.eventService.updateSection({ 41 | ...body, 42 | event_id: event_id, 43 | section_id: section_id, 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/common/domain/domain-event-manager.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter2 from 'eventemitter2'; 2 | import { AggregateRoot } from './aggregate-root'; 3 | 4 | export class DomainEventManager { 5 | domainEventsSubscriber: EventEmitter2; 6 | integrationEventsSubscriber: EventEmitter2; 7 | 8 | constructor() { 9 | this.domainEventsSubscriber = new EventEmitter2({ 10 | wildcard: true, 11 | }); 12 | this.integrationEventsSubscriber = new EventEmitter2({ 13 | wildcard: true, 14 | }); 15 | } 16 | 17 | register(event: string, handler: any) { 18 | this.domainEventsSubscriber.on(event, handler); 19 | } 20 | 21 | registerForIntegrationEvent(event: string, handler: any) { 22 | this.integrationEventsSubscriber.on(event, handler); 23 | } 24 | 25 | async publish(aggregateRoot: AggregateRoot) { 26 | for (const event of aggregateRoot.events) { 27 | const eventClassName = event.constructor.name; 28 | await this.domainEventsSubscriber.emitAsync(eventClassName, event); 29 | } 30 | } 31 | 32 | async publishForIntegrationEvent(aggregateRoot: AggregateRoot) { 33 | for (const event of aggregateRoot.events) { 34 | const eventClassName = event.constructor.name; 35 | await this.integrationEventsSubscriber.emitAsync(eventClassName, event); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/stored-events/domain/entities/stored-event.entity.ts: -------------------------------------------------------------------------------- 1 | import { AggregateRoot } from '../../../common/domain/aggregate-root'; 2 | import { IDomainEvent } from '../../../common/domain/domain-event'; 3 | import Uuid from '../../../common/domain/value-objects/uuid.vo'; 4 | 5 | export class StoredEventId extends Uuid {} 6 | 7 | export type StoredEventConstructorProps = { 8 | body: string; 9 | occurred_on: Date; 10 | type_name: string; 11 | }; 12 | 13 | export type StoredEventCommand = { 14 | domain_event: IDomainEvent; 15 | occurred_on: Date; 16 | }; 17 | 18 | export class StoredEvent extends AggregateRoot { 19 | id: StoredEventId; 20 | body: string; 21 | occurred_on: Date; 22 | type_name: string; 23 | 24 | constructor(props: StoredEventConstructorProps, id?: StoredEventId) { 25 | super(); 26 | this.id = id ?? new StoredEventId(); 27 | this.body = props.body; 28 | this.occurred_on = props.occurred_on; 29 | this.type_name = props.type_name; 30 | } 31 | 32 | static create(domainEvent: IDomainEvent) { 33 | return new StoredEvent({ 34 | body: JSON.stringify(domainEvent), 35 | type_name: domainEvent.constructor.name, 36 | occurred_on: domainEvent.occurred_on, 37 | }); 38 | } 39 | 40 | toJSON() { 41 | return { 42 | id: this.id, 43 | body: this.body, 44 | ocurred_on: this.occurred_on, 45 | }; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/events/events/events.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Param, Post, Put } from '@nestjs/common'; 2 | import { EventService } from '../../@core/events/application/event.service'; 3 | import { EventDto } from './event.dto'; 4 | 5 | @Controller('events') 6 | export class EventsController { 7 | constructor(private eventService: EventService) {} 8 | 9 | @Get() 10 | async list() { 11 | const events = await this.eventService.findEvents(); 12 | logSizeInBytes('events', events[0]); 13 | return events; 14 | } 15 | 16 | @Post() 17 | create( 18 | @Body() 19 | body: EventDto, 20 | ) { 21 | return this.eventService.create(body); 22 | } 23 | 24 | @Put(':event_id/publish-all') 25 | publish(@Param('event_id') event_id: string) { 26 | return this.eventService.publishAll({ event_id: event_id }); 27 | } 28 | } 29 | 30 | const getSizeInBytes = (obj) => { 31 | let str = null; 32 | if (typeof obj === 'string') { 33 | // If obj is a string, then use it 34 | str = obj; 35 | } else { 36 | // Else, make obj into a string 37 | str = JSON.stringify(obj); 38 | } 39 | // Get the length of the Uint8Array 40 | const bytes = new TextEncoder().encode(str).length; 41 | return bytes; 42 | }; 43 | 44 | const logSizeInBytes = (description, obj) => { 45 | const bytes = getSizeInBytes(obj); 46 | console.log(`${description} is approximately ${bytes} B`); 47 | }; 48 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/database/database.module.ts: -------------------------------------------------------------------------------- 1 | import { MikroOrmModule } from '@mikro-orm/nestjs'; 2 | import { Global, Module } from '@nestjs/common'; 3 | import { 4 | CustomerSchema, 5 | EventSchema, 6 | EventSectionSchema, 7 | EventSpotSchema, 8 | OrderSchema, 9 | PartnerSchema, 10 | SpotReservationSchema, 11 | } from '../@core/events/infra/db/schemas'; 12 | import { EntityManager } from '@mikro-orm/mysql'; 13 | import { UnitOfWorkMikroOrm } from '../@core/common/infra/unit-of-work-mikro-orm'; 14 | import { StoredEventSchema } from '../@core/stored-events/infra/db/schemas'; 15 | 16 | @Global() 17 | @Module({ 18 | imports: [ 19 | MikroOrmModule.forRoot({ 20 | entities: [ 21 | CustomerSchema, 22 | PartnerSchema, 23 | EventSchema, 24 | EventSectionSchema, 25 | EventSpotSchema, 26 | OrderSchema, 27 | SpotReservationSchema, 28 | StoredEventSchema, 29 | ], 30 | dbName: 'events', 31 | host: 'localhost', 32 | port: 3306, 33 | user: 'root', 34 | password: 'root', 35 | type: 'mysql', 36 | forceEntityConstructor: true, 37 | }), 38 | ], 39 | providers: [ 40 | { 41 | provide: 'IUnitOfWork', 42 | useFactory(em: EntityManager) { 43 | return new UnitOfWorkMikroOrm(em); 44 | }, 45 | inject: [EntityManager], 46 | }, 47 | ], 48 | exports: ['IUnitOfWork'], 49 | }) 50 | export class DatabaseModule {} 51 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/domain-events/domain-events.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module, OnModuleInit } from '@nestjs/common'; 2 | import { DomainEventManager } from '../@core/common/domain/domain-event-manager'; 3 | import { IntegrationEventsPublisher } from './integration-events.publisher'; 4 | import { MikroOrmModule } from '@mikro-orm/nestjs'; 5 | import { StoredEventMysqlRepository } from '../@core/stored-events/infra/db/repositories/stored-event-mysql.repository'; 6 | import { EntityManager } from '@mikro-orm/mysql'; 7 | import { IDomainEvent } from '../@core/common/domain/domain-event'; 8 | import { ModuleRef } from '@nestjs/core'; 9 | import { StoredEventSchema } from '../@core/stored-events/infra/db/schemas'; 10 | 11 | @Global() 12 | @Module({ 13 | imports: [MikroOrmModule.forFeature([StoredEventSchema])], 14 | providers: [ 15 | DomainEventManager, 16 | IntegrationEventsPublisher, 17 | { 18 | provide: 'IStoredEventRepository', 19 | useFactory: (em) => new StoredEventMysqlRepository(em), 20 | inject: [EntityManager], 21 | }, 22 | ], 23 | exports: [DomainEventManager], 24 | }) 25 | export class DomainEventsModule implements OnModuleInit { 26 | constructor( 27 | private readonly domainEventManager: DomainEventManager, 28 | private moduleRef: ModuleRef, 29 | ) {} 30 | 31 | onModuleInit() { 32 | this.domainEventManager.register('*', async (event: IDomainEvent) => { 33 | const repo = await this.moduleRef.resolve('IStoredEventRepository'); 34 | await repo.add(event); 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/common/domain/value-objects/value-object.ts: -------------------------------------------------------------------------------- 1 | import isEqual from 'lodash/isEqual'; 2 | 3 | export abstract class ValueObject { 4 | protected readonly _value: Value; 5 | 6 | constructor(value: Value) { 7 | this._value = deepFreeze(value); 8 | } 9 | 10 | get value(): Value { 11 | return this._value; 12 | } 13 | 14 | public equals(obj: this): boolean { 15 | if (obj === null || obj === undefined) { 16 | return false; 17 | } 18 | 19 | if (obj.value === undefined) { 20 | return false; 21 | } 22 | 23 | if (obj.constructor.name !== this.constructor.name) { 24 | return false; 25 | } 26 | 27 | return isEqual(this.value, obj.value); 28 | } 29 | 30 | toString = () => { 31 | if (typeof this.value !== 'object' || this.value === null) { 32 | try { 33 | return this.value.toString(); 34 | } catch (e) { 35 | return this.value + ''; 36 | } 37 | } 38 | const valueStr = this.value.toString(); 39 | return valueStr === '[object Object]' 40 | ? JSON.stringify(this.value) 41 | : valueStr; 42 | }; 43 | } 44 | 45 | export function deepFreeze(obj: T) { 46 | try { 47 | const propNames = Object.getOwnPropertyNames(obj); 48 | 49 | for (const name of propNames) { 50 | const value = obj[name as keyof T]; 51 | 52 | if (value && typeof value === 'object') { 53 | deepFreeze(value); 54 | } 55 | } 56 | 57 | return Object.freeze(obj); 58 | } catch (e) { 59 | return obj; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/common/domain/value-objects/cpf.vo.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from './value-object'; 2 | 3 | export class Cpf extends ValueObject { 4 | constructor(value: string) { 5 | super(value.replace(/\D/g, '')); 6 | this.validate(); 7 | } 8 | 9 | private validate() { 10 | if (this.value.length != 11) { 11 | throw new InvalidCpfError( 12 | 'CPF must have 11 digits, but has ' + this.value.length + ' digits', 13 | ); 14 | } 15 | 16 | const allDigitsEquals = /^\d{1}(\d)\1{10}$/.test(this.value); 17 | if (allDigitsEquals) { 18 | throw new InvalidCpfError('CPF must have at least two different digits'); 19 | } 20 | 21 | let sum = 0; 22 | for (let i = 0; i < 9; i++) { 23 | sum += parseInt(this.value.charAt(i)) * (10 - i); 24 | } 25 | let firstDigit = 11 - (sum % 11); 26 | if (firstDigit > 9) { 27 | firstDigit = 0; 28 | } 29 | 30 | sum = 0; 31 | for (let i = 0; i < 10; i++) { 32 | sum += parseInt(this.value.charAt(i)) * (11 - i); 33 | } 34 | let secondDigit = 11 - (sum % 11); 35 | if (secondDigit > 9) { 36 | secondDigit = 0; 37 | } 38 | 39 | if ( 40 | firstDigit !== parseInt(this.value.charAt(9)) || 41 | secondDigit !== parseInt(this.value.charAt(10)) 42 | ) { 43 | throw new InvalidCpfError('CPF is invalid'); 44 | } 45 | } 46 | } 47 | 48 | export class InvalidCpfError extends Error { 49 | constructor(message) { 50 | super(message); 51 | this.name = 'InvalidCpfError'; 52 | } 53 | } 54 | 55 | export default Cpf; 56 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/entities/event-spot.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '../../../common/domain/entity'; 2 | import Uuid from '../../../common/domain/value-objects/uuid.vo'; 3 | 4 | export class EventSpotId extends Uuid {} 5 | 6 | export type EventSpotConstructorProps = { 7 | id?: EventSpotId | string; 8 | location: string | null; 9 | is_reserved: boolean; 10 | is_published: boolean; 11 | }; 12 | 13 | export class EventSpot extends Entity { 14 | id: EventSpotId; 15 | location: string | null; 16 | is_reserved: boolean; 17 | is_published: boolean; 18 | 19 | constructor(props: EventSpotConstructorProps) { 20 | super(); 21 | this.id = 22 | typeof props.id === 'string' 23 | ? new EventSpotId(props.id) 24 | : props.id ?? new EventSpotId(); 25 | this.location = props.location; 26 | this.is_reserved = props.is_reserved; 27 | this.is_published = props.is_published; 28 | } 29 | 30 | static create() { 31 | return new EventSpot({ 32 | location: null, 33 | is_published: false, 34 | is_reserved: false, 35 | }); 36 | } 37 | 38 | changeLocation(location: string) { 39 | this.location = location; 40 | } 41 | 42 | publish() { 43 | this.is_published = true; 44 | } 45 | 46 | unPublish() { 47 | this.is_published = false; 48 | } 49 | 50 | markAsReserved() { 51 | this.is_reserved = true; 52 | } 53 | 54 | toJSON() { 55 | return { 56 | id: this.id.value, 57 | location: this.location, 58 | reserved: this.is_reserved, 59 | is_published: this.is_published, 60 | }; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/entities/customer.entity.ts: -------------------------------------------------------------------------------- 1 | import { AggregateRoot } from '../../../common/domain/aggregate-root'; 2 | import Cpf from '../../../common/domain/value-objects/cpf.vo'; 3 | import Uuid from '../../../common/domain/value-objects/uuid.vo'; 4 | import { CustomerChangedName } from '../events/domain-events/customer-changed-name.event'; 5 | import { CustomerCreated } from '../events/domain-events/customer-created.event'; 6 | 7 | export class CustomerId extends Uuid {} 8 | 9 | export type CustomerConstructorProps = { 10 | id?: CustomerId | string; 11 | cpf: Cpf; 12 | name: string; 13 | }; 14 | 15 | export class Customer extends AggregateRoot { 16 | id: CustomerId; 17 | cpf: Cpf; 18 | name: string; 19 | 20 | constructor(props: CustomerConstructorProps) { 21 | super(); 22 | this.id = 23 | typeof props.id === 'string' 24 | ? new CustomerId(props.id) 25 | : props.id ?? new CustomerId(); 26 | this.cpf = props.cpf; 27 | this.name = props.name; 28 | } 29 | 30 | static create(command: { name: string; cpf: string }) { 31 | const customer = new Customer({ 32 | name: command.name, 33 | cpf: new Cpf(command.cpf), 34 | }); 35 | customer.addEvent( 36 | new CustomerCreated(customer.id, customer.name, customer.cpf), 37 | ); 38 | return customer; 39 | } 40 | 41 | changeName(name: string) { 42 | this.name = name; 43 | this.addEvent(new CustomerChangedName(this.id, this.name)); 44 | } 45 | 46 | toJSON() { 47 | return { 48 | id: this.id.value, 49 | cpf: this.cpf.value, 50 | name: this.name, 51 | }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/entities/partner.entity.ts: -------------------------------------------------------------------------------- 1 | import { AggregateRoot } from '../../../common/domain/aggregate-root'; 2 | import Uuid from '../../../common/domain/value-objects/uuid.vo'; 3 | import { PartnerChangedName } from '../events/domain-events/partner-changed-name.event'; 4 | import { PartnerCreated } from '../events/domain-events/partner-created.event'; 5 | import { Event } from './event.entity'; 6 | 7 | export class PartnerId extends Uuid {} 8 | 9 | export type InitEventCommand = { 10 | name: string; 11 | description?: string | null; 12 | date: Date; 13 | }; 14 | 15 | export type PartnerConstructorProps = { 16 | id?: PartnerId | string; 17 | name: string; 18 | }; 19 | 20 | export class Partner extends AggregateRoot { 21 | id: PartnerId; 22 | name: string; 23 | 24 | constructor(props: PartnerConstructorProps, id?: PartnerId) { 25 | super(); 26 | this.id = 27 | typeof props.id === 'string' 28 | ? new PartnerId(props.id) 29 | : props.id ?? new PartnerId(); 30 | this.name = props.name; 31 | } 32 | 33 | static create(command: { name: string }) { 34 | const partner = new Partner({ 35 | name: command.name, 36 | }); 37 | partner.addEvent(new PartnerCreated(partner.id, partner.name)); 38 | return partner; 39 | } 40 | 41 | initEvent(command: InitEventCommand) { 42 | return Event.create({ 43 | ...command, 44 | partner_id: this.id, 45 | }); 46 | } 47 | 48 | changeName(name: string) { 49 | this.name = name; 50 | this.addEvent(new PartnerChangedName(this.id, this.name)); 51 | } 52 | 53 | toJSON() { 54 | return { 55 | id: this.id.value, 56 | name: this.name, 57 | }; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/infra/db/repositories/__tests__/partner-mysql.repository.spec.ts: -------------------------------------------------------------------------------- 1 | import { MikroORM, MySqlDriver } from '@mikro-orm/mysql'; 2 | import { PartnerSchema } from '../../schemas'; 3 | import { Partner } from '../../../../domain/entities/partner.entity'; 4 | import { PartnerMysqlRepository } from '../partner-mysql.repository'; 5 | 6 | test('partner repository', async () => { 7 | const orm = await MikroORM.init({ 8 | entities: [PartnerSchema], 9 | dbName: 'events', 10 | host: 'localhost', 11 | port: 3306, 12 | user: 'root', 13 | password: 'root', 14 | type: 'mysql', 15 | forceEntityConstructor: true, 16 | }); 17 | await orm.schema.refreshDatabase(); 18 | const em = orm.em.fork(); 19 | const partnerRepo = new PartnerMysqlRepository(em); 20 | 21 | const partner = Partner.create({ name: 'Partner 1' }); 22 | await partnerRepo.add(partner); 23 | await em.flush(); 24 | await em.clear(); // limpa o cache do entity manager (unit of work) 25 | 26 | let partnerFound = await partnerRepo.findById(partner.id); 27 | expect(partnerFound.id.equals(partner.id)).toBeTruthy(); 28 | expect(partnerFound.name).toBe(partner.name); 29 | 30 | partner.changeName('Partner 2'); 31 | await partnerRepo.add(partner); 32 | await em.flush(); 33 | await em.clear(); // limpa o cache do entity manager (unit of work) 34 | 35 | partnerFound = await partnerRepo.findById(partner.id); 36 | expect(partnerFound.id.equals(partner.id)).toBeTruthy(); 37 | expect(partnerFound.name).toBe(partner.name); 38 | 39 | console.log(await partnerRepo.findAll()); 40 | 41 | partnerRepo.delete(partner); 42 | await em.flush(); 43 | 44 | console.log(await partnerRepo.findAll()); 45 | 46 | await orm.close(); 47 | }); 48 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/infra/db/repositories/__tests__/event-mysql.repository.spec.ts: -------------------------------------------------------------------------------- 1 | import { MikroORM, MySqlDriver } from '@mikro-orm/mysql'; 2 | import { 3 | EventSchema, 4 | EventSectionSchema, 5 | EventSpotSchema, 6 | PartnerSchema, 7 | } from '../../schemas'; 8 | import { Event } from '../../../../domain/entities/event.entity'; 9 | import { EventMysqlRepository } from '../event-mysql.repository'; 10 | import { Partner } from '../../../../domain/entities/partner.entity'; 11 | import { PartnerMysqlRepository } from '../partner-mysql.repository'; 12 | 13 | test('Event repository', async () => { 14 | const orm = await MikroORM.init({ 15 | entities: [EventSchema, EventSectionSchema, EventSpotSchema, PartnerSchema], 16 | dbName: 'events', 17 | host: 'localhost', 18 | port: 3306, 19 | user: 'root', 20 | password: 'root', 21 | type: 'mysql', 22 | forceEntityConstructor: true, 23 | debug: true, 24 | }); 25 | await orm.schema.refreshDatabase(); 26 | const em = orm.em.fork(); 27 | const partnerRepo = new PartnerMysqlRepository(em); 28 | const eventRepo = new EventMysqlRepository(em); 29 | 30 | const partner = Partner.create({ name: 'Partner 1' }); 31 | await partnerRepo.add(partner); 32 | const event = partner.initEvent({ 33 | name: 'Event 1', 34 | date: new Date(), 35 | description: 'Event 1 description', 36 | }); 37 | 38 | event.addSection({ 39 | name: 'Section 1', 40 | description: 'Section 1 description', 41 | price: 100, 42 | total_spots: 1000, 43 | }); 44 | 45 | await eventRepo.add(event); 46 | await em.flush(); 47 | await em.clear(); 48 | 49 | const eventFound = await eventRepo.findById(event.id); 50 | console.log(eventFound); 51 | 52 | await orm.close(); 53 | }); 54 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/infra/db/repositories/__tests__/customer-mysql.repository.spec.ts: -------------------------------------------------------------------------------- 1 | import { MikroORM, MySqlDriver } from '@mikro-orm/mysql'; 2 | import { CustomerSchema } from '../../schemas'; 3 | import { Customer } from '../../../../domain/entities/customer.entity'; 4 | import { CustomerMysqlRepository } from '../customer-mysql.repository'; 5 | 6 | test('Customer repository', async () => { 7 | const orm = await MikroORM.init({ 8 | entities: [CustomerSchema], 9 | dbName: 'events', 10 | host: 'localhost', 11 | port: 3306, 12 | user: 'root', 13 | password: 'root', 14 | type: 'mysql', 15 | forceEntityConstructor: true, 16 | }); 17 | await orm.schema.refreshDatabase(); 18 | const em = orm.em.fork(); 19 | const customerRepo = new CustomerMysqlRepository(em); 20 | 21 | const customer = Customer.create({ 22 | name: 'Customer 1', 23 | cpf: '703.758.870-91', 24 | }); 25 | await customerRepo.add(customer); 26 | await em.flush(); 27 | await em.clear(); // limpa o cache do entity manager (unit of work) 28 | 29 | let customerFound = await customerRepo.findById(customer.id); 30 | expect(customerFound.id.equals(customer.id)).toBeTruthy(); 31 | expect(customerFound.name).toBe(customer.name); 32 | expect(customerFound.cpf.value).toBe('70375887091'); 33 | 34 | customer.changeName('Customer 2'); 35 | await customerRepo.add(customer); 36 | await em.flush(); 37 | await em.clear(); // limpa o cache do entity manager (unit of work) 38 | 39 | customerFound = await customerRepo.findById(customer.id); 40 | expect(customerFound.id.equals(customer.id)).toBeTruthy(); 41 | expect(customerFound.name).toBe(customer.name); 42 | 43 | console.log(await customerRepo.findAll()); 44 | 45 | customerRepo.delete(customer); 46 | await em.flush(); 47 | 48 | console.log(await customerRepo.findAll()); 49 | 50 | await orm.close(); 51 | }); 52 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/entities/__tests__/event.entity.spec.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '../event.entity'; 2 | import { PartnerId } from '../partner.entity'; 3 | import { initOrm } from './helpers'; 4 | 5 | describe('Event Entity Unit Tests', () => { 6 | initOrm(); 7 | it('deve criar um evento', () => { 8 | const event = Event.create({ 9 | name: 'Evento 1', 10 | description: 'Descrição do evento 1', 11 | date: new Date(), 12 | partner_id: new PartnerId(), 13 | }); 14 | 15 | event.addSection({ 16 | name: 'Sessão 1', 17 | description: 'Descrição da sessão 1', 18 | total_spots: 100, 19 | price: 1000, 20 | }); 21 | 22 | expect(event.sections.size).toBe(1); 23 | expect(event.total_spots).toBe(100); 24 | 25 | const [section] = event.sections; 26 | 27 | expect(section.spots.size).toBe(100); 28 | 29 | // const spot = EventSpot.create(); 30 | 31 | // section.spots.add(spot); 32 | 33 | // console.dir(event.toJSON(), { depth: 10 }); 34 | 35 | // não é valido 36 | // customer = new Customer({ 37 | // id: '123', new CustomerId() || new CustomerId('') 38 | // name: 'João', 39 | // cpf: '99346413050', 40 | // }); 41 | }); 42 | 43 | test('deve publicar todos os itens do evento', () => { 44 | const event = Event.create({ 45 | name: 'Evento 1', 46 | description: 'Descrição do evento 1', 47 | date: new Date(), 48 | partner_id: new PartnerId(), 49 | }); 50 | 51 | event.addSection({ 52 | name: 'Sessão 1', 53 | description: 'Descrição da sessão 1', 54 | total_spots: 100, 55 | price: 1000, 56 | }); 57 | 58 | event.addSection({ 59 | name: 'Sessão 2', 60 | description: 'Descrição da sessão 2', 61 | total_spots: 1000, 62 | price: 50, 63 | }); 64 | 65 | event.publishAll(); 66 | 67 | expect(event.is_published).toBe(true); 68 | 69 | const [section1, section2] = event._sections.values(); 70 | expect(section1.is_published).toBe(true); 71 | expect(section2.is_published).toBe(true); 72 | 73 | [...section1.spots, ...section2.spots].forEach((spot) => { 74 | expect(spot.is_published).toBe(true); 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/entities/spot-reservation.entity.ts: -------------------------------------------------------------------------------- 1 | import { AggregateRoot } from '../../../common/domain/aggregate-root'; 2 | import { SpotReservationChanged } from '../events/domain-events/spot-reservation-changed.event'; 3 | import { SpotReservationCreated } from '../events/domain-events/spot-reservation-created.event'; 4 | import { CustomerId } from './customer.entity'; 5 | import { EventSpotId } from './event-spot'; 6 | 7 | export type SpotReservationCreateCommand = { 8 | spot_id: EventSpotId | string; 9 | customer_id: CustomerId; 10 | }; 11 | 12 | export type SpotReservationConstructorProps = { 13 | spot_id: EventSpotId | string; 14 | reservation_date: Date; 15 | customer_id: CustomerId; 16 | }; 17 | 18 | export class SpotReservation extends AggregateRoot { 19 | spot_id: EventSpotId; 20 | reservation_date: Date; 21 | customer_id: CustomerId; 22 | 23 | constructor(props: SpotReservationConstructorProps) { 24 | super(); 25 | this.spot_id = 26 | props.spot_id instanceof EventSpotId 27 | ? props.spot_id 28 | : new EventSpotId(props.spot_id); 29 | this.reservation_date = props.reservation_date; 30 | this.customer_id = 31 | props.customer_id instanceof CustomerId 32 | ? props.customer_id 33 | : new CustomerId(props.customer_id); 34 | } 35 | 36 | static create(command: SpotReservationCreateCommand) { 37 | const reservation = new SpotReservation({ 38 | spot_id: command.spot_id, 39 | customer_id: command.customer_id, 40 | reservation_date: new Date(), 41 | }); 42 | reservation.addEvent( 43 | new SpotReservationCreated( 44 | reservation.spot_id, 45 | reservation.reservation_date, 46 | reservation.customer_id, 47 | ), 48 | ); 49 | return reservation; 50 | } 51 | 52 | changeReservation(customer_id: CustomerId) { 53 | this.customer_id = customer_id; 54 | this.reservation_date = new Date(); 55 | this.addEvent( 56 | new SpotReservationChanged( 57 | this.spot_id, 58 | this.reservation_date, 59 | this.customer_id, 60 | ), 61 | ); 62 | } 63 | 64 | toJSON() { 65 | return { 66 | spot_id: this.spot_id.value, 67 | customer_id: this.customer_id.value, 68 | reservation_date: this.reservation_date, 69 | }; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /api.http: -------------------------------------------------------------------------------- 1 | ### 2 | GET http://localhost:3000 3 | 4 | ### 5 | GET http://localhost:3000/partners 6 | 7 | ### 8 | POST http://localhost:3000/partners 9 | Content-Type: application/json 10 | 11 | { 12 | "name": "Partner 1" 13 | } 14 | 15 | ### 16 | @partner_id = 076a2ae5-b4ed-438d-89fc-e1a4b42372ba 17 | 18 | ### 19 | GET http://localhost:3000/customers 20 | 21 | ### 22 | POST http://localhost:3000/customers 23 | Content-Type: application/json 24 | 25 | { 26 | "name": "Customer 1", 27 | "cpf": "592.110.870-74" 28 | } 29 | 30 | ### 31 | @customer_id = 61f0ff6e-0bb1-4635-8899-4b88ce46975b 32 | 33 | ### 34 | GET http://localhost:3000/events 35 | 36 | ### 37 | POST http://localhost:3000/events 38 | Content-Type: application/json 39 | 40 | { 41 | "name": "Event 1", 42 | "description": "Description 1", 43 | "date": "2020-01-01T00:00:00.000Z", 44 | "partner_id": "{{partner_id}}" 45 | } 46 | 47 | ### 48 | @event_id = 3181cb03-59e6-469a-9942-358b07882a72 49 | 50 | ### 51 | GET http://localhost:3000/events/{{event_id}}/sections 52 | 53 | ### 54 | POST http://localhost:3000/events/{{event_id}}/sections 55 | Content-Type: application/json 56 | 57 | { 58 | "name": "Section 1", 59 | "description": "Description 1", 60 | "total_spots": 1, 61 | "price": 200 62 | } 63 | 64 | ### 65 | PUT http://localhost:3000/events/{{event_id}}/publish-all 66 | 67 | ### 68 | @section_id = 42a5f5b1-30c5-4b2f-b9b8-84e5cc948542 69 | 70 | ### 71 | PUT http://localhost:3000/events/{{event_id}}/sections/{{section_id}} 72 | Content-Type: application/json 73 | 74 | { 75 | "name": "Section 1 updateddddd", 76 | "description": "Description 1 updatedddd" 77 | } 78 | 79 | ### 80 | GET http://localhost:3000/events/{{event_id}}/sections/{{section_id}}/spots 81 | 82 | ### 83 | PUT http://localhost:3000/events/{{event_id}}/sections/{{section_id}}/spots/2179f4e2-b3bc-4be9-b55d-f0876f24987d 84 | Content-Type: application/json 85 | 86 | { 87 | "location": "Location 1 updateddddqqqq" 88 | } 89 | 90 | ### 91 | GET http://localhost:3000/events/{{event_id}}/orders 92 | 93 | ### 94 | POST http://localhost:3000/events/{{event_id}}/orders 95 | Content-Type: application/json 96 | 97 | { 98 | "customer_id": "{{customer_id}}", 99 | "section_id": "{{section_id}}", 100 | "spot_id": "96298e63-5a38-4352-b97d-322259f10921", 101 | "card_token": "tok_visa" 102 | } -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/entities/order.entity.ts: -------------------------------------------------------------------------------- 1 | import { AggregateRoot } from '../../../common/domain/aggregate-root'; 2 | import Uuid from '../../../common/domain/value-objects/uuid.vo'; 3 | import { OrderCancelled } from '../events/domain-events/order-cancelled.event'; 4 | import { OrderCreated } from '../events/domain-events/order-created.event'; 5 | import { OrderPaid } from '../events/domain-events/order-paid.event'; 6 | import { CustomerId } from './customer.entity'; 7 | import { EventSpotId } from './event-spot'; 8 | 9 | export enum OrderStatus { 10 | PENDING, 11 | PAID, 12 | CANCELLED, 13 | } 14 | 15 | export class OrderId extends Uuid {} 16 | 17 | export type OrderConstructorProps = { 18 | id?: OrderId | string; 19 | customer_id: CustomerId; 20 | amount: number; 21 | event_spot_id: EventSpotId; 22 | }; 23 | 24 | export class Order extends AggregateRoot { 25 | id: OrderId; 26 | customer_id: CustomerId; 27 | amount: number; 28 | event_spot_id: EventSpotId; 29 | status: OrderStatus = OrderStatus.PENDING; 30 | 31 | constructor(props: OrderConstructorProps) { 32 | super(); 33 | this.id = 34 | typeof props.id === 'string' 35 | ? new OrderId(props.id) 36 | : props.id ?? new OrderId(); 37 | this.amount = props.amount; 38 | this.customer_id = 39 | props.customer_id instanceof CustomerId 40 | ? props.customer_id 41 | : new CustomerId(props.customer_id); 42 | this.event_spot_id = 43 | props.event_spot_id instanceof EventSpotId 44 | ? props.event_spot_id 45 | : new EventSpotId(props.event_spot_id); 46 | } 47 | 48 | static create(props: OrderConstructorProps) { 49 | const order = new Order(props); 50 | order.addEvent( 51 | new OrderCreated( 52 | order.id, 53 | order.customer_id, 54 | order.amount, 55 | order.event_spot_id, 56 | order.status, 57 | ), 58 | ); 59 | return order; 60 | } 61 | 62 | pay() { 63 | this.status = OrderStatus.PAID; 64 | this.addEvent(new OrderPaid(this.id, this.status)); 65 | } 66 | 67 | cancel() { 68 | this.status = OrderStatus.CANCELLED; 69 | this.addEvent(new OrderCancelled(this.id, this.status)); 70 | } 71 | 72 | toJSON() { 73 | return { 74 | id: this.id.value, 75 | amount: this.amount, 76 | customer_id: this.customer_id.value, 77 | event_spot_id: this.event_spot_id.value, 78 | }; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/common/domain/my-collection.ts: -------------------------------------------------------------------------------- 1 | import { Collection } from '@mikro-orm/core'; 2 | 3 | export interface ICollection { 4 | getItems(): Iterable; 5 | add(item: T, ...items: T[]): void; 6 | remove(item: T, ...items: T[]): void; 7 | find(predicate: (item: T) => boolean): T | undefined; 8 | forEach(callbackfn: (value: T, index: number) => void): void; 9 | map(callbackfn: (value: T, index: number) => U): U[]; 10 | removeAll(): void; 11 | count(): number; 12 | size: number; 13 | values(): T[]; 14 | [Symbol.iterator](): IterableIterator; 15 | } 16 | //Design Pattern - Proxy 17 | 18 | export type AnyCollection = Collection; 19 | 20 | export class MyCollectionFactory { 21 | static create(ref): ICollection { 22 | const collection = new Collection(ref); 23 | collection['initialized'] = false; 24 | return MyCollectionFactory.createProxy(collection); 25 | } 26 | 27 | static createFrom(target: Collection): ICollection { 28 | return MyCollectionFactory.createProxy(target); 29 | } 30 | 31 | private static createProxy( 32 | target: Collection, 33 | ): ICollection { 34 | //@ts-expect-error - Proxy 35 | return new Proxy(target, { 36 | get(target, prop, receiver) { 37 | if (prop === 'find') { 38 | return (predicate: (item: T) => boolean): T | undefined => { 39 | return target.getItems(false).find(predicate); 40 | }; 41 | } 42 | 43 | if (prop === 'forEach') { 44 | return (callbackfn: (value: T, index: number) => void): void => { 45 | return target.getItems(false).forEach(callbackfn); 46 | }; 47 | } 48 | 49 | if (prop === 'count') { 50 | return () => { 51 | return target.isInitialized() ? target.getItems().length : 0; 52 | }; 53 | } 54 | 55 | if (prop === 'map') { 56 | return (callbackfn: (value: T, index: number) => any): any[] => { 57 | return target.getItems(false).map(callbackfn); 58 | }; 59 | } 60 | 61 | if (prop === 'size') { 62 | return target.getItems(false).length; 63 | } 64 | 65 | if (prop === 'values') { 66 | return () => { 67 | return target.getItems(false); 68 | }; 69 | } 70 | 71 | return Reflect.get(target, prop, receiver); 72 | }, 73 | }); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/application/customer.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { MikroORM, MySqlDriver } from '@mikro-orm/mysql'; 2 | import { CustomerSchema } from '../infra/db/schemas'; 3 | import { CustomerMysqlRepository } from '../infra/db/repositories/customer-mysql.repository'; 4 | import { Customer } from '../domain/entities/customer.entity'; 5 | import { CustomerService } from './customer.service'; 6 | import { UnitOfWorkMikroOrm } from '../../common/infra/unit-of-work-mikro-orm'; 7 | 8 | test('deve listar os customers', async () => { 9 | const orm = await MikroORM.init({ 10 | entities: [CustomerSchema], 11 | dbName: 'events', 12 | host: 'localhost', 13 | port: 3306, 14 | user: 'root', 15 | password: 'root', 16 | type: 'mysql', 17 | forceEntityConstructor: true, 18 | }); 19 | await orm.schema.refreshDatabase(); 20 | const em = orm.em.fork(); 21 | const unitOfWork = new UnitOfWorkMikroOrm(em); 22 | const customerRepo = new CustomerMysqlRepository(em); 23 | const customerService = new CustomerService(customerRepo, unitOfWork); 24 | const customer = Customer.create({ 25 | name: 'Customer 1', 26 | cpf: '70375887091', 27 | }); 28 | await customerRepo.add(customer); 29 | await em.flush(); 30 | await em.clear(); 31 | 32 | const customers = await customerService.list(); 33 | console.log(customers); 34 | await orm.close(); 35 | }); 36 | 37 | test('deve registrar um customer', async () => { 38 | const orm = await MikroORM.init({ 39 | entities: [CustomerSchema], 40 | dbName: 'events', 41 | host: 'localhost', 42 | port: 3306, 43 | user: 'root', 44 | password: 'root', 45 | type: 'mysql', 46 | forceEntityConstructor: true, 47 | }); 48 | await orm.schema.refreshDatabase(); 49 | const em = orm.em.fork(); 50 | const unitOfWork = new UnitOfWorkMikroOrm(em); 51 | const customerRepo = new CustomerMysqlRepository(em); 52 | const customerService = new CustomerService(customerRepo, unitOfWork); 53 | 54 | const customer = await customerService.register({ 55 | name: 'Customer 1', 56 | cpf: '70375887091', 57 | }); 58 | 59 | expect(customer).toBeInstanceOf(Customer); 60 | expect(customer.id).toBeDefined(); 61 | expect(customer.name).toBe('Customer 1'); 62 | expect(customer.cpf.value).toBe('70375887091'); 63 | 64 | await em.clear(); 65 | 66 | const customerFound = await customerRepo.findById(customer.id); 67 | expect(customerFound).toBeInstanceOf(Customer); 68 | expect(customerFound.id).toBeDefined(); 69 | expect(customerFound.name).toBe('Customer 1'); 70 | expect(customerFound.cpf.value).toBe('70375887091'); 71 | 72 | await orm.close(); 73 | }); 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mba-ddd-venda-ingresso", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"apps/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/apps/mba-ddd-venda-ingresso/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "test": "jest --runInBand", 17 | "test:watch": "jest --watch --runInBand", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./apps/mba-ddd-venda-ingresso/test/jest-e2e.json", 21 | "prepare": "husky install" 22 | }, 23 | "dependencies": { 24 | "@golevelup/nestjs-rabbitmq": "^4.0.0", 25 | "@mikro-orm/cli": "^5.7.12", 26 | "@mikro-orm/core": "^5.7.12", 27 | "@mikro-orm/mysql": "^5.7.12", 28 | "@mikro-orm/nestjs": "^5.2.0", 29 | "@nestjs/bull": "^10.0.1", 30 | "@nestjs/common": "^10.0.0", 31 | "@nestjs/core": "^10.0.0", 32 | "@nestjs/platform-express": "^10.0.0", 33 | "bull": "^4.10.4", 34 | "class-transformer": "^0.5.1", 35 | "class-validator": "^0.14.0", 36 | "eventemitter2": "^6.4.9", 37 | "reflect-metadata": "^0.1.13", 38 | "rxjs": "^7.8.1", 39 | "uuid": "^9.0.0" 40 | }, 41 | "devDependencies": { 42 | "@nestjs/cli": "^10.0.0", 43 | "@nestjs/schematics": "^10.0.0", 44 | "@nestjs/testing": "^10.0.0", 45 | "@swc/cli": "^0.1.62", 46 | "@swc/core": "^1.3.68", 47 | "@swc/jest": "^0.2.26", 48 | "@types/express": "^4.17.17", 49 | "@types/jest": "^29.5.2", 50 | "@types/node": "^20.3.1", 51 | "@types/supertest": "^2.0.12", 52 | "@typescript-eslint/eslint-plugin": "^5.59.11", 53 | "@typescript-eslint/parser": "^5.59.11", 54 | "eslint": "^8.42.0", 55 | "eslint-config-prettier": "^8.8.0", 56 | "eslint-plugin-prettier": "^4.2.1", 57 | "husky": "^8.0.3", 58 | "jest": "^29.6.1", 59 | "prettier": "^2.8.8", 60 | "source-map-support": "^0.5.21", 61 | "supertest": "^6.3.3", 62 | "ts-jest": "^29.1.0", 63 | "ts-loader": "^9.4.3", 64 | "ts-node": "^10.9.1", 65 | "tsconfig-paths": "^4.2.0", 66 | "typescript": "^5.1.3" 67 | }, 68 | "jest": { 69 | "moduleFileExtensions": [ 70 | "js", 71 | "json", 72 | "ts" 73 | ], 74 | "rootDir": ".", 75 | "testRegex": ".*\\.spec\\.ts$", 76 | "transform": { 77 | "^.+\\.(t|j)s$": "@swc/jest" 78 | }, 79 | "collectCoverageFrom": [ 80 | "**/*.(t|j)s" 81 | ], 82 | "coverageDirectory": "./coverage", 83 | "testEnvironment": "node", 84 | "roots": [ 85 | "/apps/" 86 | ] 87 | }, 88 | "mikro-orm": { 89 | "useTsNode": true, 90 | "configPaths": [ 91 | "./apps/mba-ddd-venda-ingresso/src/mikro-orm.config.ts", 92 | "./dist/mikro-orm.config.js" 93 | ] 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/application/order.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { MikroORM, MySqlDriver } from '@mikro-orm/mysql'; 2 | import { 3 | CustomerSchema, 4 | EventSchema, 5 | EventSectionSchema, 6 | EventSpotSchema, 7 | OrderSchema, 8 | PartnerSchema, 9 | SpotReservationSchema, 10 | } from '../infra/db/schemas'; 11 | import { CustomerMysqlRepository } from '../infra/db/repositories/customer-mysql.repository'; 12 | import { Customer } from '../domain/entities/customer.entity'; 13 | import { UnitOfWorkMikroOrm } from '../../common/infra/unit-of-work-mikro-orm'; 14 | import { PartnerMysqlRepository } from '../infra/db/repositories/partner-mysql.repository'; 15 | import { Partner } from '../domain/entities/partner.entity'; 16 | import { EventMysqlRepository } from '../infra/db/repositories/event-mysql.repository'; 17 | import { OrderService } from './order.service'; 18 | import { OrderMysqlRepository } from '../infra/db/repositories/order-mysql.repository'; 19 | import { SpotReservationMysqlRepository } from '../infra/db/repositories/spot-reservation-mysql.repository'; 20 | 21 | test('deve criar uma order', async () => { 22 | const orm = await MikroORM.init({ 23 | entities: [ 24 | CustomerSchema, 25 | PartnerSchema, 26 | EventSchema, 27 | EventSectionSchema, 28 | EventSpotSchema, 29 | OrderSchema, 30 | SpotReservationSchema, 31 | ], 32 | dbName: 'events', 33 | host: 'localhost', 34 | port: 3306, 35 | user: 'root', 36 | password: 'root', 37 | type: 'mysql', 38 | forceEntityConstructor: true, 39 | }); 40 | await orm.schema.refreshDatabase(); 41 | const em = orm.em.fork(); 42 | const unitOfWork = new UnitOfWorkMikroOrm(em); 43 | const customerRepo = new CustomerMysqlRepository(em); 44 | const partnerRepo = new PartnerMysqlRepository(em); 45 | const eventRepo = new EventMysqlRepository(em); 46 | const customer = Customer.create({ 47 | name: 'Customer 1', 48 | cpf: '70375887091', 49 | }); 50 | await customerRepo.add(customer); 51 | 52 | const partner = Partner.create({ 53 | name: 'Partner 1', 54 | }); 55 | await partnerRepo.add(partner); 56 | 57 | const event = partner.initEvent({ 58 | name: 'Event 1', 59 | description: 'Event 1', 60 | date: new Date(), 61 | }); 62 | 63 | event.addSection({ 64 | name: 'Section 1', 65 | description: 'Section 1', 66 | price: 100, 67 | total_spots: 1000, 68 | }); 69 | 70 | event.publishAll(); 71 | 72 | await eventRepo.add(event); 73 | 74 | await unitOfWork.commit(); 75 | await em.clear(); 76 | 77 | const orderRepo = new OrderMysqlRepository(em); 78 | const spotReservationRepo = new SpotReservationMysqlRepository(em); 79 | const orderService = new OrderService( 80 | orderRepo, 81 | customerRepo, 82 | eventRepo, 83 | spotReservationRepo, 84 | unitOfWork, 85 | ); 86 | 87 | const op1 = orderService.create({ 88 | event_id: event.id.value, 89 | section_id: event.sections[0].id.value, 90 | customer_id: customer.id.value, 91 | spot_id: event.sections[0].spots[0].id.value, 92 | }); 93 | 94 | const op2 = orderService.create({ 95 | event_id: event.id.value, 96 | section_id: event.sections[0].id.value, 97 | customer_id: customer.id.value, 98 | spot_id: event.sections[0].spots[0].id.value, 99 | }); 100 | 101 | try { 102 | await Promise.all([op1, op2]); 103 | } catch (e) { 104 | console.log(e); 105 | console.log(await orderRepo.findAll()); 106 | console.log(await spotReservationRepo.findAll()); 107 | } 108 | 109 | await orm.close(); 110 | }); 111 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/application/order.service.ts: -------------------------------------------------------------------------------- 1 | import { IUnitOfWork } from '../../common/application/unit-of-work.interface'; 2 | import { EventSectionId } from '../domain/entities/event-section'; 3 | import { EventSpotId } from '../domain/entities/event-spot'; 4 | import { Order } from '../domain/entities/order.entity'; 5 | import { SpotReservation } from '../domain/entities/spot-reservation.entity'; 6 | import { ICustomerRepository } from '../domain/repositories/customer-repository.interface'; 7 | import { IEventRepository } from '../domain/repositories/event-repository.interface'; 8 | import { IOrderRepository } from '../domain/repositories/order-repository.interface'; 9 | import { ISpotReservationRepository } from '../domain/repositories/spot-reservation-repository.interface'; 10 | import { PaymentGateway } from './payment.gateway'; 11 | 12 | export class OrderService { 13 | constructor( 14 | private orderRepo: IOrderRepository, 15 | private customerRepo: ICustomerRepository, 16 | private eventRepo: IEventRepository, 17 | private spotReservationRepo: ISpotReservationRepository, 18 | private uow: IUnitOfWork, 19 | private paymentGateway: PaymentGateway, 20 | ) {} 21 | 22 | list() { 23 | return this.orderRepo.findAll(); 24 | } 25 | 26 | async create(input: { 27 | event_id: string; 28 | section_id: string; 29 | spot_id: string; 30 | customer_id: string; 31 | card_token: string; 32 | }) { 33 | //const {customer, event} = Promise.all([]) 34 | const customer = await this.customerRepo.findById(input.customer_id); 35 | 36 | if (!customer) { 37 | throw new Error('Customer not found'); 38 | } 39 | 40 | const event = await this.eventRepo.findById(input.event_id); 41 | 42 | if (!event) { 43 | throw new Error('Event not found'); 44 | } 45 | 46 | const sectionId = new EventSectionId(input.section_id); 47 | const spotId = new EventSpotId(input.spot_id); 48 | 49 | if (!event.allowReserveSpot({ section_id: sectionId, spot_id: spotId })) { 50 | throw new Error('Spot not available'); 51 | } 52 | 53 | const spotReservation = await this.spotReservationRepo.findById(spotId); 54 | 55 | if (spotReservation) { 56 | throw new Error('Spot already reserved'); 57 | } 58 | return this.uow.runTransaction(async () => { 59 | const spotReservationCreated = SpotReservation.create({ 60 | spot_id: spotId, 61 | customer_id: customer.id, 62 | }); 63 | 64 | await this.spotReservationRepo.add(spotReservationCreated); 65 | try { 66 | await this.uow.commit(); 67 | const section = event.sections.find((s) => s.id.equals(sectionId)); 68 | await this.paymentGateway.payment({ 69 | token: input.card_token, 70 | amount: section.price, 71 | }); 72 | 73 | const order = Order.create({ 74 | customer_id: customer.id, 75 | event_spot_id: spotId, 76 | amount: section.price, 77 | }); 78 | order.pay(); 79 | await this.orderRepo.add(order); 80 | 81 | event.markSpotAsReserved({ 82 | section_id: sectionId, 83 | spot_id: spotId, 84 | }); 85 | 86 | this.eventRepo.add(event); 87 | 88 | await this.uow.commit(); 89 | return order; 90 | } catch (e) { 91 | const section = event.sections.find((s) => s.id.equals(sectionId)); 92 | const order = Order.create({ 93 | customer_id: customer.id, 94 | event_spot_id: spotId, 95 | amount: section.price, 96 | }); 97 | order.cancel(); 98 | this.orderRepo.add(order); 99 | await this.uow.commit(); 100 | throw new Error('Aconteceu um erro reservar o seu lugar'); 101 | } 102 | }); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/entities/event-section.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '../../../common/domain/entity'; 2 | import { 3 | AnyCollection, 4 | ICollection, 5 | MyCollectionFactory, 6 | } from '../../../common/domain/my-collection'; 7 | import Uuid from '../../../common/domain/value-objects/uuid.vo'; 8 | import { EventSpot, EventSpotId } from './event-spot'; 9 | 10 | export class EventSectionId extends Uuid {} 11 | 12 | export type EventSectionCreateCommand = { 13 | name: string; 14 | description?: string | null; 15 | total_spots: number; 16 | price: number; 17 | }; 18 | 19 | export type EventSectionConstructorProps = { 20 | id?: EventSectionId | string; 21 | name: string; 22 | description: string | null; 23 | is_published: boolean; 24 | total_spots: number; 25 | total_spots_reserved: number; 26 | price: number; 27 | }; 28 | 29 | export class EventSection extends Entity { 30 | id: EventSectionId; 31 | name: string; 32 | description: string | null; 33 | is_published: boolean; 34 | total_spots: number; 35 | total_spots_reserved: number; 36 | price: number; 37 | private _spots: ICollection; 38 | 39 | constructor(props: EventSectionConstructorProps) { 40 | super(); 41 | this.id = 42 | typeof props.id === 'string' 43 | ? new EventSectionId(props.id) 44 | : props.id ?? new EventSectionId(); 45 | this.name = props.name; 46 | this.description = props.description; 47 | this.is_published = props.is_published; 48 | this.total_spots = props.total_spots; 49 | this.total_spots_reserved = props.total_spots_reserved; 50 | this.price = props.price; 51 | this._spots = MyCollectionFactory.create(this); 52 | } 53 | 54 | static create(command: EventSectionCreateCommand) { 55 | const section = new EventSection({ 56 | ...command, 57 | description: command.description ?? null, 58 | is_published: false, 59 | total_spots_reserved: 0, 60 | }); 61 | 62 | section.initSpots(); 63 | return section; 64 | } 65 | 66 | private initSpots() { 67 | for (let i = 0; i < this.total_spots; i++) { 68 | this.spots.add(EventSpot.create()); 69 | } 70 | } 71 | 72 | changeName(name: string) { 73 | this.name = name; 74 | } 75 | 76 | changeDescription(description: string | null) { 77 | this.description = description; 78 | } 79 | 80 | changePrice(price: number) { 81 | this.price = price; 82 | } 83 | 84 | changeLocation(command: { spot_id: EventSpotId; location: string }) { 85 | const spot = this.spots.find((spot) => spot.id.equals(command.spot_id)); 86 | if (!spot) { 87 | throw new Error('Spot not found'); 88 | } 89 | spot.changeLocation(command.location); 90 | } 91 | 92 | publishAll() { 93 | this.publish(); 94 | this.spots.forEach((spot) => spot.publish()); 95 | } 96 | 97 | publish() { 98 | this.is_published = true; 99 | } 100 | 101 | unPublish() { 102 | this.is_published = false; 103 | } 104 | 105 | allowReserveSpot(spot_id: EventSpotId) { 106 | if (!this.is_published) { 107 | return false; 108 | } 109 | 110 | const spot = this.spots.find((spot) => spot.id.equals(spot_id)); 111 | 112 | if (!spot) { 113 | throw new Error('Spot not found'); 114 | } 115 | 116 | if (spot.is_reserved) { 117 | return false; 118 | } 119 | 120 | if (!spot.is_published) { 121 | return false; 122 | } 123 | 124 | return true; 125 | } 126 | 127 | markSpotAsReserved(spot_id: EventSpotId) { 128 | const spot = this.spots.find((spot) => spot.id.equals(spot_id)); 129 | if (!spot) { 130 | throw new Error('Spot not found'); 131 | } 132 | if (spot.is_reserved) { 133 | throw new Error('Spot already reserved'); 134 | } 135 | spot.markAsReserved(); 136 | } 137 | 138 | get spots(): ICollection { 139 | return this._spots as ICollection; 140 | } 141 | 142 | set spots(spots: AnyCollection) { 143 | this._spots = MyCollectionFactory.createFrom(spots); 144 | } 145 | 146 | toJSON() { 147 | return { 148 | id: this.id.value, 149 | name: this.name, 150 | description: this.description, 151 | is_published: this.is_published, 152 | total_spots: this.total_spots, 153 | total_spots_reserved: this.total_spots_reserved, 154 | price: this.price, 155 | spots: [...this.spots].map((spot) => spot.toJSON()), 156 | }; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/application/event.service.ts: -------------------------------------------------------------------------------- 1 | import { IUnitOfWork } from '../../common/application/unit-of-work.interface'; 2 | import { EventSectionId } from '../domain/entities/event-section'; 3 | import { EventSpotId } from '../domain/entities/event-spot'; 4 | import { IEventRepository } from '../domain/repositories/event-repository.interface'; 5 | import { IPartnerRepository } from '../domain/repositories/partner-repository.interface'; 6 | 7 | export class EventService { 8 | constructor( 9 | private eventRepo: IEventRepository, 10 | private partnerRepo: IPartnerRepository, 11 | private uow: IUnitOfWork, 12 | ) {} 13 | 14 | findEvents() { 15 | return this.eventRepo.findAll(); 16 | } 17 | 18 | async findSections(event_id: string) { 19 | const event = await this.eventRepo.findById(event_id); 20 | return event.sections; 21 | } 22 | 23 | async create(input: { 24 | name: string; 25 | description?: string | null; 26 | date: Date; 27 | partner_id: string; 28 | }) { 29 | const partner = await this.partnerRepo.findById(input.partner_id); 30 | if (!partner) { 31 | throw new Error('Partner not found'); 32 | } 33 | 34 | const event = partner.initEvent({ 35 | name: input.name, 36 | date: input.date, 37 | description: input.description, 38 | }); 39 | 40 | this.eventRepo.add(event); 41 | await this.uow.commit(); 42 | return event; 43 | } 44 | 45 | async update( 46 | id: string, 47 | input: { name?: string; description?: string; date?: Date }, 48 | ) { 49 | const event = await this.eventRepo.findById(id); 50 | 51 | if (!event) { 52 | throw new Error('Event not found'); 53 | } 54 | 55 | input.name && event.changeName(input.name); 56 | input.description && event.changeDescription(input.description); 57 | input.date && event.changeDate(input.date); 58 | 59 | this.eventRepo.add(event); 60 | await this.uow.commit(); 61 | 62 | return event; 63 | } 64 | 65 | async addSection(input: { 66 | name: string; 67 | description?: string | null; 68 | total_spots: number; 69 | price: number; 70 | event_id: string; 71 | }) { 72 | const event = await this.eventRepo.findById(input.event_id); 73 | 74 | if (!event) { 75 | throw new Error('Event not found'); 76 | } 77 | 78 | event.addSection({ 79 | name: input.name, 80 | description: input.description, 81 | total_spots: input.total_spots, 82 | price: input.price, 83 | }); 84 | await this.eventRepo.add(event); 85 | await this.uow.commit(); 86 | return event; 87 | } 88 | 89 | async updateSection(input: { 90 | name: string; 91 | description?: string | null; 92 | event_id: string; 93 | section_id: string; 94 | }) { 95 | const event = await this.eventRepo.findById(input.event_id); 96 | 97 | if (!event) { 98 | throw new Error('Event not found'); 99 | } 100 | 101 | const sectionId = new EventSectionId(input.section_id); 102 | event.changeSectionInformation({ 103 | section_id: sectionId, 104 | name: input.name, 105 | description: input.description, 106 | }); 107 | await this.eventRepo.add(event); 108 | await this.uow.commit(); 109 | return event.sections; 110 | } 111 | 112 | async findSpots(input: { event_id: string; section_id: string }) { 113 | const event = await this.eventRepo.findById(input.event_id); 114 | 115 | if (!event) { 116 | throw new Error('Event not found'); 117 | } 118 | 119 | const section = event.sections.find((section) => 120 | section.id.equals(new EventSectionId(input.section_id)), 121 | ); 122 | if (!section) { 123 | throw new Error('Section not found'); 124 | } 125 | return section.spots; 126 | } 127 | 128 | async updateLocation(input: { 129 | location: string; 130 | event_id: string; 131 | section_id: string; 132 | spot_id: string; 133 | }) { 134 | const event = await this.eventRepo.findById(input.event_id); 135 | 136 | if (!event) { 137 | throw new Error('Event not found'); 138 | } 139 | 140 | const sectionId = new EventSectionId(input.section_id); 141 | const spotId = new EventSpotId(input.spot_id); 142 | event.changeLocation({ 143 | section_id: sectionId, 144 | spot_id: spotId, 145 | location: input.location, 146 | }); 147 | await this.eventRepo.add(event); 148 | const section = event.sections.find((section) => 149 | section.id.equals(new EventSectionId(input.section_id)), 150 | ); 151 | await this.uow.commit(); 152 | return section.spots.find((spot) => spot.id.equals(spotId)); 153 | } 154 | 155 | async publishAll(input: { event_id: string }) { 156 | const event = await this.eventRepo.findById(input.event_id); 157 | 158 | if (!event) { 159 | throw new Error('Event not found'); 160 | } 161 | 162 | event.publishAll(); 163 | 164 | await this.eventRepo.add(event); 165 | await this.uow.commit(); 166 | return event; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/infra/db/schemas.ts: -------------------------------------------------------------------------------- 1 | import { Cascade, EntitySchema } from '@mikro-orm/core'; 2 | import { Partner } from '../../domain/entities/partner.entity'; 3 | import { PartnerIdSchemaType } from './types/partner-id.schema-type'; 4 | import { Customer } from '../../domain/entities/customer.entity'; 5 | import { CustomerIdSchemaType } from './types/customer-id.schema-type'; 6 | import { CpfSchemaType } from './types/cpf.schema-type'; 7 | import { EventIdSchemaType } from './types/event-id.schema-type'; 8 | import { EventSection } from '../../domain/entities/event-section'; 9 | import { EventSectionIdSchemaType } from './types/event-section-id.schema-type'; 10 | import { EventSpot } from '../../domain/entities/event-spot'; 11 | import { EventSpotIdSchemaType } from './types/event-spot-id.schema-type'; 12 | import { Event } from '../../domain/entities/event.entity'; 13 | import { SpotReservation } from '../../domain/entities/spot-reservation.entity'; 14 | import { Order, OrderStatus } from '../../domain/entities/order.entity'; 15 | import { OrderIdSchemaType } from './types/order-id.schema-type'; 16 | 17 | export const PartnerSchema = new EntitySchema({ 18 | class: Partner, 19 | properties: { 20 | id: { primary: true, customType: new PartnerIdSchemaType() }, 21 | name: { type: 'string', length: 255 }, 22 | }, 23 | }); 24 | 25 | export const CustomerSchema = new EntitySchema({ 26 | class: Customer, 27 | uniques: [{ properties: ['cpf'] }], 28 | properties: { 29 | id: { 30 | customType: new CustomerIdSchemaType(), 31 | primary: true, 32 | }, 33 | cpf: { customType: new CpfSchemaType() }, 34 | name: { type: 'string', length: 255 }, 35 | }, 36 | }); 37 | 38 | export const EventSchema = new EntitySchema({ 39 | class: Event, 40 | properties: { 41 | id: { 42 | customType: new EventIdSchemaType(), 43 | primary: true, 44 | }, 45 | name: { type: 'string', length: 255 }, 46 | description: { type: 'text', nullable: true }, 47 | date: { type: 'date' }, 48 | is_published: { type: 'boolean', default: false }, 49 | total_spots: { type: 'number', default: 0 }, 50 | total_spots_reserved: { type: 'number', default: 0 }, 51 | sections: { 52 | reference: '1:m', 53 | entity: () => EventSection, 54 | mappedBy: (section) => section.event_id, 55 | eager: true, 56 | cascade: [Cascade.ALL], 57 | }, 58 | partner_id: { 59 | reference: 'm:1', 60 | entity: () => Partner, 61 | hidden: true, 62 | mapToPk: true, 63 | customType: new PartnerIdSchemaType(), 64 | inherited: true, 65 | }, 66 | }, 67 | }); 68 | 69 | export const EventSectionSchema = new EntitySchema({ 70 | class: EventSection, 71 | properties: { 72 | id: { 73 | customType: new EventSectionIdSchemaType(), 74 | primary: true, 75 | }, 76 | name: { type: 'string', length: 255 }, 77 | description: { type: 'text', nullable: true }, 78 | is_published: { type: 'boolean', default: false }, 79 | total_spots: { type: 'number', default: 0 }, 80 | total_spots_reserved: { type: 'number', default: 0 }, 81 | price: { type: 'number', default: 0 }, 82 | spots: { 83 | reference: '1:m', 84 | entity: () => EventSpot, 85 | mappedBy: (section) => section.event_section_id, 86 | eager: true, 87 | cascade: [Cascade.ALL], 88 | }, 89 | event_id: { 90 | reference: 'm:1', 91 | entity: () => Event, 92 | hidden: true, 93 | mapToPk: true, 94 | customType: new EventIdSchemaType(), 95 | }, 96 | }, 97 | }); 98 | 99 | export const EventSpotSchema = new EntitySchema({ 100 | class: EventSpot, 101 | properties: { 102 | id: { 103 | customType: new EventSpotIdSchemaType(), 104 | primary: true, 105 | }, 106 | location: { type: 'string', length: 255, nullable: true }, 107 | is_reserved: { type: 'boolean', default: false }, 108 | is_published: { type: 'boolean', default: false }, 109 | event_section_id: { 110 | reference: 'm:1', 111 | entity: () => EventSection, 112 | hidden: true, 113 | mapToPk: true, 114 | customType: new EventSectionIdSchemaType(), 115 | }, 116 | }, 117 | }); 118 | 119 | export const SpotReservationSchema = new EntitySchema({ 120 | class: SpotReservation, 121 | properties: { 122 | spot_id: { 123 | customType: new EventSpotIdSchemaType(), 124 | primary: true, 125 | reference: 'm:1', 126 | entity: () => EventSpot, 127 | mapToPk: true, 128 | }, 129 | reservation_date: { type: 'date' }, 130 | customer_id: { 131 | reference: 'm:1', 132 | entity: () => Customer, 133 | mapToPk: true, 134 | hidden: true, 135 | inherited: true, 136 | customType: new CustomerIdSchemaType(), 137 | }, 138 | }, 139 | }); 140 | 141 | export const OrderSchema = new EntitySchema({ 142 | class: Order, 143 | properties: { 144 | id: { 145 | customType: new OrderIdSchemaType(), 146 | primary: true, 147 | }, 148 | amount: { type: 'number' }, 149 | status: { enum: true, items: () => OrderStatus }, 150 | customer_id: { 151 | reference: 'm:1', 152 | entity: () => Customer, 153 | mapToPk: true, 154 | hidden: true, 155 | inherited: true, 156 | customType: new CustomerIdSchemaType(), 157 | }, 158 | event_spot_id: { 159 | reference: 'm:1', 160 | entity: () => EventSpot, 161 | hidden: true, 162 | mapToPk: true, 163 | inherited: true, 164 | customType: new EventSpotIdSchemaType(), 165 | }, 166 | }, 167 | }); 168 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/events/events.module.ts: -------------------------------------------------------------------------------- 1 | import { MikroOrmModule } from '@mikro-orm/nestjs'; 2 | import { Module, OnModuleInit } from '@nestjs/common'; 3 | import { 4 | CustomerSchema, 5 | EventSchema, 6 | EventSectionSchema, 7 | EventSpotSchema, 8 | OrderSchema, 9 | PartnerSchema, 10 | SpotReservationSchema, 11 | } from '../@core/events/infra/db/schemas'; 12 | import { PartnerMysqlRepository } from '../@core/events/infra/db/repositories/partner-mysql.repository'; 13 | import { EntityManager } from '@mikro-orm/mysql'; 14 | import { CustomerMysqlRepository } from '../@core/events/infra/db/repositories/customer-mysql.repository'; 15 | import { EventMysqlRepository } from '../@core/events/infra/db/repositories/event-mysql.repository'; 16 | import { OrderMysqlRepository } from '../@core/events/infra/db/repositories/order-mysql.repository'; 17 | import { SpotReservationMysqlRepository } from '../@core/events/infra/db/repositories/spot-reservation-mysql.repository'; 18 | import { PartnerService } from '../@core/events/application/partner.service'; 19 | import { CustomerService } from '../@core/events/application/customer.service'; 20 | import { EventService } from '../@core/events/application/event.service'; 21 | import { OrderService } from '../@core/events/application/order.service'; 22 | import { PaymentGateway } from '../@core/events/application/payment.gateway'; 23 | import { IPartnerRepository } from '../@core/events/domain/repositories/partner-repository.interface'; 24 | import { PartnersController } from './partners/partners.controller'; 25 | import { CustomersController } from './customers/customers.controller'; 26 | import { EventsController } from './events/events.controller'; 27 | import { EventSectionsController } from './events/event-sections.controller'; 28 | import { EventSpotsController } from './events/event-spots.controller'; 29 | import { OrdersController } from './orders/orders.controller'; 30 | import { ApplicationModule } from '../application/application.module'; 31 | import { ApplicationService } from '../@core/common/application/application.service'; 32 | import { DomainEventManager } from '../@core/common/domain/domain-event-manager'; 33 | import { PartnerCreated } from '../@core/events/domain/events/domain-events/partner-created.event'; 34 | import { MyHandlerHandler } from '../@core/events/application/handlers/my-handler.handler'; 35 | import { ModuleRef } from '@nestjs/core'; 36 | import { BullModule, InjectQueue } from '@nestjs/bull'; 37 | import { Queue } from 'bull'; 38 | import { IIntegrationEvent } from '../@core/common/domain/integration-event'; 39 | import { PartnerCreatedIntegrationEvent } from '../@core/events/domain/events/integration-events/partner-created.int-events'; 40 | 41 | @Module({ 42 | imports: [ 43 | MikroOrmModule.forFeature([ 44 | CustomerSchema, 45 | PartnerSchema, 46 | EventSchema, 47 | EventSectionSchema, 48 | EventSpotSchema, 49 | OrderSchema, 50 | SpotReservationSchema, 51 | ]), 52 | ApplicationModule, 53 | BullModule.registerQueue({ 54 | name: 'integration-events', 55 | }), 56 | ], 57 | providers: [ 58 | { 59 | provide: 'IPartnerRepository', 60 | useFactory: (em: EntityManager) => new PartnerMysqlRepository(em), 61 | inject: [EntityManager], 62 | }, 63 | { 64 | provide: 'ICustomerRepository', 65 | useFactory: (em: EntityManager) => new CustomerMysqlRepository(em), 66 | inject: [EntityManager], 67 | }, 68 | { 69 | provide: 'IEventRepository', 70 | useFactory: (em: EntityManager) => new EventMysqlRepository(em), 71 | inject: [EntityManager], 72 | }, 73 | { 74 | provide: 'IOrderRepository', 75 | useFactory: (em: EntityManager) => new OrderMysqlRepository(em), 76 | inject: [EntityManager], 77 | }, 78 | { 79 | provide: 'ISpotReservationRepository', 80 | useFactory: (em: EntityManager) => new SpotReservationMysqlRepository(em), 81 | inject: [EntityManager], 82 | }, 83 | { 84 | provide: PartnerService, 85 | useFactory: ( 86 | partnerRepo: IPartnerRepository, 87 | appService: ApplicationService, 88 | ) => new PartnerService(partnerRepo, appService), 89 | inject: ['IPartnerRepository', ApplicationService], 90 | }, 91 | { 92 | provide: CustomerService, 93 | useFactory: (customerRepo, uow) => new CustomerService(customerRepo, uow), 94 | inject: ['ICustomerRepository', 'IUnitOfWork'], 95 | }, 96 | { 97 | provide: EventService, 98 | useFactory: (eventRepo, partnerRepo, uow) => 99 | new EventService(eventRepo, partnerRepo, uow), 100 | inject: ['IEventRepository', 'IPartnerRepository', 'IUnitOfWork'], 101 | }, 102 | PaymentGateway, 103 | { 104 | provide: OrderService, 105 | useFactory: ( 106 | orderRepo, 107 | customerRepo, 108 | eventRepo, 109 | spotReservationRepo, 110 | uow, 111 | paymentGateway, 112 | ) => 113 | new OrderService( 114 | orderRepo, 115 | customerRepo, 116 | eventRepo, 117 | spotReservationRepo, 118 | uow, 119 | paymentGateway, 120 | ), 121 | inject: [ 122 | 'IOrderRepository', 123 | 'ICustomerRepository', 124 | 'IEventRepository', 125 | 'ISpotReservationRepository', 126 | 'IUnitOfWork', 127 | PaymentGateway, 128 | ], 129 | }, 130 | { 131 | provide: MyHandlerHandler, 132 | useFactory: ( 133 | partnerRepo: IPartnerRepository, 134 | domainEventManager: DomainEventManager, 135 | ) => new MyHandlerHandler(partnerRepo, domainEventManager), 136 | inject: ['IPartnerRepository', DomainEventManager], 137 | }, 138 | ], 139 | controllers: [ 140 | PartnersController, 141 | CustomersController, 142 | EventsController, 143 | EventSectionsController, 144 | EventSpotsController, 145 | OrdersController, 146 | ], 147 | }) 148 | export class EventsModule implements OnModuleInit { 149 | constructor( 150 | private readonly domainEventManager: DomainEventManager, 151 | private moduleRef: ModuleRef, 152 | @InjectQueue('integration-events') 153 | private integrationEventsQueue: Queue, 154 | ) {} 155 | 156 | onModuleInit() { 157 | console.log('EventsModule initialized'); 158 | MyHandlerHandler.listensTo().forEach((eventName: string) => { 159 | this.domainEventManager.register(eventName, async (event) => { 160 | const handler: MyHandlerHandler = await this.moduleRef.resolve( 161 | MyHandlerHandler, 162 | ); 163 | await handler.handle(event); 164 | }); 165 | }); 166 | this.domainEventManager.registerForIntegrationEvent( 167 | PartnerCreated.name, 168 | async (event) => { 169 | console.log('integration events'); 170 | const integrationEvent = new PartnerCreatedIntegrationEvent(event); 171 | await this.integrationEventsQueue.add(integrationEvent); 172 | }, 173 | ); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /apps/mba-ddd-venda-ingresso/src/@core/events/domain/entities/event.entity.ts: -------------------------------------------------------------------------------- 1 | import { AggregateRoot } from '../../../common/domain/aggregate-root'; 2 | import { PartnerId } from './partner.entity'; 3 | import Uuid from '../../../common/domain/value-objects/uuid.vo'; 4 | import { EventSection, EventSectionId } from './event-section'; 5 | import { 6 | AnyCollection, 7 | ICollection, 8 | MyCollectionFactory, 9 | } from '../../../common/domain/my-collection'; 10 | import { EventSpotId } from './event-spot'; 11 | import { EventCreated } from '../events/domain-events/event-created.event'; 12 | import { EventChangedName } from '../events/domain-events/event-changed-name.event'; 13 | import { EventChangedDescription } from '../events/domain-events/event-changed-description.event'; 14 | import { EventChangedDate } from '../events/domain-events/event-changed-date.event'; 15 | import { EventPublishAll } from '../events/domain-events/event-publish-all.event'; 16 | import { EventPublish } from '../events/domain-events/event-publish.event'; 17 | import { EventUnpublish } from '../events/domain-events/event-unpublish.event'; 18 | import { EventAddedSection } from '../events/domain-events/event-added-section.event'; 19 | import { EventChangedSectionSection } from '../events/domain-events/event-changed-section-information.event'; 20 | import { EventChangedSpotLocation } from '../events/domain-events/event-changed-spot-location.event'; 21 | import { EventMarkedSportAsReserved } from '../events/domain-events/event-marked-sport-as-reserved.event'; 22 | 23 | export class EventId extends Uuid {} 24 | 25 | export type CreateEventCommand = { 26 | name: string; 27 | description?: string | null; 28 | date: Date; 29 | partner_id: PartnerId; 30 | }; 31 | 32 | export type AddSectionCommand = { 33 | name: string; 34 | description?: string | null; 35 | total_spots: number; 36 | price: number; 37 | }; 38 | 39 | export type EventConstructorProps = { 40 | id?: EventId | string; 41 | name: string; 42 | description: string | null; 43 | date: Date; 44 | is_published: boolean; 45 | total_spots: number; 46 | total_spots_reserved: number; 47 | partner_id: PartnerId | string; 48 | }; 49 | 50 | export class Event extends AggregateRoot { 51 | id: EventId; 52 | name: string; 53 | description: string | null; 54 | date: Date; 55 | is_published: boolean; 56 | total_spots: number; 57 | total_spots_reserved: number; 58 | partner_id: PartnerId; 59 | private _sections: ICollection; 60 | 61 | constructor(props: EventConstructorProps) { 62 | super(); 63 | this.id = 64 | typeof props.id === 'string' 65 | ? new EventId(props.id) 66 | : props.id ?? new EventId(); 67 | 68 | this.name = props.name; 69 | this.description = props.description; 70 | this.date = props.date; 71 | this.is_published = props.is_published; 72 | this.total_spots = props.total_spots; 73 | this.total_spots_reserved = props.total_spots_reserved; 74 | this.partner_id = 75 | props.partner_id instanceof PartnerId 76 | ? props.partner_id 77 | : new PartnerId(props.partner_id); 78 | this._sections = MyCollectionFactory.create(this); 79 | } 80 | 81 | static create(command: CreateEventCommand) { 82 | const event = new Event({ 83 | ...command, 84 | description: command.description ?? null, 85 | is_published: false, 86 | total_spots: 0, 87 | total_spots_reserved: 0, 88 | }); 89 | event.addEvent( 90 | new EventCreated( 91 | event.id, 92 | event.name, 93 | event.description, 94 | event.date, 95 | event.is_published, 96 | event.total_spots, 97 | event.total_spots_reserved, 98 | event.partner_id, 99 | ), 100 | ); 101 | return event; 102 | } 103 | 104 | changeName(name: string) { 105 | this.name = name; 106 | this.addEvent(new EventChangedName(this.id, this.name)); 107 | } 108 | 109 | changeDescription(description: string | null) { 110 | this.description = description; 111 | this.addEvent(new EventChangedDescription(this.id, this.description)); 112 | } 113 | 114 | changeDate(date: Date) { 115 | this.date = date; 116 | this.addEvent(new EventChangedDate(this.id, this.date)); 117 | } 118 | 119 | publishAll() { 120 | this.publish(); 121 | this._sections.forEach((section) => section.publishAll()); 122 | this.addEvent( 123 | new EventPublishAll( 124 | this.id, 125 | this._sections.map((s) => s.id), 126 | ), 127 | ); 128 | } 129 | 130 | publish() { 131 | this.is_published = true; 132 | this.addEvent(new EventPublish(this.id)); 133 | } 134 | 135 | unPublish() { 136 | this.is_published = false; 137 | this.addEvent(new EventUnpublish(this.id)); 138 | } 139 | 140 | addSection(command: AddSectionCommand) { 141 | const section = EventSection.create(command); 142 | this._sections.add(section); 143 | this.total_spots += section.total_spots; 144 | this.addEvent( 145 | new EventAddedSection( 146 | this.id, 147 | section.name, 148 | section.description, 149 | section.total_spots, 150 | section.price, 151 | this.total_spots, 152 | ), 153 | ); 154 | } 155 | 156 | changeSectionInformation(command: { 157 | section_id: EventSectionId; 158 | name?: string; 159 | description?: string | null; 160 | }) { 161 | const section = this.sections.find((section) => 162 | section.id.equals(command.section_id), 163 | ); 164 | if (!section) { 165 | throw new Error('Section not found'); 166 | } 167 | 'name' in command && section.changeName(command.name); 168 | 'description' in command && section.changeDescription(command.description); 169 | this.addEvent( 170 | new EventChangedSectionSection( 171 | this.id, 172 | section.id, 173 | section.name, 174 | section.description, 175 | ), 176 | ); 177 | } 178 | 179 | changeLocation(command: { 180 | section_id: EventSectionId; 181 | spot_id: EventSpotId; 182 | location: string; 183 | }) { 184 | const section = this.sections.find((section) => 185 | section.id.equals(command.section_id), 186 | ); 187 | if (!section) { 188 | throw new Error('Section not found'); 189 | } 190 | section.changeLocation(command); 191 | this.addEvent( 192 | new EventChangedSpotLocation( 193 | this.id, 194 | section.id, 195 | command.spot_id, 196 | command.location, 197 | ), 198 | ); 199 | } 200 | 201 | allowReserveSpot(data: { section_id: EventSectionId; spot_id: EventSpotId }) { 202 | if (!this.is_published) { 203 | return false; 204 | } 205 | 206 | const section = this.sections.find((s) => s.id.equals(data.section_id)); 207 | if (!section) { 208 | throw new Error('Section not found'); 209 | } 210 | 211 | return section.allowReserveSpot(data.spot_id); 212 | } 213 | 214 | markSpotAsReserved(command: { 215 | section_id: EventSectionId; 216 | spot_id: EventSpotId; 217 | }) { 218 | const section = this.sections.find((s) => s.id.equals(command.section_id)); 219 | 220 | if (!section) { 221 | throw new Error('Section not found'); 222 | } 223 | 224 | section.markSpotAsReserved(command.spot_id); 225 | this.addEvent( 226 | new EventMarkedSportAsReserved(this.id, section.id, command.spot_id), 227 | ); 228 | } 229 | 230 | get sections(): ICollection { 231 | return this._sections as ICollection; 232 | } 233 | 234 | set sections(sections: AnyCollection) { 235 | this._sections = MyCollectionFactory.createFrom(sections); 236 | } 237 | 238 | toJSON() { 239 | return { 240 | id: this.id.value, 241 | name: this.name, 242 | description: this.description, 243 | date: this.date, 244 | is_published: this.is_published, 245 | total_spots: this.total_spots, 246 | total_spots_reserved: this.total_spots_reserved, 247 | partner_id: this.partner_id.value, 248 | sections: [...this._sections].map((section) => section.toJSON()), 249 | }; 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /scorecard.md: -------------------------------------------------------------------------------- 1 | # Scorecard do DDD 2 | 3 | | Seu projeto tem uma pontuação total de 7 pontos ou mais? | | | 4 | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 5 | | Se seu projeto | Pontos | Concepções de suporte | 6 | | Se sua aplicação for completamente centrada em dados e se
qualificar verdadeiramente para uma solução CRUD pura,
em que cada operação é basicamente uma consulta simples
de banco de dados para criar, ler, atualizar ou excluir, você
não precisa do DDD. Sua equipe só precisa colocar um rosto
bonito em um editor de tabelas de banco de dados. Em outras
palavras, se você puder confiar no fato de que os usuários irão
inserir os dados diretamente em uma tabela, atualizá-los e, às
vezes, excluí-los, você nem mesmo precisará de uma interface
do usuário. Isso não é realista, mas é conceitualmente rele-
vante. Se pudesse usar uma ferramenta simples de desenvol-
vimento de banco de dados para criar uma solução, você não
desperdiçaria o tempo e dinheiro de sua empresa no DDD. | 0 | Isso parece óbvio, mas normalmente não é fácil determinar
simples versus complexo. Não é como se todas as aplicações
que não são CRUD puras merecem o tempo e o esforço
do uso do DDD. Assim, talvez possamos sugerir outros
indicadores para nos ajudar a traçar uma linha entre o que é
complexo e o que não é ... | 7 | | Se seu sistema exigir apenas 30 ou menos operações de
negócio, ele provavelmente é bem simples. Isso significaria
que a aplicação não teria um total de mais de 30 histórias de
usuário ou fluxos de caso de uso, com cada um desses fluxos
tendo apenas uma lógica mínima de negócio. Se você puder
desenvolver rápida e facilmente esse tipo de aplicação usando
o Ruby on Rails ou o Groovy and Grails e não se importar
com a falta de poder e controle em relação à complexidade e
alteração, o sistema provavelmente não precisará usar o DDD. | 1 | Para ser claro, estou falando de 25 a 30 únicos métodos
de negócio, não de 25 a 30 interfaces de serviço comple-
tas, cada uma com vários métodos. O último pode ser
complexo. | 8 | | Assim, digamos que, em algum lugar no intervalo entre 30 e
40 histórias de usuário ou fluxos de caso de uso, a comple-
xidade poderia ser pior. Seu sistema pode estar entrando no
território do DDD. | 2 | O risco é do comprador: Bem frequentemente a complexi-
dade não é reconhecida rapidamente. Nós, desenvolvedores
de software, somos realmente muito bons para subestimar
a complexidade e o nível de esforços. Só porque talvez
queiramos codificar uma aplicação no Rails ou Grails não
significa que devemos. No longo prazo, essas aplicações
poderiam prejudicar mais do que ajudar. | 9 | | Mesmo que a aplicação não seja complexa agora, a comp exi-
dade dela aumentará? Você só pode saber isso ao certo depois
que os usuários reais começam a trabalhar com ela, mas há
um passo na coluna "Pensamentos de suporte" que pode
ajudar a revelar a situação real.
Tenha cuidado aqui. Se houver absolutamente qualquer indício
de que a aplicação tem complexidade mesmo moderada
— este
é um bom momento para ser paranoico —, isso pode ser uma
indicação suficiente de que ela na verdade será mais do que
noderadamente complexa. Incline-se em direção ao DDD. | 3 | Aqui vale a pena analisar os cenários de uso mais complexos com especialistas em domínio e ver aonde eles levam.
Os especialistas em domínio ...
1... . já estão solicitando recursos mais complexos? Se sim,
isso provavelmente é uma indicação de que a aplicação já é
ou em breve se tornará excessivamente complexa para usar
uma abordagem CRUD.
2....estão entediado com os recursos ao ponto em que
dificilmente vale a pena discuti-los? Provavelmente não é
complexa | 10 | | Os recursos da aplicação serão alterados com requencla
longo de alguns anos, e você não pode antecipar que as altera-
cões serão simples. | 4 | O DDD pode ajudá-lo a gerenciar a complexi a e a refatoraçãode seu modelo ao longo do tempo. | 11 | | Você não entende o Domínio (2) porque ele é novo. a
medida em que você e sua equipe sabem, ninguém fez isso
antes. Isso provavelmente significa que ele é complexo ou,
pelo menos, merece a devida diligência com análise analítica
\*ra. determinar o nível de complexidade. | 5 | Você precisará trabalhar com especialistas em domínio e
testar os modelos para fazer a coisa certa. Você certamente
também pontuou em um ou mais dos critérios anteriores,
portanto, use o DDD. | 12 | --------------------------------------------------------------------------------