Skip to content

Commit

Permalink
feature #6 - add domain, business and adaters of get products function
Browse files Browse the repository at this point in the history
  • Loading branch information
Eugeniosales committed Oct 16, 2022
1 parent 9223996 commit 558934c
Show file tree
Hide file tree
Showing 16 changed files with 461 additions and 1 deletion.
12 changes: 12 additions & 0 deletions src/1-domain/entities/product.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { CurrencyEnum } from '../../2-business/enums/currencyEnum'

export interface Product {
category: string
id: number
name: string
price: number
promotionalPrice: number
rating: number
colors: string[]
currency: CurrencyEnum
}
8 changes: 8 additions & 0 deletions src/2-business/dto/product/getProductstInputDto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { CurrencyEnum } from '../../enums/currencyEnum'
import { ProductCategoryEnum } from '../../enums/productCategory'

export interface GetProductsInputDto {
category: ProductCategoryEnum
baseCurrency: CurrencyEnum
targetCurrency?: CurrencyEnum
}
5 changes: 5 additions & 0 deletions src/2-business/enums/productCategory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum ProductCategoryEnum {
CASUAL_SHOES = 'shoes',
CASUAL_SHIRT = 'shirt',
SOCCER = 'soccer'
}
7 changes: 7 additions & 0 deletions src/2-business/repositories/iProductRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Product } from '../../1-domain/entities/product'
import { CurrencyEnum } from '../enums/currencyEnum'
import { ProductCategoryEnum } from '../enums/productCategory'

export interface IProductRepository {
getByCategoryAndCurrency (category: ProductCategoryEnum, baseCurrency: CurrencyEnum): Promise<Product[]>
}
6 changes: 6 additions & 0 deletions src/2-business/services/iExchangeRateInternalService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ExchangeRate } from '../../1-domain/entities/exchangeRate'
import { CurrencyEnum } from '../../2-business/enums/currencyEnum'

export interface IExchangeRateInternalService {
get (base: CurrencyEnum): Promise<ExchangeRate>
}
51 changes: 51 additions & 0 deletions src/2-business/useCases/product/getProductsUseCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { IExchangeRateInternalService } from '../../services/iExchangeRateInternalService'
import { IProductRepository } from '../../repositories/iProductRepository'
import { GetProductsInputDto } from '../../dto/product/getProductstInputDto'
import { Product } from '../../../1-domain/entities/product'
import { CurrencyEnum } from '../../enums/currencyEnum'

export class GetProductsUseCase {

constructor (
private readonly productRepository: IProductRepository,
private readonly exchangeRateInternalService: IExchangeRateInternalService
) {}

private logPrefix: string = 'GetExchangeRateUseCase'

async execute (input: GetProductsInputDto): Promise<Product[]> {
console.log(`${this.logPrefix} :: start`)
try {
const { category, baseCurrency, targetCurrency } = input
let products: Product[] = await this.productRepository.getByCategoryAndCurrency(category, baseCurrency)

console.log(`${this.logPrefix} ::`, { products })

products = await this.calculatePriceByLatestCurrentExchangeRate(products, baseCurrency, targetCurrency)

console.log(`${this.logPrefix} :: products with currency updated ::`, { products })
console.log(`${this.logPrefix} :: end`)

return products
} catch (error) {
throw error
}
}

private async calculatePriceByLatestCurrentExchangeRate (products: Product[], originCurrency: CurrencyEnum, targetCurrency: CurrencyEnum | undefined): Promise<Product[]> {
const exchangeRate = await this.exchangeRateInternalService.get(originCurrency)
console.log('calculatePriceByLatestCurrentExchangeRate ::', { exchangeRate })

const targetCurrencyExchangeRate = exchangeRate.rates[targetCurrency || originCurrency]

return products.map(product => {
return {
...product,
price: product.price * targetCurrencyExchangeRate,
promotionalPrice: product.promotionalPrice * targetCurrencyExchangeRate
}
})
}
}

export default GetProductsUseCase
47 changes: 47 additions & 0 deletions src/3-adapters/controller/product/getProductsController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ControllerBase } from '../../controllerBase'
import { Output } from '../../../2-business/dto/output'
import { InputGetProducts } from '../../serializers/product/getProducts/input'
import { OutputGetProducts } from '../../serializers/product/getProducts/output'
import { IProductRepository } from '../../../2-business/repositories/iProductRepository'
import { IExchangeRateInternalService } from '../../../2-business/services/iExchangeRateInternalService'
import { GetProductsUseCase } from '../../../2-business/useCases/product/getProductsUseCase'
import { Product } from '../../../1-domain/entities/product'

