diff --git a/backend/.gitignore b/backend/.gitignore index d6b3d95c..9268639c 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,4 +1,5 @@ build/ .idea/ .gradle/ - +**/node_modules +package-lock.json diff --git a/backend/individual-assignment/bootstrap.ts b/backend/individual-assignment/bootstrap.ts deleted file mode 100644 index 3b3bdfd5..00000000 --- a/backend/individual-assignment/bootstrap.ts +++ /dev/null @@ -1,106 +0,0 @@ -class Order { - public items: OrderItem[] = []; - public closedAt: Date | null = null; - public payment: Payment | null = null; - - constructor(public customer: Customer, public address: Address) {} - - get totalAmount(): number { - return this.items.reduce((sum, item) => sum + item.total, 0); - } - - addProduct(product: Product, quantity: number): void { - const productAlreadyAdded = this.items.some(item => item.product === product); - if (productAlreadyAdded) { - throw new Error("The product has already been added. Change the amount if you want more."); - } - - this.items.push(new OrderItem(product, quantity)); - } - - pay(method: PaymentMethod): void { - if (this.payment) { - throw new Error("The order has already been paid!"); - } - - if (this.items.length === 0) { - throw new Error("Empty order cannot be paid!"); - } - - this.payment = new Payment(this, method); - this.close(); - } - - public close(): void { - this.closedAt = new Date(); - } -} - -class OrderItem { - constructor(public product: Product, public quantity: number) {} - - get total(): number { - return this.product.price * this.quantity; - } -} - -class Payment { - public paidAt: Date; - public authorizationNumber: number; - public amount: number; - public invoice: Invoice; - - constructor(public order: Order, public paymentMethod: PaymentMethod) { - this.paidAt = new Date(); - this.authorizationNumber = this.paidAt.getTime(); - this.amount = order.totalAmount; - this.invoice = new Invoice(order); - } -} - -class CreditCard implements PaymentMethod { - constructor(public number: string) {} -} - -interface PaymentMethod {} - -class Invoice { - public billingAddress: Address; - public shippingAddress: Address; - - constructor(public order: Order) { - this.billingAddress = order.address; - this.shippingAddress = order.address; - } -} - -class Product { - constructor(public name: string, public type: ProductType, public price: number) {} -} - -enum ProductType { - PHYSICAL = 'PHYSICAL', - BOOK = 'BOOK', - DIGITAL = 'DIGITAL', - MEMBERSHIP = 'MEMBERSHIP' -} - -class Address {} - -class Customer {} - -// Exemplo de uso -const shirt = new Product("Flowered t-shirt", ProductType.PHYSICAL, 35.00); -const netflix = new Product("Familiar plan", ProductType.MEMBERSHIP, 29.90); -const book = new Product("The Hitchhiker's Guide to the Galaxy", ProductType.BOOK, 120.00); -const music = new Product("Stairway to Heaven", ProductType.DIGITAL, 5.00); - -const order = new Order(new Customer(), new Address()); - -order.addProduct(shirt, 2); -order.addProduct(netflix, 1); -order.addProduct(book, 1); -order.addProduct(music, 1); - -order.pay(new CreditCard("43567890-987654367")); -// now, how to deal with shipping rules then? diff --git a/backend/individual-assignment/typescript/SolutionDescription.md b/backend/individual-assignment/typescript/SolutionDescription.md new file mode 100644 index 00000000..a44cd700 --- /dev/null +++ b/backend/individual-assignment/typescript/SolutionDescription.md @@ -0,0 +1,58 @@ +## Desafio para Backend Software Engineer + +A primeira parte da solução foi organizar as classes em arquivos específicos, de forma a visualizar melhor o que existia e para que cada classe tenha seu objetivo único. + +Para compreender melhor o código e garantir que seguisse funcionando corretamente, a segunda parte da minha solução foi aplicar testes de unidade. Nessa etapa, decidi implementar também testes para as novas funcionalidades a serem criadas após a compra de cada tipo de produto. Para testar esses novos casos, coloquei as condições encadeadas em if/else somente para dividir e ter mais clareza das condições. + +Agora com os testes dos cenários de sucesso das novas condições, criei novas classes para cada tipo de processamento com base no tipo de produto, usei um mapeamento dos respectivos tipos e classes associadas para os produtos. + +Movi os arquivos relacionados a produto para uma pasta específica para melhorar a organização, o que deve ser feito com os outros domínios para organização conforme eles forem crescendo. + +Por fim, adicionei um formatador de código para seguir um padrão. Ainda existem várias melhorias que podem ser aplicadas, como por exemplo a aplicação de testes para demais cenários, a organização da classe de pagamento e organização do restante do projeto. + + +### Exemplo de Futura Organização: + +``` +/src +├── bootstrap.ts +├── /common +│ ├── /providers +│ │ └── /tests +│ │ └── emailProviderService.spec.ts +│ │ └── emailProviderService.ts +├── /address +│ ├── /tests +│ │ └── address.spec.ts +│ ├── address.ts +├── /customer +│ ├── /tests +│ │ └── customer.spec.ts +│ ├── customer.ts +├── /invoice +│ ├── /tests +│ │ └── invoice.spec.ts +│ ├── invoice.ts +├── /order +│ ├── /tests +│ │ └── order.spec.ts +│ │ └── orderItem.spec.ts +│ ├── order.ts +│ ├── orderItem.ts +├── /payment +│ ├── /tests +│ │ └── payment.spec.ts +│ │ └── paymentMethod**.spec.ts +│ ├── payment.ts +│ ├── paymentMethod**.ts +│ ├── paymentTypes.ts +├── /product +│ ├── /index.ts // Opcional +│ ├── /product.ts +│ ├── /processors +│ │ └── /tests +│ │ └── *...Processor.spec.ts +│ │ └── *...Processor.ts +│ ├── /tests +│ │ └── product.spec.ts +``` \ No newline at end of file diff --git a/backend/individual-assignment/typescript/jest.config.js b/backend/individual-assignment/typescript/jest.config.js new file mode 100644 index 00000000..b07ae7b0 --- /dev/null +++ b/backend/individual-assignment/typescript/jest.config.js @@ -0,0 +1,4 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', +}; \ No newline at end of file diff --git a/backend/individual-assignment/typescript/package.json b/backend/individual-assignment/typescript/package.json new file mode 100644 index 00000000..c20bf864 --- /dev/null +++ b/backend/individual-assignment/typescript/package.json @@ -0,0 +1,12 @@ +{ + "scripts": { + "test": "jest", + "format": "prettier --write src/**" + }, + "devDependencies": { + "@types/jest": "^29.5.12", + "jest": "^29.7.0", + "prettier": "^3.3.3", + "ts-jest": "^29.2.4" + } +} diff --git a/backend/individual-assignment/typescript/src/address.ts b/backend/individual-assignment/typescript/src/address.ts new file mode 100644 index 00000000..9c33af8a --- /dev/null +++ b/backend/individual-assignment/typescript/src/address.ts @@ -0,0 +1 @@ +export class Address {} diff --git a/backend/individual-assignment/typescript/src/bootstrap.spec.ts b/backend/individual-assignment/typescript/src/bootstrap.spec.ts new file mode 100644 index 00000000..b848c830 --- /dev/null +++ b/backend/individual-assignment/typescript/src/bootstrap.spec.ts @@ -0,0 +1,27 @@ +import order from "./bootstrap"; +import { OrderItem } from "./orderItem"; +import { CreditCard } from "./payment"; + +describe("bootstrap", () => { + it("should order be correctely defined", () => { + expect(order).toBeDefined(); + expect(order.items).toHaveLength(4); + expect(order.totalAmount).toEqual(224.9); + expect(order.closedAt).toBeDefined(); + + order.items.forEach((item) => { + expect(item).toBeInstanceOf(OrderItem); + expect(item.total).toBeDefined(); + expect(item.quantity).toBeDefined(); + expect(item.product.name).toBeDefined(); + expect(item.product.type).toBeDefined(); + expect(item.product.price).toBeDefined(); + }); + }); + + it("should not allow duplicate payment to order", () => { + expect(() => order.pay(new CreditCard("43567890-987654367"))).toThrow( + "The order has already been paid!", + ); + }); +}); diff --git a/backend/individual-assignment/typescript/src/bootstrap.ts b/backend/individual-assignment/typescript/src/bootstrap.ts new file mode 100644 index 00000000..9ced5c30 --- /dev/null +++ b/backend/individual-assignment/typescript/src/bootstrap.ts @@ -0,0 +1,30 @@ +import { Address } from "./address"; +import { Customer } from "./customer"; +import { Order } from "./order"; +import { CreditCard } from "./payment"; +import { Product, ProductType } from "./product/product"; + +// Exemplo de uso +const shirt = new Product("Flowered t-shirt", ProductType.PHYSICAL, 35.0); +const netflix = new Product("Familiar plan", ProductType.MEMBERSHIP, 29.9); +const book = new Product( + "The Hitchhiker's Guide to the Galaxy", + ProductType.BOOK, + 120.0, +); +const music = new Product("Stairway to Heaven", ProductType.DIGITAL, 5.0); + +const order = new Order( + new Customer("Maria", "maria@gmail.com"), + new Address(), +); + +order.addProduct(shirt, 2); +order.addProduct(netflix, 1); +order.addProduct(book, 1); +order.addProduct(music, 1); + +order.pay(new CreditCard("43567890-987654367")); + +export default order; // to test this case +// now, how to deal with shipping rules then? diff --git a/backend/individual-assignment/typescript/src/customer.ts b/backend/individual-assignment/typescript/src/customer.ts new file mode 100644 index 00000000..b0e81649 --- /dev/null +++ b/backend/individual-assignment/typescript/src/customer.ts @@ -0,0 +1,6 @@ +export class Customer { + constructor( + public name: string, + public email: string, + ) {} +} diff --git a/backend/individual-assignment/typescript/src/emailProviderService.ts b/backend/individual-assignment/typescript/src/emailProviderService.ts new file mode 100644 index 00000000..727bf380 --- /dev/null +++ b/backend/individual-assignment/typescript/src/emailProviderService.ts @@ -0,0 +1,15 @@ +export class EmailProviderService { + /** + * Method to be implemented + */ + public send( + recipient: string, + subject: string, + body: string, + from: string = "defaultEmail@gmail.com", + ) { + console.log( + `Sendind email to ${recipient} from ${from}. Subject: ${subject}. Body: ${body}`, + ); + } +} diff --git a/backend/individual-assignment/typescript/src/invoice.ts b/backend/individual-assignment/typescript/src/invoice.ts new file mode 100644 index 00000000..fe4fd19e --- /dev/null +++ b/backend/individual-assignment/typescript/src/invoice.ts @@ -0,0 +1,12 @@ +import { Address } from "./address"; +import { Order } from "./order"; + +export class Invoice { + public billingAddress: Address; + public shippingAddress: Address; + + constructor(public order: Order) { + this.billingAddress = order.address; + this.shippingAddress = order.address; + } +} diff --git a/backend/individual-assignment/typescript/src/order.spec.ts b/backend/individual-assignment/typescript/src/order.spec.ts new file mode 100644 index 00000000..07cd861c --- /dev/null +++ b/backend/individual-assignment/typescript/src/order.spec.ts @@ -0,0 +1,128 @@ +import { Address } from "./address"; +import { Customer } from "./customer"; +import { Order } from "./order"; +import { CreditCard, Payment } from "./payment"; +import { + Product, + ProductType, + BookProductProcessor, + DigitalProductProcessor, + PhysicalProductProcessor, + MembershipProductProcessor, +} from "./product"; + +describe("order", () => { + let order: Order; + + beforeEach(() => { + jest.clearAllMocks(); + order = new Order(new Customer("Maria", "maria@gmail.com"), new Address()); + }); + + const mockBookProductProcess = jest.fn(); + const mockDigitalProductProcess = jest.fn(); + const mockPhysicalProductProcess = jest.fn(); + const mockMembershipProductProcess = jest.fn(); + + jest + .spyOn(BookProductProcessor.prototype, "process") + .mockImplementation(mockBookProductProcess); + jest + .spyOn(DigitalProductProcessor.prototype, "process") + .mockImplementation(mockDigitalProductProcess); + jest + .spyOn(PhysicalProductProcessor.prototype, "process") + .mockImplementation(mockPhysicalProductProcess); + jest + .spyOn(MembershipProductProcessor.prototype, "process") + .mockImplementation(mockMembershipProductProcess); + + const shirt = new Product("Basic t-shirt", ProductType.PHYSICAL, 49.9); + const max = new Product("Standard plan", ProductType.MEMBERSHIP, 29.9); + const book = new Product( + "The Hitchhiker's Guide to the Galaxy", + ProductType.BOOK, + 120.0, + ); + const music = new Product("Stairway to Heaven", ProductType.DIGITAL, 5.0); + const card = new CreditCard("43567890-987654367"); + + it("should add products to order", () => { + order.addProduct(shirt, 2); + + expect(order.items).toHaveLength(1); + expect(order.items[0].product).toEqual(shirt); + expect(order.items[0].quantity).toEqual(2); + + order.addProduct(max, 1); + + expect(order.items).toHaveLength(2); + expect(order.items[1].product).toEqual(max); + expect(order.items[1].quantity).toEqual(1); + expect(order.totalAmount).toEqual(129.7); + + order.addProduct(music, 1); + + expect(order.items).toHaveLength(3); + expect(order.items[2].product).toEqual(music); + expect(order.items[2].quantity).toEqual(1); + expect(order.totalAmount).toEqual(134.7); + + order.pay(card); + + expect(mockBookProductProcess).not.toHaveBeenCalled(); + expect(mockDigitalProductProcess).toHaveBeenCalled(); + expect(mockPhysicalProductProcess).toHaveBeenCalled(); + expect(mockMembershipProductProcess).toHaveBeenCalled(); + }); + + it("should not add the same product twice", () => { + order.addProduct(book, 2); + expect(() => order.addProduct(book, 1)).toThrow( + "The product has already been added. Change the amount if you want more.", + ); + }); + + it("should process payment and close order", () => { + order.addProduct(shirt, 2); + + expect(order.closedAt).toBeNull(); + + order.pay(card); + + expect(order.closedAt).not.toBeNull(); + expect(order.payment).not.toBeNull(); + expect(order.payment).toBeInstanceOf(Payment); + expect(order.payment?.paidAt).not.toBeNull(); + expect(order.payment?.authorizationNumber).toBeGreaterThan(0); + expect(order.payment?.amount).toEqual(order.totalAmount); + expect(order.payment?.invoice.billingAddress).toEqual(order.address); + expect(order.payment?.invoice.shippingAddress).toEqual(order.address); + }); + + it("should not allow duplicate payments", () => { + order.addProduct(shirt, 2); + order.pay(card); + expect(() => order.pay(card)).toThrow("The order has already been paid!"); + }); + + it("should throw error when trying to pay empty order", () => { + expect(() => order.pay(card)).toThrow("Empty order cannot be paid!"); + }); + + it.each` + product | mockProcessFunction | productType + ${shirt} | ${mockPhysicalProductProcess} | ${"PHYSICAL"} + ${max} | ${mockMembershipProductProcess} | ${"MEMBERSHIP"} + ${book} | ${mockBookProductProcess} | ${"BOOK"} + ${music} | ${mockDigitalProductProcess} | ${"DIGITAL"} + `( + "should call correct processor when product type is $productType", + ({ product, mockProcessFunction }) => { + order.addProduct(product, 1); + order.pay(card); + + expect(mockProcessFunction).toHaveBeenCalled(); + }, + ); +}); diff --git a/backend/individual-assignment/typescript/src/order.ts b/backend/individual-assignment/typescript/src/order.ts new file mode 100644 index 00000000..20ab564e --- /dev/null +++ b/backend/individual-assignment/typescript/src/order.ts @@ -0,0 +1,91 @@ +import { Address } from "./address"; +import { Customer } from "./customer"; +import { + Product, + ProductType, + BookProductProcessor, + DigitalProductProcessor, + PhysicalProductProcessor, + MembershipProductProcessor, + ProductProcessorAfterPurchase, +} from "./product"; +import { Payment, PaymentMethod } from "./payment"; +import { OrderItem } from "./orderItem"; +import { EmailProviderService } from "./emailProviderService"; + +export class Order { + public items: OrderItem[] = []; + public closedAt: Date | null = null; + public payment: Payment | null = null; + public description: string | null = null; + private processorsAfterPurchase: Map< + ProductType, + ProductProcessorAfterPurchase + > = new Map(); + + constructor( + public customer: Customer, + public address: Address, + ) { + this.processorsAfterPurchase.set( + ProductType.PHYSICAL, + new PhysicalProductProcessor(), + ); + this.processorsAfterPurchase.set( + ProductType.BOOK, + new BookProductProcessor({}), + ); + this.processorsAfterPurchase.set( + ProductType.MEMBERSHIP, + new MembershipProductProcessor(), + ); + this.processorsAfterPurchase.set( + ProductType.DIGITAL, + new DigitalProductProcessor(new EmailProviderService()), + ); + } + + public get totalAmount(): number { + return this.items.reduce((sum, item) => sum + item.total, 0); + } + + public addProduct(product: Product, quantity: number): void { + const productAlreadyAdded = this.items.some( + (item) => item.product === product, + ); + if (productAlreadyAdded) { + throw new Error( + "The product has already been added. Change the amount if you want more.", + ); + } + + this.items.push(new OrderItem(product, quantity)); + } + + public pay(method: PaymentMethod): void { + if (this.payment) { + throw new Error("The order has already been paid!"); + } + + if (this.items.length === 0) { + throw new Error("Empty order cannot be paid!"); + } + + this.payment = new Payment(this, method); + this.close(); + this.processingActionsAfterPurchase(); + } + + private processingActionsAfterPurchase(): void { + this.items.forEach((item) => { + const productProcessor = this.processorsAfterPurchase.get( + item.product.type, + ); + if (productProcessor) productProcessor.process(this); + }); + } + + public close(): void { + this.closedAt = new Date(); + } +} diff --git a/backend/individual-assignment/typescript/src/orderItem.ts b/backend/individual-assignment/typescript/src/orderItem.ts new file mode 100644 index 00000000..1c65f114 --- /dev/null +++ b/backend/individual-assignment/typescript/src/orderItem.ts @@ -0,0 +1,12 @@ +import { Product } from "./product/product"; + +export class OrderItem { + constructor( + public product: Product, + public quantity: number, + ) {} + + public get total(): number { + return this.product.price * this.quantity; + } +} diff --git a/backend/individual-assignment/typescript/src/payment.ts b/backend/individual-assignment/typescript/src/payment.ts new file mode 100644 index 00000000..aae316a9 --- /dev/null +++ b/backend/individual-assignment/typescript/src/payment.ts @@ -0,0 +1,25 @@ +import { Invoice } from "./invoice"; +import { Order } from "./order"; + +export interface PaymentMethod {} + +export class Payment { + public paidAt: Date; + public authorizationNumber: number; + public amount: number; + public invoice: Invoice; + + constructor( + public order: Order, + public paymentMethod: PaymentMethod, + ) { + this.paidAt = new Date(); + this.authorizationNumber = this.paidAt.getTime(); + this.amount = order.totalAmount; + this.invoice = new Invoice(order); + } +} + +export class CreditCard implements PaymentMethod { + constructor(public number: string) {} +} diff --git a/backend/individual-assignment/typescript/src/product/bookProductProcessor.spec.ts b/backend/individual-assignment/typescript/src/product/bookProductProcessor.spec.ts new file mode 100644 index 00000000..bff27fa0 --- /dev/null +++ b/backend/individual-assignment/typescript/src/product/bookProductProcessor.spec.ts @@ -0,0 +1,20 @@ +import { BookProductProcessor } from "./bookProductProcessor"; + +describe("BookProductProcessor", () => { + let bookProductProcessor: BookProductProcessor; + + beforeEach(() => { + jest.clearAllMocks(); + bookProductProcessor = new BookProductProcessor({}); + }); + const consoleLogSpy = jest.spyOn(console, "log"); + + describe("process()", () => { + it("should call console.log with correct value", () => { + bookProductProcessor.process(); + expect(consoleLogSpy).toHaveBeenCalledWith( + "Create shipping label with notification that it is a tax-exempt item as provided in the Constitution Art. 150, VI, d.", + ); + }); + }); +}); diff --git a/backend/individual-assignment/typescript/src/product/bookProductProcessor.ts b/backend/individual-assignment/typescript/src/product/bookProductProcessor.ts new file mode 100644 index 00000000..70675d05 --- /dev/null +++ b/backend/individual-assignment/typescript/src/product/bookProductProcessor.ts @@ -0,0 +1,18 @@ +import { ProductProcessorAfterPurchase } from "./product"; + +/** + * Service to be created and moved to specific file + */ +class ShippingLabelService {} + +export class BookProductProcessor implements ProductProcessorAfterPurchase { + constructor(private shippingLabelService: ShippingLabelService) {} + /** + * Method to be implemented + */ + public process() { + console.log( + "Create shipping label with notification that it is a tax-exempt item as provided in the Constitution Art. 150, VI, d.", + ); + } +} diff --git a/backend/individual-assignment/typescript/src/product/digitalProductProcessor.spec.ts b/backend/individual-assignment/typescript/src/product/digitalProductProcessor.spec.ts new file mode 100644 index 00000000..636b4179 --- /dev/null +++ b/backend/individual-assignment/typescript/src/product/digitalProductProcessor.spec.ts @@ -0,0 +1,40 @@ +import { Address } from "../address"; +import { Customer } from "../customer"; +import { EmailProviderService } from "../emailProviderService"; +import { Order } from "../order"; +import { DigitalProductProcessor } from "./digitalProductProcessor"; + +describe("DigitalProductProcessor", () => { + let digitalProductProcessor: DigitalProductProcessor; + const mockEmailSend = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + digitalProductProcessor = new DigitalProductProcessor({ + send: mockEmailSend, + } as EmailProviderService); + }); + + const consoleLogSpy = jest.spyOn(console, "log"); + const order = new Order( + new Customer("Customer Name", "customer@test.com"), + new Address(), + ); + + describe("process()", () => { + it("should call console.log with correct value", () => { + digitalProductProcessor.process(order); + expect(consoleLogSpy).toHaveBeenCalledWith( + "Send the purchase description by email to the customer", + ); + expect(consoleLogSpy).toHaveBeenCalledWith( + "Grant a R$10 discount voucher to the customer Customer Name associated with the payment", + ); + expect(mockEmailSend).toHaveBeenCalledWith( + "customer@test.com", + "Purchase Description", + "Default Purchase Description", + ); + }); + }); +}); diff --git a/backend/individual-assignment/typescript/src/product/digitalProductProcessor.ts b/backend/individual-assignment/typescript/src/product/digitalProductProcessor.ts new file mode 100644 index 00000000..2f5c64e2 --- /dev/null +++ b/backend/individual-assignment/typescript/src/product/digitalProductProcessor.ts @@ -0,0 +1,31 @@ +import { Order } from "../order"; +import { Customer } from "../customer"; +import { EmailProviderService } from "../emailProviderService"; +import { ProductProcessorAfterPurchase } from "./product"; + +export class DigitalProductProcessor implements ProductProcessorAfterPurchase { + constructor(private emailProvider: EmailProviderService) {} + + public process(order: Order) { + this.sendEmail(order.customer, order.description); + this.createDiscountVoucher(order.customer); + } + + private sendEmail(customer: Customer, description: string | null) { + console.log("Send the purchase description by email to the customer"); + this.emailProvider.send( + customer.email, + "Purchase Description", + description ?? "Default Purchase Description", + ); + } + + /** + * Method to be implemented + */ + private createDiscountVoucher(customer: Customer) { + console.log( + `Grant a R$10 discount voucher to the customer ${customer.name} associated with the payment`, + ); + } +} diff --git a/backend/individual-assignment/typescript/src/product/index.ts b/backend/individual-assignment/typescript/src/product/index.ts new file mode 100644 index 00000000..fe1b85b2 --- /dev/null +++ b/backend/individual-assignment/typescript/src/product/index.ts @@ -0,0 +1,5 @@ +export * from "./product"; +export * from "./bookProductProcessor"; +export * from "./digitalProductProcessor"; +export * from "./membershipProductProcessor"; +export * from "./physicalProductProcessor"; diff --git a/backend/individual-assignment/typescript/src/product/membershipProductProcessor.spec.ts b/backend/individual-assignment/typescript/src/product/membershipProductProcessor.spec.ts new file mode 100644 index 00000000..f4c2a08e --- /dev/null +++ b/backend/individual-assignment/typescript/src/product/membershipProductProcessor.spec.ts @@ -0,0 +1,22 @@ +import { MembershipProductProcessor } from "./membershipProductProcessor"; + +describe("MembershipProductProcessor", () => { + let membershipProductProcessor: MembershipProductProcessor; + + beforeEach(() => { + jest.clearAllMocks(); + membershipProductProcessor = new MembershipProductProcessor(); + }); + + const consoleLogSpy = jest.spyOn(console, "log"); + + describe("process()", () => { + it("should call console.log with correct value", () => { + membershipProductProcessor.process(); + expect(consoleLogSpy).toHaveBeenCalledWith("Activate the subscription"); + expect(consoleLogSpy).toHaveBeenCalledWith( + "Notify the user via email about the subscription", + ); + }); + }); +}); diff --git a/backend/individual-assignment/typescript/src/product/membershipProductProcessor.ts b/backend/individual-assignment/typescript/src/product/membershipProductProcessor.ts new file mode 100644 index 00000000..0de2012b --- /dev/null +++ b/backend/individual-assignment/typescript/src/product/membershipProductProcessor.ts @@ -0,0 +1,13 @@ +import { ProductProcessorAfterPurchase } from "./product"; + +export class MembershipProductProcessor + implements ProductProcessorAfterPurchase +{ + /** + * Method to be implemented + */ + public process() { + console.log("Activate the subscription"); + console.log("Notify the user via email about the subscription"); + } +} diff --git a/backend/individual-assignment/typescript/src/product/physicalProductProcessor.spec.ts b/backend/individual-assignment/typescript/src/product/physicalProductProcessor.spec.ts new file mode 100644 index 00000000..a22c1c67 --- /dev/null +++ b/backend/individual-assignment/typescript/src/product/physicalProductProcessor.spec.ts @@ -0,0 +1,19 @@ +import { PhysicalProductProcessor } from "./physicalProductProcessor"; + +describe("PhysicalProductProcessor", () => { + let physicalProductProcessor: PhysicalProductProcessor; + + beforeEach(() => { + jest.clearAllMocks(); + physicalProductProcessor = new PhysicalProductProcessor(); + }); + + const consoleLogSpy = jest.spyOn(console, "log"); + + describe("process()", () => { + it("should call console.log with correct value", () => { + physicalProductProcessor.process(); + expect(consoleLogSpy).toHaveBeenCalledWith("Create shipping label"); + }); + }); +}); diff --git a/backend/individual-assignment/typescript/src/product/physicalProductProcessor.ts b/backend/individual-assignment/typescript/src/product/physicalProductProcessor.ts new file mode 100644 index 00000000..278c8e9c --- /dev/null +++ b/backend/individual-assignment/typescript/src/product/physicalProductProcessor.ts @@ -0,0 +1,10 @@ +import { ProductProcessorAfterPurchase } from "./product"; + +export class PhysicalProductProcessor implements ProductProcessorAfterPurchase { + /** + * Method to be implemented + */ + public process() { + console.log("Create shipping label"); + } +} diff --git a/backend/individual-assignment/typescript/src/product/product.ts b/backend/individual-assignment/typescript/src/product/product.ts new file mode 100644 index 00000000..2a394c5d --- /dev/null +++ b/backend/individual-assignment/typescript/src/product/product.ts @@ -0,0 +1,20 @@ +import { Order } from "../order"; + +export enum ProductType { + PHYSICAL = "PHYSICAL", + BOOK = "BOOK", + DIGITAL = "DIGITAL", + MEMBERSHIP = "MEMBERSHIP", +} + +export interface ProductProcessorAfterPurchase { + process(order: Order): void; +} + +export class Product { + constructor( + public name: string, + public type: ProductType, + public price: number, + ) {} +}