├── .gitignore ├── .swcrc ├── jest.config.ts ├── package-lock.json ├── package.json ├── src ├── domain │ ├── @shared │ │ ├── event │ │ │ ├── event-dispatcher.interface.ts │ │ │ ├── event-dispatcher.spec.ts │ │ │ ├── event-dispatcher.ts │ │ │ ├── event-handler.interface.ts │ │ │ └── event.interface.ts │ │ └── repository │ │ │ └── repository-interface.ts │ ├── checkout │ │ ├── entity │ │ │ ├── order.spec.ts │ │ │ ├── order.ts │ │ │ └── order_item.ts │ │ ├── factory │ │ │ ├── order.factory.spec.ts │ │ │ └── order.factory.ts │ │ ├── repository │ │ │ └── order-repository.interface.ts │ │ └── service │ │ │ ├── order.service.spec.ts │ │ │ └── order.service.ts │ ├── customer │ │ ├── entity │ │ │ ├── customer.spec.ts │ │ │ └── customer.ts │ │ ├── factory │ │ │ ├── customer.factory.spec.ts │ │ │ └── customer.factory.ts │ │ ├── repository │ │ │ └── customer-repository.interface.ts │ │ └── value-object │ │ │ └── address.ts │ └── product │ │ ├── entity │ │ ├── product-b.ts │ │ ├── product.interface.ts │ │ ├── product.spec.ts │ │ └── product.ts │ │ ├── event │ │ ├── handler │ │ │ └── send-email-when-product-is-created.handler.ts │ │ └── product-created.event.ts │ │ ├── factory │ │ ├── product.factory.spec.ts │ │ └── product.factory.ts │ │ ├── repository │ │ └── product-repository.interface.ts │ │ └── service │ │ ├── product.service.spec.ts │ │ └── product.service.ts └── infrastructure │ ├── customer │ └── repository │ │ └── sequelize │ │ ├── customer.model.ts │ │ ├── customer.repository.spec.ts │ │ └── customer.repository.ts │ ├── order │ └── repository │ │ └── sequilize │ │ ├── order-item.model.ts │ │ ├── order.model.ts │ │ ├── order.repository.spec.ts │ │ └── order.repository.ts │ └── product │ └── repository │ └── sequelize │ ├── product.model.ts │ ├── product.repository.spec.ts │ └── product.repository.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | .swc 4 | dist -------------------------------------------------------------------------------- /.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsc" : { 3 | "parser": { 4 | "syntax": "typescript", 5 | "decorators": true 6 | }, 7 | "transform": { 8 | "legacyDecorator": true, 9 | "decoratorMetadata": true 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property and type check, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | export default { 7 | transform: { 8 | "^.+\.(t|j)sx?$": ["@swc/jest"], 9 | 10 | }, 11 | // All imported modules in your tests should be mocked automatically 12 | // automock: false, 13 | 14 | // Stop running tests after `n` failures 15 | // bail: 0, 16 | 17 | // The directory where Jest should store its cached dependency information 18 | // cacheDirectory: "/private/var/folders/h_/n9cr86t52279b8msmjm777z00000gn/T/jest_dx", 19 | 20 | // Automatically clear mock calls, instances and results before every test 21 | clearMocks: true, 22 | 23 | // Indicates whether the coverage information should be collected while executing the test 24 | // collectCoverage: false, 25 | 26 | // An array of glob patterns indicating a set of files for which coverage information should be collected 27 | // collectCoverageFrom: undefined, 28 | 29 | // The directory where Jest should output its coverage files 30 | // coverageDirectory: undefined, 31 | 32 | // An array of regexp pattern strings used to skip coverage collection 33 | // coveragePathIgnorePatterns: [ 34 | // "/node_modules/" 35 | // ], 36 | 37 | // Indicates which provider should be used to instrument code for coverage 38 | coverageProvider: "v8", 39 | 40 | // A list of reporter names that Jest uses when writing coverage reports 41 | // coverageReporters: [ 42 | // "json", 43 | // "text", 44 | // "lcov", 45 | // "clover" 46 | // ], 47 | 48 | // An object that configures minimum threshold enforcement for coverage results 49 | // coverageThreshold: undefined, 50 | 51 | // A path to a custom dependency extractor 52 | // dependencyExtractor: undefined, 53 | 54 | // Make calling deprecated APIs throw helpful error messages 55 | // errorOnDeprecated: false, 56 | 57 | // Force coverage collection from ignored files using an array of glob patterns 58 | // forceCoverageMatch: [], 59 | 60 | // A path to a module which exports an async function that is triggered once before all test suites 61 | // globalSetup: undefined, 62 | 63 | // A path to a module which exports an async function that is triggered once after all test suites 64 | // globalTeardown: undefined, 65 | 66 | // A set of global variables that need to be available in all test environments 67 | // globals: {}, 68 | 69 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 70 | // maxWorkers: "50%", 71 | 72 | // An array of directory names to be searched recursively up from the requiring module's location 73 | // moduleDirectories: [ 74 | // "node_modules" 75 | // ], 76 | 77 | // An array of file extensions your modules use 78 | // moduleFileExtensions: [ 79 | // "js", 80 | // "jsx", 81 | // "ts", 82 | // "tsx", 83 | // "json", 84 | // "node" 85 | // ], 86 | 87 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 88 | // moduleNameMapper: {}, 89 | 90 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 91 | // modulePathIgnorePatterns: [], 92 | 93 | // Activates notifications for test results 94 | // notify: false, 95 | 96 | // An enum that specifies notification mode. Requires { notify: true } 97 | // notifyMode: "failure-change", 98 | 99 | // A preset that is used as a base for Jest's configuration 100 | // preset: undefined, 101 | 102 | // Run tests from one or more projects 103 | // projects: undefined, 104 | 105 | // Use this configuration option to add custom reporters to Jest 106 | // reporters: undefined, 107 | 108 | // Automatically reset mock state before every test 109 | // resetMocks: false, 110 | 111 | // Reset the module registry before running each individual test 112 | // resetModules: false, 113 | 114 | // A path to a custom resolver 115 | // resolver: undefined, 116 | 117 | // Automatically restore mock state and implementation before every test 118 | // restoreMocks: false, 119 | 120 | // The root directory that Jest should scan for tests and modules within 121 | // rootDir: undefined, 122 | 123 | // A list of paths to directories that Jest should use to search for files in 124 | // roots: [ 125 | // "" 126 | // ], 127 | 128 | // Allows you to use a custom runner instead of Jest's default test runner 129 | // runner: "jest-runner", 130 | 131 | // The paths to modules that run some code to configure or set up the testing environment before each test 132 | // setupFiles: [], 133 | 134 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 135 | // setupFilesAfterEnv: [], 136 | 137 | // The number of seconds after which a test is considered as slow and reported as such in the results. 138 | // slowTestThreshold: 5, 139 | 140 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 141 | // snapshotSerializers: [], 142 | 143 | // The test environment that will be used for testing 144 | // testEnvironment: "jest-environment-node", 145 | 146 | // Options that will be passed to the testEnvironment 147 | // testEnvironmentOptions: {}, 148 | 149 | // Adds a location field to test results 150 | // testLocationInResults: false, 151 | 152 | // The glob patterns Jest uses to detect test files 153 | // testMatch: [ 154 | // "**/__tests__/**/*.[jt]s?(x)", 155 | // "**/?(*.)+(spec|test).[tj]s?(x)" 156 | // ], 157 | 158 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 159 | // testPathIgnorePatterns: [ 160 | // "/node_modules/" 161 | // ], 162 | 163 | // The regexp pattern or array of patterns that Jest uses to detect test files 164 | // testRegex: [], 165 | 166 | // This option allows the use of a custom results processor 167 | // testResultsProcessor: undefined, 168 | 169 | // This option allows use of a custom test runner 170 | // testRunner: "jest-circus/runner", 171 | 172 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 173 | // testURL: "http://localhost", 174 | 175 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 176 | // timers: "real", 177 | 178 | // A map from regular expressions to paths to transformers 179 | // transform: undefined, 180 | 181 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 182 | // transformIgnorePatterns: [ 183 | // "/node_modules/", 184 | // "\\.pnp\\.[^\\/]+$" 185 | // ], 186 | 187 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 188 | // unmockedModulePathPatterns: undefined, 189 | 190 | // Indicates whether each individual test should be reported during the run 191 | // verbose: undefined, 192 | 193 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 194 | // watchPathIgnorePatterns: [], 195 | 196 | // Whether to use watchman for file crawling 197 | // watchman: true, 198 | }; 199 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@swc/cli": "^0.1.55", 4 | "@swc/core": "^1.2.148", 5 | "@swc/jest": "^0.2.20", 6 | "@types/jest": "^27.4.1", 7 | "jest": "^27.5.1", 8 | "ts-node": "^10.6.0", 9 | "tslint": "^6.1.3", 10 | "typescript": "^4.5.5" 11 | }, 12 | "scripts": { 13 | "test": "npm run tsc -- --noEmit && jest", 14 | "tsc": "tsc" 15 | }, 16 | "dependencies": { 17 | "@types/uuid": "^8.3.4", 18 | "reflect-metadata": "^0.1.13", 19 | "sequelize": "^6.17.0", 20 | "sequelize-typescript": "^2.1.3", 21 | "sqlite3": "^5.0.2", 22 | "uuid": "^8.3.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/domain/@shared/event/event-dispatcher.interface.ts: -------------------------------------------------------------------------------- 1 | import EventHandlerInterface from "./event-handler.interface"; 2 | import EventInterface from "./event.interface"; 3 | 4 | export default interface EventDispatcherInterface { 5 | notify(event: EventInterface): void; 6 | register(eventName: string, eventHandler: EventHandlerInterface): void; 7 | unregister(eventName: string, eventHandler: EventHandlerInterface): void; 8 | unregisterAll(): void; 9 | } 10 | -------------------------------------------------------------------------------- /src/domain/@shared/event/event-dispatcher.spec.ts: -------------------------------------------------------------------------------- 1 | import SendEmailWhenProductIsCreatedHandler from "../../product/event/handler/send-email-when-product-is-created.handler"; 2 | import ProductCreatedEvent from "../../product/event/product-created.event"; 3 | import EventDispatcher from "./event-dispatcher"; 4 | 5 | describe("Domain events tests", () => { 6 | it("should register an event handler", () => { 7 | const eventDispatcher = new EventDispatcher(); 8 | const eventHandler = new SendEmailWhenProductIsCreatedHandler(); 9 | 10 | eventDispatcher.register("ProductCreatedEvent", eventHandler); 11 | 12 | expect( 13 | eventDispatcher.getEventHandlers["ProductCreatedEvent"] 14 | ).toBeDefined(); 15 | expect(eventDispatcher.getEventHandlers["ProductCreatedEvent"].length).toBe( 16 | 1 17 | ); 18 | expect( 19 | eventDispatcher.getEventHandlers["ProductCreatedEvent"][0] 20 | ).toMatchObject(eventHandler); 21 | }); 22 | 23 | it("should unregister an event handler", () => { 24 | const eventDispatcher = new EventDispatcher(); 25 | const eventHandler = new SendEmailWhenProductIsCreatedHandler(); 26 | 27 | eventDispatcher.register("ProductCreatedEvent", eventHandler); 28 | 29 | expect( 30 | eventDispatcher.getEventHandlers["ProductCreatedEvent"][0] 31 | ).toMatchObject(eventHandler); 32 | 33 | eventDispatcher.unregister("ProductCreatedEvent", eventHandler); 34 | 35 | expect( 36 | eventDispatcher.getEventHandlers["ProductCreatedEvent"] 37 | ).toBeDefined(); 38 | expect(eventDispatcher.getEventHandlers["ProductCreatedEvent"].length).toBe( 39 | 0 40 | ); 41 | }); 42 | 43 | it("should unregister all event handlers", () => { 44 | const eventDispatcher = new EventDispatcher(); 45 | const eventHandler = new SendEmailWhenProductIsCreatedHandler(); 46 | 47 | eventDispatcher.register("ProductCreatedEvent", eventHandler); 48 | 49 | expect( 50 | eventDispatcher.getEventHandlers["ProductCreatedEvent"][0] 51 | ).toMatchObject(eventHandler); 52 | 53 | eventDispatcher.unregisterAll(); 54 | 55 | expect( 56 | eventDispatcher.getEventHandlers["ProductCreatedEvent"] 57 | ).toBeUndefined(); 58 | }); 59 | 60 | it("should notify all event handlers", () => { 61 | const eventDispatcher = new EventDispatcher(); 62 | const eventHandler = new SendEmailWhenProductIsCreatedHandler(); 63 | const spyEventHandler = jest.spyOn(eventHandler, "handle"); 64 | 65 | eventDispatcher.register("ProductCreatedEvent", eventHandler); 66 | 67 | expect( 68 | eventDispatcher.getEventHandlers["ProductCreatedEvent"][0] 69 | ).toMatchObject(eventHandler); 70 | 71 | const productCreatedEvent = new ProductCreatedEvent({ 72 | name: "Product 1", 73 | description: "Product 1 description", 74 | price: 10.0, 75 | }); 76 | 77 | // Quando o notify for executado o SendEmailWhenProductIsCreatedHandler.handle() deve ser chamado 78 | eventDispatcher.notify(productCreatedEvent); 79 | 80 | expect(spyEventHandler).toHaveBeenCalled(); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /src/domain/@shared/event/event-dispatcher.ts: -------------------------------------------------------------------------------- 1 | import EventDispatcherInterface from "./event-dispatcher.interface"; 2 | import EventHandlerInterface from "./event-handler.interface"; 3 | import eventInterface from "./event.interface"; 4 | 5 | export default class EventDispatcher implements EventDispatcherInterface { 6 | private eventHandlers: { [eventName: string]: EventHandlerInterface[] } = {}; 7 | 8 | get getEventHandlers(): { [eventName: string]: EventHandlerInterface[] } { 9 | return this.eventHandlers; 10 | } 11 | 12 | register(eventName: string, eventHandler: EventHandlerInterface): void { 13 | if (!this.eventHandlers[eventName]) { 14 | this.eventHandlers[eventName] = []; 15 | } 16 | this.eventHandlers[eventName].push(eventHandler); 17 | } 18 | 19 | unregister(eventName: string, eventHandler: EventHandlerInterface): void { 20 | if (this.eventHandlers[eventName]) { 21 | const index = this.eventHandlers[eventName].indexOf(eventHandler); 22 | if (index !== -1) { 23 | this.eventHandlers[eventName].splice(index, 1); 24 | } 25 | } 26 | } 27 | 28 | unregisterAll(): void { 29 | this.eventHandlers = {}; 30 | } 31 | 32 | notify(event: eventInterface): void { 33 | const eventName = event.constructor.name; 34 | if (this.eventHandlers[eventName]) { 35 | this.eventHandlers[eventName].forEach((eventHandler) => { 36 | eventHandler.handle(event); 37 | }); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/domain/@shared/event/event-handler.interface.ts: -------------------------------------------------------------------------------- 1 | import EventInterface from './event.interface'; 2 | export default interface EventHandlerInterface { 3 | handle(event: T): void; 4 | } -------------------------------------------------------------------------------- /src/domain/@shared/event/event.interface.ts: -------------------------------------------------------------------------------- 1 | export default interface EventInterface { 2 | dataTimeOccurred: Date; 3 | eventData: any; 4 | } 5 | -------------------------------------------------------------------------------- /src/domain/@shared/repository/repository-interface.ts: -------------------------------------------------------------------------------- 1 | export default interface RepositoryInterface { 2 | create(entity: T): Promise; 3 | update(entity: T): Promise; 4 | find(id: string): Promise; 5 | findAll(): Promise; 6 | } -------------------------------------------------------------------------------- /src/domain/checkout/entity/order.spec.ts: -------------------------------------------------------------------------------- 1 | import Order from "./order"; 2 | import OrderItem from "./order_item"; 3 | 4 | describe("Order unit tests", () => { 5 | it("should throw error when id is empty", () => { 6 | expect(() => { 7 | let order = new Order("", "123", []); 8 | }).toThrowError("Id is required"); 9 | }); 10 | 11 | it("should throw error when customerId is empty", () => { 12 | expect(() => { 13 | let order = new Order("123", "", []); 14 | }).toThrowError("CustomerId is required"); 15 | }); 16 | 17 | it("should throw error when items is empty", () => { 18 | expect(() => { 19 | let order = new Order("123", "123", []); 20 | }).toThrowError("Items are required"); 21 | }); 22 | 23 | it("should calculate total", () => { 24 | const item = new OrderItem("i1", "Item 1", 100, "p1", 2); 25 | const item2 = new OrderItem("i2", "Item 2", 200, "p2", 2); 26 | const order = new Order("o1", "c1", [item]); 27 | 28 | let total = order.total(); 29 | 30 | expect(order.total()).toBe(200); 31 | 32 | const order2 = new Order("o1", "c1", [item, item2]); 33 | total = order2.total(); 34 | expect(total).toBe(600); 35 | }); 36 | 37 | it("should throw error if the item qte is less or equal zero 0", () => { 38 | expect(() => { 39 | const item = new OrderItem("i1", "Item 1", 100, "p1", 0); 40 | const order = new Order("o1", "c1", [item]); 41 | }).toThrowError("Quantity must be greater than 0"); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/domain/checkout/entity/order.ts: -------------------------------------------------------------------------------- 1 | import OrderItem from "./order_item"; 2 | export default class Order { 3 | private _id: string; 4 | private _customerId: string; 5 | private _items: OrderItem[]; 6 | private _total: number; 7 | 8 | constructor(id: string, customerId: string, items: OrderItem[]) { 9 | this._id = id; 10 | this._customerId = customerId; 11 | this._items = items; 12 | this._total = this.total(); 13 | this.validate(); 14 | } 15 | 16 | get id(): string { 17 | return this._id; 18 | } 19 | 20 | get customerId(): string { 21 | return this._customerId; 22 | } 23 | 24 | get items(): OrderItem[] { 25 | return this._items; 26 | } 27 | 28 | validate(): boolean { 29 | if (this._id.length === 0) { 30 | throw new Error("Id is required"); 31 | } 32 | if (this._customerId.length === 0) { 33 | throw new Error("CustomerId is required"); 34 | } 35 | if (this._items.length === 0) { 36 | throw new Error("Items are required"); 37 | } 38 | 39 | if (this._items.some((item) => item.quantity <= 0)) { 40 | throw new Error("Quantity must be greater than 0"); 41 | } 42 | 43 | return true; 44 | } 45 | 46 | total(): number { 47 | return this._items.reduce((acc, item) => acc + item.total(), 0); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/domain/checkout/entity/order_item.ts: -------------------------------------------------------------------------------- 1 | export default class OrderItem { 2 | private _id: string; 3 | private _productId: string; 4 | private _name: string; 5 | private _price: number; 6 | private _quantity: number; 7 | private _total: number; 8 | 9 | constructor( 10 | id: string, 11 | name: string, 12 | price: number, 13 | productId: string, 14 | quantity: number 15 | ) { 16 | this._id = id; 17 | this._name = name; 18 | this._price = price; 19 | this._productId = productId; 20 | this._quantity = quantity; 21 | this._total = this.total(); 22 | } 23 | 24 | get id(): string { 25 | return this._id; 26 | } 27 | 28 | get name(): string { 29 | return this._name; 30 | } 31 | 32 | get productId(): string { 33 | return this._productId; 34 | } 35 | 36 | get quantity(): number { 37 | return this._quantity; 38 | } 39 | 40 | get price(): number { 41 | return this._price; 42 | } 43 | 44 | total(): number { 45 | return this._price * this._quantity 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/domain/checkout/factory/order.factory.spec.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuid } from "uuid"; 2 | import OrderFactory from "./order.factory"; 3 | 4 | describe("Order factory unit test", () => { 5 | it("should create an order", () => { 6 | const orderProps = { 7 | id: uuid(), 8 | customerId: uuid(), 9 | items: [ 10 | { 11 | id: uuid(), 12 | name: "Product 1", 13 | productId: uuid(), 14 | quantity: 1, 15 | price: 100, 16 | }, 17 | ], 18 | }; 19 | 20 | const order = OrderFactory.create(orderProps); 21 | 22 | expect(order.id).toEqual(orderProps.id); 23 | expect(order.customerId).toEqual(orderProps.customerId); 24 | expect(order.items.length).toBe(1); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/domain/checkout/factory/order.factory.ts: -------------------------------------------------------------------------------- 1 | import Order from "../entity/order"; 2 | import OrderItem from "../entity/order_item"; 3 | 4 | interface OrderFactoryProps { 5 | id: string; 6 | customerId: string; 7 | items: { 8 | id: string; 9 | name: string; 10 | productId: string; 11 | quantity: number; 12 | price: number; 13 | }[]; 14 | } 15 | 16 | export default class OrderFactory { 17 | public static create(props: OrderFactoryProps): Order { 18 | const items = props.items.map((item) => { 19 | return new OrderItem( 20 | item.id, 21 | item.name, 22 | item.price, 23 | item.productId, 24 | item.quantity 25 | ); 26 | }); 27 | 28 | return new Order(props.id, props.customerId, items); 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/domain/checkout/repository/order-repository.interface.ts: -------------------------------------------------------------------------------- 1 | import RepositoryInterface from "../../@shared/repository/repository-interface"; 2 | import Order from "../entity/order"; 3 | 4 | export default interface OrderRepositoryInterface extends RepositoryInterface {} -------------------------------------------------------------------------------- /src/domain/checkout/service/order.service.spec.ts: -------------------------------------------------------------------------------- 1 | import Customer from "../../customer/entity/customer"; 2 | import Order from "../entity/order"; 3 | import OrderItem from "../entity/order_item"; 4 | import OrderService from "./order.service"; 5 | describe("Order service unit tets", () => { 6 | it("should place an order", () => { 7 | const customer = new Customer("c1", "Customer 1"); 8 | const item1 = new OrderItem("i1", "Item 1", 10, "p1", 1); 9 | 10 | const order = OrderService.placeOrder(customer, [item1]); 11 | 12 | expect(customer.rewardPoints).toBe(5); 13 | expect(order.total()).toBe(10); 14 | }); 15 | 16 | it("should get total of all orders", () => { 17 | const item1 = new OrderItem("i1", "Item 1", 100, "p1", 1); 18 | const item2 = new OrderItem("i2", "Item 2", 200, "p2", 2); 19 | 20 | const order = new Order("o1", "c1", [item1]); 21 | const order2 = new Order("o2", "c1", [item2]); 22 | 23 | const total = OrderService.total([order, order2]); 24 | 25 | expect(total).toBe(500); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/domain/checkout/service/order.service.ts: -------------------------------------------------------------------------------- 1 | import Order from "../entity/order"; 2 | import OrderItem from "../entity/order_item"; 3 | import { v4 as uuid } from "uuid"; 4 | import Customer from "../../customer/entity/customer"; 5 | 6 | export default class OrderService { 7 | static placeOrder(customer: Customer, items: OrderItem[]): Order { 8 | if (items.length === 0) { 9 | throw new Error("Order must have at least one item"); 10 | } 11 | const order = new Order(uuid(), customer.id, items); 12 | customer.addRewardPoints(order.total() / 2); 13 | return order; 14 | } 15 | 16 | static total(orders: Order[]): number { 17 | return orders.reduce((acc, order) => acc + order.total(), 0); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/domain/customer/entity/customer.spec.ts: -------------------------------------------------------------------------------- 1 | import Address from "../value-object/address"; 2 | import Customer from "./customer"; 3 | 4 | describe("Customer unit tests", () => { 5 | it("should throw error when id is empty", () => { 6 | expect(() => { 7 | let customer = new Customer("", "John"); 8 | }).toThrowError("Id is required"); 9 | }); 10 | 11 | it("should throw error when name is empty", () => { 12 | expect(() => { 13 | let customer = new Customer("123", ""); 14 | }).toThrowError("Name is required"); 15 | }); 16 | 17 | it("should change name", () => { 18 | // Arrange 19 | const customer = new Customer("123", "John"); 20 | 21 | // Act 22 | customer.changeName("Jane"); 23 | 24 | // Assert 25 | expect(customer.name).toBe("Jane"); 26 | }); 27 | 28 | it("should activate customer", () => { 29 | const customer = new Customer("1", "Customer 1"); 30 | const address = new Address("Street 1", 123, "13330-250", "São Paulo"); 31 | customer.Address = address; 32 | 33 | customer.activate(); 34 | 35 | expect(customer.isActive()).toBe(true); 36 | }); 37 | 38 | it("should throw error when address is undefined when you activate a customer", () => { 39 | expect(() => { 40 | const customer = new Customer("1", "Customer 1"); 41 | customer.activate(); 42 | }).toThrowError("Address is mandatory to activate a customer"); 43 | }); 44 | 45 | it("should deactivate customer", () => { 46 | const customer = new Customer("1", "Customer 1"); 47 | 48 | customer.deactivate(); 49 | 50 | expect(customer.isActive()).toBe(false); 51 | }); 52 | 53 | it("should add reward points", () => { 54 | const customer = new Customer("1", "Customer 1"); 55 | expect(customer.rewardPoints).toBe(0); 56 | 57 | customer.addRewardPoints(10); 58 | expect(customer.rewardPoints).toBe(10); 59 | 60 | customer.addRewardPoints(10); 61 | expect(customer.rewardPoints).toBe(20); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /src/domain/customer/entity/customer.ts: -------------------------------------------------------------------------------- 1 | import Address from "../value-object/address"; 2 | 3 | export default class Customer { 4 | private _id: string; 5 | private _name: string = ""; 6 | private _address!: Address; 7 | private _active: boolean = false; 8 | private _rewardPoints: number = 0; 9 | 10 | constructor(id: string, name: string) { 11 | this._id = id; 12 | this._name = name; 13 | this.validate(); 14 | } 15 | 16 | get id(): string { 17 | return this._id; 18 | } 19 | 20 | get name(): string { 21 | return this._name; 22 | } 23 | 24 | get rewardPoints(): number { 25 | return this._rewardPoints; 26 | } 27 | 28 | validate() { 29 | if (this._id.length === 0) { 30 | throw new Error("Id is required"); 31 | } 32 | if (this._name.length === 0) { 33 | throw new Error("Name is required"); 34 | } 35 | } 36 | 37 | changeName(name: string) { 38 | this._name = name; 39 | this.validate(); 40 | } 41 | 42 | get Address(): Address { 43 | return this._address; 44 | } 45 | 46 | changeAddress(address: Address) { 47 | this._address = address; 48 | } 49 | 50 | isActive(): boolean { 51 | return this._active; 52 | } 53 | 54 | activate() { 55 | if (this._address === undefined) { 56 | throw new Error("Address is mandatory to activate a customer"); 57 | } 58 | this._active = true; 59 | } 60 | 61 | deactivate() { 62 | this._active = false; 63 | } 64 | 65 | addRewardPoints(points: number) { 66 | this._rewardPoints += points; 67 | } 68 | 69 | set Address(address: Address) { 70 | this._address = address; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/domain/customer/factory/customer.factory.spec.ts: -------------------------------------------------------------------------------- 1 | import CustomerFactory from "./customer.factory"; 2 | import Address from "../value-object/address"; 3 | 4 | describe("Customer factory unit test", () => { 5 | it("should create a customer", () => { 6 | let customer = CustomerFactory.create("John"); 7 | 8 | expect(customer.id).toBeDefined(); 9 | expect(customer.name).toBe("John"); 10 | expect(customer.Address).toBeUndefined(); 11 | }); 12 | 13 | it("should create a customer with an address", () => { 14 | const address = new Address("Street", 1, "13330-250", "São Paulo"); 15 | 16 | let customer = CustomerFactory.createWithAddress("John", address); 17 | 18 | expect(customer.id).toBeDefined(); 19 | expect(customer.name).toBe("John"); 20 | expect(customer.Address).toBe(address); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/domain/customer/factory/customer.factory.ts: -------------------------------------------------------------------------------- 1 | import Customer from "../entity/customer"; 2 | import { v4 as uuid } from "uuid"; 3 | import Address from "../value-object/address"; 4 | 5 | export default class CustomerFactory { 6 | public static create(name: string): Customer { 7 | return new Customer(uuid(), name); 8 | } 9 | 10 | public static createWithAddress(name: string, address: Address): Customer { 11 | const customer = new Customer(uuid(), name); 12 | customer.changeAddress(address); 13 | return customer; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/domain/customer/repository/customer-repository.interface.ts: -------------------------------------------------------------------------------- 1 | import RepositoryInterface from "../../@shared/repository/repository-interface"; 2 | import Customer from "../entity/customer"; 3 | 4 | export default interface CustomerRepositoryInterface 5 | extends RepositoryInterface {} 6 | -------------------------------------------------------------------------------- /src/domain/customer/value-object/address.ts: -------------------------------------------------------------------------------- 1 | export default class Address { 2 | _street: string = ""; 3 | _number: number = 0; 4 | _zip: string = ""; 5 | _city: string = ""; 6 | 7 | constructor(street: string, number: number, zip: string, city: string) { 8 | this._street = street; 9 | this._number = number; 10 | this._zip = zip; 11 | this._city = city; 12 | 13 | this.validate(); 14 | } 15 | 16 | get street(): string { 17 | return this._street; 18 | } 19 | 20 | get number(): number { 21 | return this._number; 22 | } 23 | 24 | get zip(): string { 25 | return this._zip; 26 | } 27 | 28 | get city(): string { 29 | return this._city; 30 | } 31 | 32 | validate() { 33 | if (this._street.length === 0) { 34 | throw new Error("Street is required"); 35 | } 36 | if (this._number === 0) { 37 | throw new Error("Number is required"); 38 | } 39 | if (this._zip.length === 0) { 40 | throw new Error("Zip is required"); 41 | } 42 | if (this._city.length === 0) { 43 | throw new Error("City is required"); 44 | } 45 | } 46 | 47 | toString() { 48 | return `${this._street}, ${this._number}, ${this._zip} ${this._city}`; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/domain/product/entity/product-b.ts: -------------------------------------------------------------------------------- 1 | import ProductInterface from "./product.interface"; 2 | 3 | export default class ProductB implements ProductInterface { 4 | private _id: string; 5 | private _name: string; 6 | private _price: number; 7 | 8 | constructor(id: string, name: string, price: number) { 9 | this._id = id; 10 | this._name = name; 11 | this._price = price; 12 | this.validate(); 13 | } 14 | 15 | get id(): string { 16 | return this._id; 17 | } 18 | 19 | get name(): string { 20 | return this._name; 21 | } 22 | 23 | get price(): number { 24 | return this._price * 2; 25 | } 26 | 27 | changeName(name: string): void { 28 | this._name = name; 29 | this.validate(); 30 | } 31 | 32 | changePrice(price: number): void { 33 | this._price = price; 34 | this.validate(); 35 | } 36 | 37 | validate(): boolean { 38 | if (this._id.length === 0) { 39 | throw new Error("Id is required"); 40 | } 41 | if (this._name.length === 0) { 42 | throw new Error("Name is required"); 43 | } 44 | if (this._price < 0) { 45 | throw new Error("Price must be greater than zero"); 46 | } 47 | return true; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/domain/product/entity/product.interface.ts: -------------------------------------------------------------------------------- 1 | export default interface ProductInterface { 2 | get id(): string; 3 | get name(): string; 4 | get price(): number; 5 | } 6 | -------------------------------------------------------------------------------- /src/domain/product/entity/product.spec.ts: -------------------------------------------------------------------------------- 1 | import Product from "./product"; 2 | 3 | describe("Product unit tests", () => { 4 | it("should throw error when id is empty", () => { 5 | expect(() => { 6 | const product = new Product("", "Product 1", 100); 7 | }).toThrowError("Id is required"); 8 | }); 9 | 10 | it("should throw error when name is empty", () => { 11 | expect(() => { 12 | const product = new Product("123", "", 100); 13 | }).toThrowError("Name is required"); 14 | }); 15 | 16 | it("should throw error when price is less than zero", () => { 17 | expect(() => { 18 | const product = new Product("123", "Name", -1); 19 | }).toThrowError("Price must be greater than zero"); 20 | }); 21 | 22 | it("should change name", () => { 23 | const product = new Product("123", "Product 1", 100); 24 | product.changeName("Product 2"); 25 | expect(product.name).toBe("Product 2"); 26 | }); 27 | 28 | it("should change price", () => { 29 | const product = new Product("123", "Product 1", 100); 30 | product.changePrice(150); 31 | expect(product.price).toBe(150); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/domain/product/entity/product.ts: -------------------------------------------------------------------------------- 1 | import ProductInterface from "./product.interface"; 2 | 3 | export default class Product implements ProductInterface { 4 | private _id: string; 5 | private _name: string; 6 | private _price: number; 7 | 8 | constructor(id: string, name: string, price: number) { 9 | this._id = id; 10 | this._name = name; 11 | this._price = price; 12 | this.validate(); 13 | } 14 | 15 | get id(): string { 16 | return this._id; 17 | } 18 | 19 | get name(): string { 20 | return this._name; 21 | } 22 | 23 | get price(): number { 24 | return this._price; 25 | } 26 | 27 | changeName(name: string): void { 28 | this._name = name; 29 | this.validate(); 30 | } 31 | 32 | changePrice(price: number): void { 33 | this._price = price; 34 | this.validate(); 35 | } 36 | 37 | validate(): boolean { 38 | if (this._id.length === 0) { 39 | throw new Error("Id is required"); 40 | } 41 | if (this._name.length === 0) { 42 | throw new Error("Name is required"); 43 | } 44 | if (this._price < 0) { 45 | throw new Error("Price must be greater than zero"); 46 | } 47 | return true; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/domain/product/event/handler/send-email-when-product-is-created.handler.ts: -------------------------------------------------------------------------------- 1 | import EventHandlerInterface from "../../../@shared/event/event-handler.interface"; 2 | import ProductCreatedEvent from "../product-created.event"; 3 | 4 | export default class SendEmailWhenProductIsCreatedHandler 5 | implements EventHandlerInterface 6 | { 7 | handle(event: ProductCreatedEvent): void { 8 | console.log(`Sending email to .....`); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/domain/product/event/product-created.event.ts: -------------------------------------------------------------------------------- 1 | import EventInterface from "../../@shared/event/event.interface"; 2 | 3 | export default class ProductCreatedEvent implements EventInterface { 4 | dataTimeOccurred: Date; 5 | eventData: any; 6 | 7 | constructor(eventData: any) { 8 | this.dataTimeOccurred = new Date(); 9 | this.eventData = eventData; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/domain/product/factory/product.factory.spec.ts: -------------------------------------------------------------------------------- 1 | import ProductFactory from "./product.factory"; 2 | 3 | describe("Product factory unit test", () => { 4 | it("should create a proct type a", () => { 5 | const product = ProductFactory.create("a", "Product A", 1); 6 | 7 | expect(product.id).toBeDefined(); 8 | expect(product.name).toBe("Product A"); 9 | expect(product.price).toBe(1); 10 | expect(product.constructor.name).toBe("Product"); 11 | }); 12 | 13 | it("should create a proct type b", () => { 14 | const product = ProductFactory.create("b", "Product B", 1); 15 | 16 | expect(product.id).toBeDefined(); 17 | expect(product.name).toBe("Product B"); 18 | expect(product.price).toBe(2); 19 | expect(product.constructor.name).toBe("ProductB"); 20 | }); 21 | 22 | it("should throw an error when product type is not supported", () => { 23 | expect(() => ProductFactory.create("c", "Product C", 1)).toThrowError( 24 | "Product type not supported" 25 | ); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/domain/product/factory/product.factory.ts: -------------------------------------------------------------------------------- 1 | import Product from "../entity/product"; 2 | import ProductInterface from "../entity/product.interface"; 3 | import { v4 as uuid } from "uuid"; 4 | import ProductB from "../entity/product-b"; 5 | 6 | export default class ProductFactory { 7 | public static create( 8 | type: string, 9 | name: string, 10 | price: number 11 | ): ProductInterface { 12 | switch (type) { 13 | case "a": 14 | return new Product(uuid(), name, price); 15 | case "b": 16 | return new ProductB(uuid(), name, price); 17 | default: 18 | throw new Error("Product type not supported"); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/domain/product/repository/product-repository.interface.ts: -------------------------------------------------------------------------------- 1 | import RepositoryInterface from "../../@shared/repository/repository-interface"; 2 | import Product from "../entity/product"; 3 | 4 | export default interface ProductRepositoryInterface 5 | extends RepositoryInterface {} 6 | -------------------------------------------------------------------------------- /src/domain/product/service/product.service.spec.ts: -------------------------------------------------------------------------------- 1 | import Product from "../entity/product"; 2 | import ProductService from "./product.service"; 3 | 4 | describe("Product service unit tests", () => { 5 | it("should change the prices of all products", () => { 6 | const product1 = new Product("product1", "Product 1", 10); 7 | const product2 = new Product("product2", "Product 2", 20); 8 | const products = [product1, product2]; 9 | 10 | ProductService.increasePrice(products, 100); 11 | 12 | expect(product1.price).toBe(20); 13 | expect(product2.price).toBe(40); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/domain/product/service/product.service.ts: -------------------------------------------------------------------------------- 1 | import Product from "../entity/product"; 2 | 3 | export default class ProductService { 4 | static increasePrice(products: Product[], percentage: number): Product[] { 5 | products.forEach((product) => { 6 | product.changePrice((product.price * percentage) / 100 + product.price); 7 | }); 8 | return products; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/infrastructure/customer/repository/sequelize/customer.model.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Model, 4 | PrimaryKey, 5 | Column 6 | } from "sequelize-typescript"; 7 | 8 | @Table({ 9 | tableName: "customers", 10 | timestamps: false, 11 | }) 12 | export default class CustomerModel extends Model { 13 | @PrimaryKey 14 | @Column 15 | declare id: string; 16 | 17 | @Column({ allowNull: false }) 18 | declare name: string; 19 | 20 | @Column({ allowNull: false }) 21 | declare street: string; 22 | 23 | @Column({ allowNull: false }) 24 | declare number: number; 25 | 26 | @Column({ allowNull: false }) 27 | declare zipcode: string; 28 | 29 | @Column({ allowNull: false }) 30 | declare city: string; 31 | 32 | @Column({ allowNull: false }) 33 | declare active: boolean; 34 | 35 | @Column({ allowNull: false }) 36 | declare rewardPoints: number; 37 | } -------------------------------------------------------------------------------- /src/infrastructure/customer/repository/sequelize/customer.repository.spec.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize } from "sequelize-typescript"; 2 | import Customer from "../../../../domain/customer/entity/customer"; 3 | import Address from "../../../../domain/customer/value-object/address"; 4 | import CustomerModel from "./customer.model"; 5 | import CustomerRepository from "./customer.repository"; 6 | 7 | describe("Customer repository test", () => { 8 | let sequelize: Sequelize; 9 | 10 | beforeEach(async () => { 11 | sequelize = new Sequelize({ 12 | dialect: "sqlite", 13 | storage: ":memory:", 14 | logging: false, 15 | sync: { force: true }, 16 | }); 17 | 18 | await sequelize.addModels([CustomerModel]); 19 | await sequelize.sync(); 20 | }); 21 | 22 | afterEach(async () => { 23 | await sequelize.close(); 24 | }); 25 | 26 | it("should create a customer", async () => { 27 | const customerRepository = new CustomerRepository(); 28 | const customer = new Customer("123", "Customer 1"); 29 | const address = new Address("Street 1", 1, "Zipcode 1", "City 1"); 30 | customer.Address = address; 31 | await customerRepository.create(customer); 32 | 33 | const customerModel = await CustomerModel.findOne({ where: { id: "123" } }); 34 | 35 | expect(customerModel.toJSON()).toStrictEqual({ 36 | id: "123", 37 | name: customer.name, 38 | active: customer.isActive(), 39 | rewardPoints: customer.rewardPoints, 40 | street: address.street, 41 | number: address.number, 42 | zipcode: address.zip, 43 | city: address.city, 44 | }); 45 | }); 46 | 47 | it("should update a customer", async () => { 48 | const customerRepository = new CustomerRepository(); 49 | const customer = new Customer("123", "Customer 1"); 50 | const address = new Address("Street 1", 1, "Zipcode 1", "City 1"); 51 | customer.Address = address; 52 | await customerRepository.create(customer); 53 | 54 | customer.changeName("Customer 2"); 55 | await customerRepository.update(customer); 56 | const customerModel = await CustomerModel.findOne({ where: { id: "123" } }); 57 | 58 | expect(customerModel.toJSON()).toStrictEqual({ 59 | id: "123", 60 | name: customer.name, 61 | active: customer.isActive(), 62 | rewardPoints: customer.rewardPoints, 63 | street: address.street, 64 | number: address.number, 65 | zipcode: address.zip, 66 | city: address.city, 67 | }); 68 | }); 69 | 70 | it("should find a customer", async () => { 71 | const customerRepository = new CustomerRepository(); 72 | const customer = new Customer("123", "Customer 1"); 73 | const address = new Address("Street 1", 1, "Zipcode 1", "City 1"); 74 | customer.Address = address; 75 | await customerRepository.create(customer); 76 | 77 | const customerResult = await customerRepository.find(customer.id); 78 | 79 | expect(customer).toStrictEqual(customerResult); 80 | }); 81 | 82 | it("should throw an error when customer is not found", async () => { 83 | const customerRepository = new CustomerRepository(); 84 | 85 | expect(async () => { 86 | await customerRepository.find("456ABC"); 87 | }).rejects.toThrow("Customer not found"); 88 | }); 89 | 90 | it("should find all customers", async () => { 91 | const customerRepository = new CustomerRepository(); 92 | const customer1 = new Customer("123", "Customer 1"); 93 | const address1 = new Address("Street 1", 1, "Zipcode 1", "City 1"); 94 | customer1.Address = address1; 95 | customer1.addRewardPoints(10); 96 | customer1.activate(); 97 | 98 | const customer2 = new Customer("456", "Customer 2"); 99 | const address2 = new Address("Street 2", 2, "Zipcode 2", "City 2"); 100 | customer2.Address = address2; 101 | customer2.addRewardPoints(20); 102 | 103 | await customerRepository.create(customer1); 104 | await customerRepository.create(customer2); 105 | 106 | const customers = await customerRepository.findAll(); 107 | 108 | expect(customers).toHaveLength(2); 109 | expect(customers).toContainEqual(customer1); 110 | expect(customers).toContainEqual(customer2); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /src/infrastructure/customer/repository/sequelize/customer.repository.ts: -------------------------------------------------------------------------------- 1 | import Customer from "../../../../domain/customer/entity/customer"; 2 | import Address from "../../../../domain/customer/value-object/address"; 3 | import CustomerRepositoryInterface from "../../../../domain/customer/repository/customer-repository.interface"; 4 | import CustomerModel from "./customer.model"; 5 | 6 | export default class CustomerRepository implements CustomerRepositoryInterface { 7 | async create(entity: Customer): Promise { 8 | await CustomerModel.create({ 9 | id: entity.id, 10 | name: entity.name, 11 | street: entity.Address.street, 12 | number: entity.Address.number, 13 | zipcode: entity.Address.zip, 14 | city: entity.Address.city, 15 | active: entity.isActive(), 16 | rewardPoints: entity.rewardPoints, 17 | }); 18 | } 19 | 20 | async update(entity: Customer): Promise { 21 | await CustomerModel.update( 22 | { 23 | name: entity.name, 24 | street: entity.Address.street, 25 | number: entity.Address.number, 26 | zipcode: entity.Address.zip, 27 | city: entity.Address.city, 28 | active: entity.isActive(), 29 | rewardPoints: entity.rewardPoints, 30 | }, 31 | { 32 | where: { 33 | id: entity.id, 34 | }, 35 | } 36 | ); 37 | } 38 | 39 | async find(id: string): Promise { 40 | let customerModel; 41 | try { 42 | customerModel = await CustomerModel.findOne({ 43 | where: { 44 | id, 45 | }, 46 | rejectOnEmpty: true, 47 | }); 48 | } catch (error) { 49 | throw new Error("Customer not found"); 50 | } 51 | 52 | const customer = new Customer(id, customerModel.name); 53 | const address = new Address( 54 | customerModel.street, 55 | customerModel.number, 56 | customerModel.zipcode, 57 | customerModel.city 58 | ); 59 | customer.changeAddress(address); 60 | return customer; 61 | } 62 | 63 | async findAll(): Promise { 64 | const customerModels = await CustomerModel.findAll(); 65 | 66 | const customers = customerModels.map((customerModels) => { 67 | let customer = new Customer(customerModels.id, customerModels.name); 68 | customer.addRewardPoints(customerModels.rewardPoints); 69 | const address = new Address( 70 | customerModels.street, 71 | customerModels.number, 72 | customerModels.zipcode, 73 | customerModels.city 74 | ); 75 | customer.changeAddress(address); 76 | if (customerModels.active) { 77 | customer.activate(); 78 | } 79 | return customer; 80 | }); 81 | 82 | return customers; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/infrastructure/order/repository/sequilize/order-item.model.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Model, 4 | PrimaryKey, 5 | Column, 6 | ForeignKey, 7 | BelongsTo, 8 | } from "sequelize-typescript"; 9 | import ProductModel from "../../../product/repository/sequelize/product.model"; 10 | import OrderModel from "./order.model"; 11 | 12 | 13 | @Table({ 14 | tableName: "order_items", 15 | timestamps: false, 16 | }) 17 | export default class OrderItemModel extends Model { 18 | @PrimaryKey 19 | @Column 20 | declare id: string; 21 | 22 | @ForeignKey(() => ProductModel) 23 | @Column({ allowNull: false }) 24 | declare product_id: string; 25 | 26 | @BelongsTo(() => ProductModel) 27 | declare product: ProductModel; 28 | 29 | @ForeignKey(() => OrderModel) 30 | @Column({ allowNull: false }) 31 | declare order_id: string; 32 | 33 | @BelongsTo(() => OrderModel) 34 | declare order: OrderModel; 35 | 36 | @Column({ allowNull: false }) 37 | declare quantity: number; 38 | 39 | @Column({ allowNull: false }) 40 | declare name: string; 41 | 42 | @Column({ allowNull: false }) 43 | declare price: number; 44 | } 45 | -------------------------------------------------------------------------------- /src/infrastructure/order/repository/sequilize/order.model.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Model, 4 | PrimaryKey, 5 | Column, 6 | ForeignKey, 7 | BelongsTo, 8 | HasMany, 9 | } from "sequelize-typescript"; 10 | import CustomerModel from "../../../customer/repository/sequelize/customer.model"; 11 | import OrderItemModel from "./order-item.model"; 12 | 13 | @Table({ 14 | tableName: "orders", 15 | timestamps: false, 16 | }) 17 | export default class OrderModel extends Model { 18 | @PrimaryKey 19 | @Column 20 | declare id: string; 21 | 22 | @ForeignKey(() => CustomerModel) 23 | @Column({ allowNull: false }) 24 | declare customer_id: string; 25 | 26 | @BelongsTo(() => CustomerModel) 27 | declare customer: CustomerModel; 28 | 29 | @HasMany(() => OrderItemModel) 30 | declare items: OrderItemModel[]; 31 | 32 | @Column({ allowNull: false }) 33 | declare total: number; 34 | } 35 | -------------------------------------------------------------------------------- /src/infrastructure/order/repository/sequilize/order.repository.spec.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize } from "sequelize-typescript"; 2 | import Order from "../../../../domain/checkout/entity/order"; 3 | import OrderItem from "../../../../domain/checkout/entity/order_item"; 4 | import Customer from "../../../../domain/customer/entity/customer"; 5 | import Address from "../../../../domain/customer/value-object/address"; 6 | import Product from "../../../../domain/product/entity/product"; 7 | import CustomerModel from "../../../customer/repository/sequelize/customer.model"; 8 | import CustomerRepository from "../../../customer/repository/sequelize/customer.repository"; 9 | import ProductModel from "../../../product/repository/sequelize/product.model"; 10 | import ProductRepository from "../../../product/repository/sequelize/product.repository"; 11 | import OrderItemModel from "./order-item.model"; 12 | import OrderModel from "./order.model"; 13 | import OrderRepository from "./order.repository"; 14 | 15 | describe("Order repository test", () => { 16 | let sequelize: Sequelize; 17 | 18 | beforeEach(async () => { 19 | sequelize = new Sequelize({ 20 | dialect: "sqlite", 21 | storage: ":memory:", 22 | logging: false, 23 | sync: { force: true }, 24 | }); 25 | 26 | await sequelize.addModels([ 27 | CustomerModel, 28 | OrderModel, 29 | OrderItemModel, 30 | ProductModel, 31 | ]); 32 | await sequelize.sync(); 33 | }); 34 | 35 | afterEach(async () => { 36 | await sequelize.close(); 37 | }); 38 | 39 | it("should create a new order", async () => { 40 | const customerRepository = new CustomerRepository(); 41 | const customer = new Customer("123", "Customer 1"); 42 | const address = new Address("Street 1", 1, "Zipcode 1", "City 1"); 43 | customer.changeAddress(address); 44 | await customerRepository.create(customer); 45 | 46 | const productRepository = new ProductRepository(); 47 | const product = new Product("123", "Product 1", 10); 48 | await productRepository.create(product); 49 | 50 | const orderItem = new OrderItem( 51 | "1", 52 | product.name, 53 | product.price, 54 | product.id, 55 | 2 56 | ); 57 | 58 | const order = new Order("123", "123", [orderItem]); 59 | 60 | const orderRepository = new OrderRepository(); 61 | await orderRepository.create(order); 62 | 63 | const orderModel = await OrderModel.findOne({ 64 | where: { id: order.id }, 65 | include: ["items"], 66 | }); 67 | 68 | expect(orderModel.toJSON()).toStrictEqual({ 69 | id: "123", 70 | customer_id: "123", 71 | total: order.total(), 72 | items: [ 73 | { 74 | id: orderItem.id, 75 | name: orderItem.name, 76 | price: orderItem.price, 77 | quantity: orderItem.quantity, 78 | order_id: "123", 79 | product_id: "123", 80 | }, 81 | ], 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /src/infrastructure/order/repository/sequilize/order.repository.ts: -------------------------------------------------------------------------------- 1 | import Order from "../../../../domain/checkout/entity/order"; 2 | import OrderItemModel from "./order-item.model"; 3 | import OrderModel from "./order.model"; 4 | 5 | export default class OrderRepository { 6 | async create(entity: Order): Promise { 7 | await OrderModel.create( 8 | { 9 | id: entity.id, 10 | customer_id: entity.customerId, 11 | total: entity.total(), 12 | items: entity.items.map((item) => ({ 13 | id: item.id, 14 | name: item.name, 15 | price: item.price, 16 | product_id: item.productId, 17 | quantity: item.quantity, 18 | })), 19 | }, 20 | { 21 | include: [{ model: OrderItemModel }], 22 | } 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/infrastructure/product/repository/sequelize/product.model.ts: -------------------------------------------------------------------------------- 1 | import { Table, Model, PrimaryKey, Column } from "sequelize-typescript"; 2 | 3 | @Table({ 4 | tableName: "products", 5 | timestamps: false, 6 | }) 7 | export default class ProductModel extends Model { 8 | @PrimaryKey 9 | @Column 10 | declare id: string; 11 | 12 | @Column({ allowNull: false }) 13 | declare name: string; 14 | 15 | @Column({ allowNull: false }) 16 | declare price: number; 17 | } 18 | -------------------------------------------------------------------------------- /src/infrastructure/product/repository/sequelize/product.repository.spec.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize } from "sequelize-typescript"; 2 | import Product from "../../../../domain/product/entity/product"; 3 | import ProductModel from "./product.model"; 4 | import ProductRepository from "./product.repository"; 5 | 6 | describe("Product repository test", () => { 7 | let sequileze: Sequelize; 8 | 9 | beforeEach(async () => { 10 | sequileze = new Sequelize({ 11 | dialect: "sqlite", 12 | storage: ":memory:", 13 | logging: false, 14 | sync: { force: true }, 15 | }); 16 | sequileze.addModels([ProductModel]); 17 | await sequileze.sync(); 18 | }); 19 | 20 | afterEach(async () => { 21 | await sequileze.close(); 22 | }); 23 | 24 | it("should create a product", async () => { 25 | const productRepository = new ProductRepository(); 26 | const product = new Product("1", "Product 1", 100); 27 | 28 | await productRepository.create(product); 29 | 30 | const productModel = await ProductModel.findOne({ where: { id: "1" } }); 31 | 32 | expect(productModel.toJSON()).toStrictEqual({ 33 | id: "1", 34 | name: "Product 1", 35 | price: 100, 36 | }); 37 | }); 38 | 39 | it("should update a product", async () => { 40 | const productRepository = new ProductRepository(); 41 | const product = new Product("1", "Product 1", 100); 42 | 43 | await productRepository.create(product); 44 | 45 | const productModel = await ProductModel.findOne({ where: { id: "1" } }); 46 | 47 | expect(productModel.toJSON()).toStrictEqual({ 48 | id: "1", 49 | name: "Product 1", 50 | price: 100, 51 | }); 52 | 53 | product.changeName("Product 2"); 54 | product.changePrice(200); 55 | 56 | await productRepository.update(product); 57 | 58 | const productModel2 = await ProductModel.findOne({ where: { id: "1" } }); 59 | 60 | expect(productModel2.toJSON()).toStrictEqual({ 61 | id: "1", 62 | name: "Product 2", 63 | price: 200, 64 | }); 65 | }); 66 | 67 | it("should find a product", async () => { 68 | const productRepository = new ProductRepository(); 69 | const product = new Product("1", "Product 1", 100); 70 | 71 | await productRepository.create(product); 72 | 73 | const productModel = await ProductModel.findOne({ where: { id: "1" } }); 74 | 75 | const foundProduct = await productRepository.find("1"); 76 | 77 | expect(productModel.toJSON()).toStrictEqual({ 78 | id: foundProduct.id, 79 | name: foundProduct.name, 80 | price: foundProduct.price, 81 | }); 82 | }); 83 | 84 | it("should find all products", async () => { 85 | const productRepository = new ProductRepository(); 86 | const product = new Product("1", "Product 1", 100); 87 | await productRepository.create(product); 88 | 89 | const product2 = new Product("2", "Product 2", 200); 90 | await productRepository.create(product2); 91 | 92 | const foundProducts = await productRepository.findAll(); 93 | const products = [product, product2]; 94 | 95 | expect(products).toEqual(foundProducts); 96 | }); 97 | 98 | }); 99 | -------------------------------------------------------------------------------- /src/infrastructure/product/repository/sequelize/product.repository.ts: -------------------------------------------------------------------------------- 1 | import Product from "../../../../domain/product/entity/product"; 2 | import ProductRepositoryInterface from "../../../../domain/product/repository/product-repository.interface"; 3 | import ProductModel from "./product.model"; 4 | 5 | export default class ProductRepository implements ProductRepositoryInterface { 6 | async create(entity: Product): Promise { 7 | await ProductModel.create({ 8 | id: entity.id, 9 | name: entity.name, 10 | price: entity.price, 11 | }); 12 | } 13 | 14 | async update(entity: Product): Promise { 15 | await ProductModel.update( 16 | { 17 | name: entity.name, 18 | price: entity.price, 19 | }, 20 | { 21 | where: { 22 | id: entity.id, 23 | }, 24 | } 25 | ); 26 | } 27 | 28 | async find(id: string): Promise { 29 | const productModel = await ProductModel.findOne({ where: { id } }); 30 | return new Product(productModel.id, productModel.name, productModel.price); 31 | } 32 | 33 | async findAll(): Promise { 34 | const productModels = await ProductModel.findAll(); 35 | return productModels.map((productModel) => 36 | new Product(productModel.id, productModel.name, productModel.price) 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "commonjs", /* Specify what module code is generated. */ 28 | // "rootDir": "./", /* Specify the root folder within your source files. */ 29 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | // "resolveJsonModule": true, /* Enable importing .json files */ 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 50 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 51 | // "removeComments": true, /* Disable emitting comments. */ 52 | // "noEmit": true, /* Disable emitting files from a compilation. */ 53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 68 | 69 | /* Interop Constraints */ 70 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 71 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 72 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 73 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 74 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 75 | 76 | /* Type Checking */ 77 | "strict": true, /* Enable all strict type-checking options. */ 78 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 79 | "strictNullChecks": false, /* When type checking, take into account `null` and `undefined`. */ 80 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 81 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 82 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 83 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 84 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 85 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 86 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 87 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 88 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 89 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 90 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 91 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 92 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 93 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 94 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 95 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 96 | 97 | /* Completeness */ 98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 99 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 100 | }, 101 | "include": [ 102 | "src/**/*.ts" 103 | ], 104 | } 105 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": {}, 8 | "rulesDirectory": [] 9 | } --------------------------------------------------------------------------------