export class GetProductsController extends ControllerBase<InputGetProducts, OutputGetProducts> {
constructor (
private readonly productRepository: IProductRepository,
private readonly exchangeRateInternalService: IExchangeRateInternalService
) { super() }

async run (input: InputGetProducts): Promise<Output<OutputGetProducts>> {
const logPrefix = 'GetProductsController'
console.log(`${logPrefix} :: start`)
console.log(`${logPrefix} ::`, input)

try {
const getProductsUseCase = new GetProductsUseCase(
this.productRepository,
this.exchangeRateInternalService
)

const products: Product[] = await getProductsUseCase.execute(input)

console.log(`${logPrefix} :: end`)

return {
httpCode: 200,
status: 'success',
data: { products }
}

} catch (error) {
console.error(error)

return {
httpCode: 400,
status: 'error',
message: 'general error'
}
}
}
}
23 changes: 23 additions & 0 deletions src/3-adapters/serializers/product/getProducts/input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { InputClassValidator } from '../../../../4-framework/utility/classValidator'
import { IsNotEmpty, IsOptional, IsString } from 'class-validator'
import { CurrencyEnum } from '../../../../2-business/enums/currencyEnum'
import { ProductCategoryEnum } from '../../../../2-business/enums/productCategory'

export class InputGetProducts extends InputClassValidator {
@IsNotEmpty()
@IsString()
category!: ProductCategoryEnum

@IsNotEmpty()
@IsString()
baseCurrency!: CurrencyEnum

@IsOptional()
@IsString()
targetCurrency?: CurrencyEnum

constructor (obj: Partial<InputGetProducts>) {
super()
Object.assign(this, obj)
}
}
5 changes: 5 additions & 0 deletions src/3-adapters/serializers/product/getProducts/output.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Product } from '../../../../1-domain/entities/product'

export interface OutputGetProducts {
products: Product[]
}
2 changes: 1 addition & 1 deletion src/4-framework/functions/exchangeRate/_handlers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ getExchangeRate:
priority: 1
conditions:
method: GET
path: /${self:custom.baseApi}/exchangerate
path: /${self:custom.baseApiInternal}/exchangerate
request:
parameters:
querystrings:
Expand Down
12 changes: 12 additions & 0 deletions src/4-framework/functions/product/_handlers.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
getProducts:
handler: ${self:custom.funcDir}/product/getProducts.handler
vpc:
securityGroupIds:
- sg-0cd52ea46933db116
subnetdIds:
- subnet-05d245e39491bb3d2
- subnet-06761316782230c98
events:
- http:
method: GET
path: /${self:custom.baseApi}/products
27 changes: 27 additions & 0 deletions src/4-framework/functions/product/getProducts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use strict'

import { APIGatewayEvent } from 'aws-lambda'
import { GetProductsController } from '../../../3-adapters/controller/product/getProductsController'
import { InputGetProducts } from '../../../3-adapters/serializers/product/getProducts/input'
import { ProductRepository } from '../../repositories/productRepository'
import { ExchangeRateInternalService } from '../../services/exchangeRateInternalService'
import { apiGatewayHttpEventNormalizer } from '../../utility/eventAdapters'

function eventAdapter (event: APIGatewayEvent): InputGetProducts {
const body = apiGatewayHttpEventNormalizer(event)
return new InputGetProducts(body)

}

exports.handler = async (event: APIGatewayEvent) => {
const getProductsController = new GetProductsController(
new ProductRepository(),
new ExchangeRateInternalService()
)

const input = eventAdapter(event)

const response = await getProductsController.run(input)

return response
}
60 changes: 60 additions & 0 deletions src/4-framework/models/dynamo/product.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import dynamoose, { Schema } from 'dynamoose'
import { Document } from 'dynamoose/dist/Document'
import { SchemaDefinition } from 'dynamoose/dist/Schema'
import { Model, ModelOptionsOptional } from 'dynamoose/dist/Model'
import { Product } from '../../../1-domain/entities/product'

export interface ProductEntity extends Document, Product { }

const schemaDefinition: SchemaDefinition = {
category: {
type: String,
hashKey: true
},
id: {
type: String,
rangeKey: true
},
name: {
type: String,
required: true
},
price: {
type: Number,
required: true
},
promotionalPrice: {
type: Number,
required: true
},
rating: {
type: Number,
required: true
},
colors: {
type: Array,
required: true
},
currency: {
type: String,
required: true,
index: {
name: 'CategoryBaseCurrencyIndex',
global: true
}
}
}

const schema = new Schema(schemaDefinition, {
timestamps: true,
saveUnknown: true
})

const modelOptions: ModelOptionsOptional = {
throughput: 'ON_DEMAND',
create: false,
waitForActive: false
}

export const ProductModel: Model<ProductEntity> =
dynamoose.model('Product', schema, modelOptions)
14 changes: 14 additions & 0 deletions src/4-framework/repositories/productRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ExchangeRate } from '../../1-domain/entities/exchangeRate'
import { ExchangeRateModel } from '../models/dynamo/exchangeRate'
import { IExchangeRateRepository } from '../../2-business/repositories/iExchangeRateRepository'
import { CurrencyEnum } from '../../2-business/enums/currencyEnum'
import { IProductRepository } from '../../2-business/repositories/iProductRepository'
import { Product } from '../../1-domain/entities/product'
import { ProductCategoryEnum } from '../../2-business/enums/productCategory'
import { ProductModel } from '../models/dynamo/product'

export class ProductRepository implements IProductRepository {
getByCategoryAndCurrency (category: ProductCategoryEnum, baseCurrency: CurrencyEnum): Promise<Product[]> {
return ProductModel.query('category').eq(category).where('currency').eq(baseCurrency).exec()
}
}
Loading

0 comments on commit 558934c

Please sign in to comment.