From 50a600125264ae196ad528bceee49ea831fde4af Mon Sep 17 00:00:00 2001 From: David Arranz Date: Tue, 21 May 2024 18:48:40 +0200 Subject: [PATCH] . --- .prettierc.json | 11 -- .prettierrc | 13 ++ .vscode/settings.json | 7 +- .../express/passport/authMiddleware.ts | 18 +-- .../infrastructure/mappers/authuser.mapper.ts | 6 +- .../sequelize/authUser.model.ts | 3 +- .../express/controllers/listArticles/index.ts | 4 +- .../catalog/infrastructure/express/routes.ts | 8 +- .../infrastructure/mappers/article.mapper.ts | 10 +- .../infrastructure/sequelize/article.model.ts | 3 +- .../express/ExpressController.ts | 33 +--- .../express/ExpressErrorResponse.ts | 17 +-- .../infrastructure/mappers/SequelizeMapper.ts | 130 ++++++++-------- .../sales/application/CreateDealer.useCase.ts | 143 ++++++++++++++++++ .../sales/application/DeleteDealer.useCase.ts | 55 +++++++ .../sales/application/GetDealer.useCase.ts | 71 +++++++++ .../sales/application/ListDealers.useCase.ts | 57 +++++++ .../sales/application/UpdateDealer.useCase.ts | 134 ++++++++++++++++ .../src/contexts/sales/application/index.ts | 5 + .../contexts/sales/domain/entities/Dealer.ts | 21 +++ .../contexts/sales/domain/entities/index.ts | 1 + server/src/contexts/sales/domain/index.ts | 2 + .../repository/DealerRepository.interface.ts | 14 ++ .../contexts/sales/domain/repository/index.ts | 1 + .../sales/infrastructure/Dealer.repository.ts | 85 +++++++++++ .../sales/infrastructure/Sales.context.ts | 32 ++++ .../createDealer/CreateDealer.controller.ts | 119 +++++++++++++++ .../express/controllers/createDealer/index.ts | 23 +++ .../presenter/CreateDealer.presenter.ts | 16 ++ .../createDealer/presenter/index.ts | 1 + .../deleteDealer/DeleteDealer.controller.ts | 84 ++++++++++ .../express/controllers/deleteDealer/index.ts | 21 +++ .../getDealer/GetDealer.controller.ts | 97 ++++++++++++ .../express/controllers/getDealer/index.ts | 24 +++ .../presenter/GetDealer.presenter.ts | 15 ++ .../controllers/getDealer/presenter/index.ts | 1 + .../express/controllers/index.ts | 5 + .../listDealers/ListDealers.controller.ts | 84 ++++++++++ .../express/controllers/listDealers/index.ts | 23 +++ .../presenter/ListDealers.presenter.ts | 50 ++++++ .../listDealers/presenter/index.ts | 1 + .../updateDealer/UpdateDealer.controller.ts | 138 +++++++++++++++++ .../express/controllers/updateDealer/index.ts | 23 +++ .../presenter/UpdateDealer.presenter.ts | 16 ++ .../updateDealer/presenter/index.ts | 1 + .../sales/infrastructure/express/index.ts | 1 + .../sales/infrastructure/express/routes.ts | 21 +++ .../contexts/sales/infrastructure/index.ts | 1 + .../infrastructure/mappers/dealer.mapper.ts | 55 +++++++ .../sales/infrastructure/mappers/index.ts | 1 + .../infrastructure/sequelize/dealer.model.ts | 103 +++++++++++++ .../sales/infrastructure/sequelize/index.ts | 1 + .../infrastructure/mappers/user.mapper.ts | 6 +- .../infrastructure/sequelize/user.model.ts | 2 +- server/src/infrastructure/express/api/v1.ts | 22 +-- shared/lib/contexts/index.ts | 1 + .../ICreateDealer_Request.dto.ts | 22 +++ .../ICreateDealer_Response.dto.ts | 4 + .../application/dto/CreateDealer.dto/index.ts | 2 + .../ICreateReseller_Request.dto.ts | 22 +++ .../GetDealer.dto/IGetDealer_Response.dto.ts | 4 + .../application/dto/GetDealer.dto/index.ts | 1 + .../IListDealers_Response.dto.ts | 4 + .../application/dto/IListDealers.dto/index.ts | 1 + .../IUpdateDealer_Request.dto.ts | 20 +++ .../IUpdateDealer_Response.dto.ts | 4 + .../application/dto/UpdateDealer.dto/index.ts | 2 + .../contexts/sales/application/dto/index.ts | 4 + .../lib/contexts/sales/application/index.ts | 1 + shared/lib/contexts/sales/index.ts | 1 + shared/tsconfig.json | 10 -- 71 files changed, 1769 insertions(+), 173 deletions(-) delete mode 100644 .prettierc.json create mode 100644 .prettierrc create mode 100644 server/src/contexts/sales/application/CreateDealer.useCase.ts create mode 100644 server/src/contexts/sales/application/DeleteDealer.useCase.ts create mode 100644 server/src/contexts/sales/application/GetDealer.useCase.ts create mode 100644 server/src/contexts/sales/application/ListDealers.useCase.ts create mode 100644 server/src/contexts/sales/application/UpdateDealer.useCase.ts create mode 100644 server/src/contexts/sales/application/index.ts create mode 100644 server/src/contexts/sales/domain/entities/Dealer.ts create mode 100644 server/src/contexts/sales/domain/entities/index.ts create mode 100644 server/src/contexts/sales/domain/index.ts create mode 100644 server/src/contexts/sales/domain/repository/DealerRepository.interface.ts create mode 100644 server/src/contexts/sales/domain/repository/index.ts create mode 100644 server/src/contexts/sales/infrastructure/Dealer.repository.ts create mode 100644 server/src/contexts/sales/infrastructure/Sales.context.ts create mode 100644 server/src/contexts/sales/infrastructure/express/controllers/createDealer/CreateDealer.controller.ts create mode 100644 server/src/contexts/sales/infrastructure/express/controllers/createDealer/index.ts create mode 100644 server/src/contexts/sales/infrastructure/express/controllers/createDealer/presenter/CreateDealer.presenter.ts create mode 100644 server/src/contexts/sales/infrastructure/express/controllers/createDealer/presenter/index.ts create mode 100644 server/src/contexts/sales/infrastructure/express/controllers/deleteDealer/DeleteDealer.controller.ts create mode 100644 server/src/contexts/sales/infrastructure/express/controllers/deleteDealer/index.ts create mode 100644 server/src/contexts/sales/infrastructure/express/controllers/getDealer/GetDealer.controller.ts create mode 100644 server/src/contexts/sales/infrastructure/express/controllers/getDealer/index.ts create mode 100644 server/src/contexts/sales/infrastructure/express/controllers/getDealer/presenter/GetDealer.presenter.ts create mode 100644 server/src/contexts/sales/infrastructure/express/controllers/getDealer/presenter/index.ts create mode 100644 server/src/contexts/sales/infrastructure/express/controllers/index.ts create mode 100644 server/src/contexts/sales/infrastructure/express/controllers/listDealers/ListDealers.controller.ts create mode 100644 server/src/contexts/sales/infrastructure/express/controllers/listDealers/index.ts create mode 100644 server/src/contexts/sales/infrastructure/express/controllers/listDealers/presenter/ListDealers.presenter.ts create mode 100644 server/src/contexts/sales/infrastructure/express/controllers/listDealers/presenter/index.ts create mode 100644 server/src/contexts/sales/infrastructure/express/controllers/updateDealer/UpdateDealer.controller.ts create mode 100644 server/src/contexts/sales/infrastructure/express/controllers/updateDealer/index.ts create mode 100644 server/src/contexts/sales/infrastructure/express/controllers/updateDealer/presenter/UpdateDealer.presenter.ts create mode 100644 server/src/contexts/sales/infrastructure/express/controllers/updateDealer/presenter/index.ts create mode 100644 server/src/contexts/sales/infrastructure/express/index.ts create mode 100644 server/src/contexts/sales/infrastructure/express/routes.ts create mode 100644 server/src/contexts/sales/infrastructure/index.ts create mode 100644 server/src/contexts/sales/infrastructure/mappers/dealer.mapper.ts create mode 100644 server/src/contexts/sales/infrastructure/mappers/index.ts create mode 100644 server/src/contexts/sales/infrastructure/sequelize/dealer.model.ts create mode 100644 server/src/contexts/sales/infrastructure/sequelize/index.ts create mode 100644 shared/lib/contexts/sales/application/dto/CreateDealer.dto/ICreateDealer_Request.dto.ts create mode 100644 shared/lib/contexts/sales/application/dto/CreateDealer.dto/ICreateDealer_Response.dto.ts create mode 100644 shared/lib/contexts/sales/application/dto/CreateDealer.dto/index.ts create mode 100644 shared/lib/contexts/sales/application/dto/CreateReseller.dto/ICreateReseller_Request.dto.ts create mode 100644 shared/lib/contexts/sales/application/dto/GetDealer.dto/IGetDealer_Response.dto.ts create mode 100644 shared/lib/contexts/sales/application/dto/GetDealer.dto/index.ts create mode 100644 shared/lib/contexts/sales/application/dto/IListDealers.dto/IListDealers_Response.dto.ts create mode 100644 shared/lib/contexts/sales/application/dto/IListDealers.dto/index.ts create mode 100644 shared/lib/contexts/sales/application/dto/UpdateDealer.dto/IUpdateDealer_Request.dto.ts create mode 100644 shared/lib/contexts/sales/application/dto/UpdateDealer.dto/IUpdateDealer_Response.dto.ts create mode 100644 shared/lib/contexts/sales/application/dto/UpdateDealer.dto/index.ts create mode 100644 shared/lib/contexts/sales/application/dto/index.ts create mode 100644 shared/lib/contexts/sales/application/index.ts create mode 100644 shared/lib/contexts/sales/index.ts delete mode 100644 shared/tsconfig.json diff --git a/.prettierc.json b/.prettierc.json deleted file mode 100644 index 59b8ce8..0000000 --- a/.prettierc.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "printWidth": 130, - "tabWidth": 4, - "useTabs": false, - "semi": true, - "singleQuote": false, - "trailingComma": "all", - "bracketSpacing": true, - "jsxBracketSameLine": true, - "arrowParens": "always" -} diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..91a4315 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,13 @@ +{ + "bracketSpacing": true, + "useTabs": false, + "printWidth": 100, + "tabWidth": 2, + "semi": true, + "singleQuote": false, + "trailingComma": "es5", + "jsxSingleQuote": true, + "jsxBracketSameLine": false, + "arrowParens": "always", + "rcVerbose": true +} diff --git a/.vscode/settings.json b/.vscode/settings.json index e960ecf..d8c815a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,10 @@ "source.organizeImports": "explicit", "source.fixAll.eslint": "explicit" }, - "editor.formatOnSave": true + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.formatOnPaste": false, + "prettier.useEditorConfig": false, + "prettier.useTabs": false, + "prettier.configPath": ".prettierrc" } diff --git a/server/src/contexts/auth/infrastructure/express/passport/authMiddleware.ts b/server/src/contexts/auth/infrastructure/express/passport/authMiddleware.ts index 3ac1c35..977a5d8 100644 --- a/server/src/contexts/auth/infrastructure/express/passport/authMiddleware.ts +++ b/server/src/contexts/auth/infrastructure/express/passport/authMiddleware.ts @@ -1,16 +1,12 @@ import { AuthUser } from "@/contexts/auth/domain"; -import { generateExpressErrorResponse } from "@/contexts/common/infrastructure/express/ExpressErrorResponse"; +import { generateExpressError } from "@/contexts/common/infrastructure/express/ExpressErrorResponse"; import Express from "express"; import httpStatus from "http-status"; import passport from "passport"; function compose(middlewareArray: any[]) { if (!middlewareArray.length) { - return function ( - req: Express.Request, - res: Express.Response, - next: Express.NextFunction, - ) { + return function (req: Express.Request, res: Express.Response, next: Express.NextFunction) { next(); }; } @@ -18,11 +14,7 @@ function compose(middlewareArray: any[]) { const head = middlewareArray[0]; const tail = middlewareArray.slice(1); - return function ( - req: Express.Request, - res: Express.Response, - next: Express.NextFunction, - ) { + return function (req: Express.Request, res: Express.Response, next: Express.NextFunction) { head(req, res, function (err: unknown) { if (err) return next(err); compose(tail)(req, res, next); @@ -37,7 +29,7 @@ export const isLoggedUser = compose([ (req: Express.Request, res: Express.Response, next: Express.NextFunction) => { const user = req.user; if (!user.isUser) { - return generateExpressErrorResponse(req, res, httpStatus.UNAUTHORIZED); + return generateExpressError(req, res, httpStatus.UNAUTHORIZED); } next(); }, @@ -48,7 +40,7 @@ export const isAdminUser = compose([ (req: Express.Request, res: Express.Response, next: Express.NextFunction) => { const user = req.user; if (!user.isAdmin) { - return generateExpressErrorResponse(req, res, httpStatus.UNAUTHORIZED); + return generateExpressError(req, res, httpStatus.UNAUTHORIZED); } next(); }, diff --git a/server/src/contexts/auth/infrastructure/mappers/authuser.mapper.ts b/server/src/contexts/auth/infrastructure/mappers/authuser.mapper.ts index 794f3c0..cbc25ac 100644 --- a/server/src/contexts/auth/infrastructure/mappers/authuser.mapper.ts +++ b/server/src/contexts/auth/infrastructure/mappers/authuser.mapper.ts @@ -7,19 +7,19 @@ import { Email, Name, UniqueID } from "@shared/contexts"; import { AuthUser, IAuthUserProps } from "../../domain/entities"; import { IAuthContext } from "../Auth.context"; import { + AuthUserCreationAttributes, AuthUser_Model, - TCreationUser_Attributes, } from "../sequelize/authUser.model"; export interface IUserMapper extends ISequelizeMapper< AuthUser_Model, - TCreationUser_Attributes, + AuthUserCreationAttributes, AuthUser > {} class AuthUserMapper - extends SequelizeMapper + extends SequelizeMapper implements IUserMapper { public constructor(props: { context: IAuthContext }) { diff --git a/server/src/contexts/auth/infrastructure/sequelize/authUser.model.ts b/server/src/contexts/auth/infrastructure/sequelize/authUser.model.ts index fdc659d..31d8e42 100644 --- a/server/src/contexts/auth/infrastructure/sequelize/authUser.model.ts +++ b/server/src/contexts/auth/infrastructure/sequelize/authUser.model.ts @@ -6,7 +6,8 @@ import { Sequelize, } from "sequelize"; -export type TCreationUser_Attributes = InferCreationAttributes; +export type AuthUserCreationAttributes = + InferCreationAttributes; export class AuthUser_Model extends Model< InferAttributes, diff --git a/server/src/contexts/catalog/infrastructure/express/controllers/listArticles/index.ts b/server/src/contexts/catalog/infrastructure/express/controllers/listArticles/index.ts index 5ebed19..53945db 100644 --- a/server/src/contexts/catalog/infrastructure/express/controllers/listArticles/index.ts +++ b/server/src/contexts/catalog/infrastructure/express/controllers/listArticles/index.ts @@ -4,7 +4,7 @@ import { registerCatalogRepository } from "../../../Catalog.repository"; import { ListArticlesController } from "./ListArticlesController"; import { listArticlesPresenter } from "./presenter"; -export const createListArticlesController = (context: ICatalogContext) => { +export const listArticlesController = (context: ICatalogContext) => { registerCatalogRepository(context); return new ListArticlesController( @@ -12,6 +12,6 @@ export const createListArticlesController = (context: ICatalogContext) => { useCase: new ListArticlesUseCase(context), presenter: listArticlesPresenter, }, - context, + context ); }; diff --git a/server/src/contexts/catalog/infrastructure/express/routes.ts b/server/src/contexts/catalog/infrastructure/express/routes.ts index 58623aa..c79255d 100644 --- a/server/src/contexts/catalog/infrastructure/express/routes.ts +++ b/server/src/contexts/catalog/infrastructure/express/routes.ts @@ -1,6 +1,6 @@ import { applyMiddleware } from "@/contexts/common/infrastructure/express"; import Express from "express"; -import { createListArticlesController } from "./controllers"; +import { listArticlesController } from "./controllers"; /*catalogRoutes.get( "/:articleId", @@ -15,11 +15,7 @@ export const CatalogRouter = (appRouter: Express.Router) => { "/", applyMiddleware("isLoggedUser"), (req: Express.Request, res: Express.Response, next: Express.NextFunction) => - createListArticlesController(res.locals["context"]).execute( - req, - res, - next, - ), + listArticlesController(res.locals["context"]).execute(req, res, next) ); appRouter.use("/catalog", catalogRoutes); diff --git a/server/src/contexts/catalog/infrastructure/mappers/article.mapper.ts b/server/src/contexts/catalog/infrastructure/mappers/article.mapper.ts index bb6a99e..c0c2085 100644 --- a/server/src/contexts/catalog/infrastructure/mappers/article.mapper.ts +++ b/server/src/contexts/catalog/infrastructure/mappers/article.mapper.ts @@ -16,19 +16,15 @@ import { IArticleProps, } from "../../domain/entities"; import { + ArticleCreationAttributes, Article_Model, - TCreationArticle_Attributes, } from "../sequelize/article.model"; export interface IArticleMapper - extends ISequelizeMapper< - Article_Model, - TCreationArticle_Attributes, - Article - > {} + extends ISequelizeMapper {} class ArticleMapper - extends SequelizeMapper + extends SequelizeMapper implements IArticleMapper { public constructor(props: { context: ICatalogContext }) { diff --git a/server/src/contexts/catalog/infrastructure/sequelize/article.model.ts b/server/src/contexts/catalog/infrastructure/sequelize/article.model.ts index 6b51f37..98e89ef 100644 --- a/server/src/contexts/catalog/infrastructure/sequelize/article.model.ts +++ b/server/src/contexts/catalog/infrastructure/sequelize/article.model.ts @@ -8,8 +8,7 @@ import { Sequelize, } from "sequelize"; -export type TCreationArticle_Attributes = - InferCreationAttributes; +export type ArticleCreationAttributes = InferCreationAttributes; export class Article_Model extends Model< InferAttributes, diff --git a/server/src/contexts/common/infrastructure/express/ExpressController.ts b/server/src/contexts/common/infrastructure/express/ExpressController.ts index f0a034a..acdc645 100644 --- a/server/src/contexts/common/infrastructure/express/ExpressController.ts +++ b/server/src/contexts/common/infrastructure/express/ExpressController.ts @@ -5,7 +5,7 @@ import { URL } from "url"; import { IServerError } from "../../domain/errors"; import { IController } from "../Controller.interface"; import { InfrastructureError } from "../InfrastructureError"; -import { generateExpressErrorResponse } from "./ExpressErrorResponse"; +import { generateExpressError } from "./ExpressErrorResponse"; export abstract class ExpressController implements IController { protected req: express.Request; @@ -17,19 +17,13 @@ export abstract class ExpressController implements IController { protected abstract executeImpl(): Promise; - public execute( - req: express.Request, - res: express.Response, - next: express.NextFunction, - ): void { + public execute(req: express.Request, res: express.Response, next: express.NextFunction): void { this.req = req; this.res = res; this.next = next; this.serverURL = `${ - new URL( - `${this.req.protocol}://${this.req.get("host")}${this.req.originalUrl}`, - ).origin + new URL(`${this.req.protocol}://${this.req.get("host")}${this.req.originalUrl}`).origin }/api/v1`; this.file = this.req && this.req["file"]; // <-- ???? @@ -100,11 +94,7 @@ export abstract class ExpressController implements IController { } public internalServerError(message?: string, error?: IServerError) { - return this._errorResponse( - httpStatus.INTERNAL_SERVER_ERROR, - message, - error, - ); + return this._errorResponse(httpStatus.INTERNAL_SERVER_ERROR, message, error); } public todoError(message?: string) { @@ -115,24 +105,15 @@ export abstract class ExpressController implements IController { return this._errorResponse(httpStatus.SERVICE_UNAVAILABLE, message); } - private _jsonResponse( - statusCode: number, - jsonPayload: any, - ): express.Response { + private _jsonResponse(statusCode: number, jsonPayload: any): express.Response { return this.res.status(statusCode).json(jsonPayload).send(); } private _errorResponse( statusCode: number, message?: string, - error?: Error | InfrastructureError, + error?: Error | InfrastructureError ): express.Response { - return generateExpressErrorResponse( - this.req, - this.res, - statusCode, - message, - error, - ); + return generateExpressError(this.req, this.res, statusCode, message, error); } } diff --git a/server/src/contexts/common/infrastructure/express/ExpressErrorResponse.ts b/server/src/contexts/common/infrastructure/express/ExpressErrorResponse.ts index deb019d..19d1e24 100644 --- a/server/src/contexts/common/infrastructure/express/ExpressErrorResponse.ts +++ b/server/src/contexts/common/infrastructure/express/ExpressErrorResponse.ts @@ -1,18 +1,15 @@ -import { - IErrorExtra_Response_DTO, - IError_Response_DTO, -} from "@shared/contexts"; +import { IErrorExtra_Response_DTO, IError_Response_DTO } from "@shared/contexts"; import Express from "express"; import { UseCaseError } from "../../application"; import { InfrastructureError } from "../InfrastructureError"; import { ProblemDocument, ProblemDocumentExtension } from "./ProblemDocument"; -export const generateExpressErrorResponse = ( +export const generateExpressError = ( req: Express.Request, res: Express.Response, statusCode: number, message?: string, - error?: Error | InfrastructureError, + error?: Error | InfrastructureError ): Express.Response => { const context = { user: res.locals.user || undefined, @@ -23,7 +20,7 @@ export const generateExpressErrorResponse = ( const extension = new ProblemDocumentExtension({ context, - extra: error ? { ...generateExpressError(error) } : {}, + extra: error ? { ..._extractExtraInfoFromError(error) } : {}, }); const jsonPayload = new ProblemDocument( @@ -32,15 +29,13 @@ export const generateExpressErrorResponse = ( detail: message, instance: req.baseUrl, }, - extension, + extension ); return res.status(statusCode).json(jsonPayload).send(); }; -function generateExpressError( - error: Error | InfrastructureError, -): IErrorExtra_Response_DTO { +function _extractExtraInfoFromError(error: Error | InfrastructureError): IErrorExtra_Response_DTO { const useCaseError = error as UseCaseError; const payload = Array.isArray(useCaseError.payload) diff --git a/server/src/contexts/common/infrastructure/mappers/SequelizeMapper.ts b/server/src/contexts/common/infrastructure/mappers/SequelizeMapper.ts index ee4a492..03c26ec 100644 --- a/server/src/contexts/common/infrastructure/mappers/SequelizeMapper.ts +++ b/server/src/contexts/common/infrastructure/mappers/SequelizeMapper.ts @@ -1,37 +1,27 @@ import { Collection, Entity, ICollection, Result } from "@shared/contexts"; import { Model, ValidationError } from "sequelize"; -import { - FieldValueError, - RequiredFieldMissingError, -} from "../../domain/errors"; +import { FieldValueError, RequiredFieldMissingError } from "../../domain/errors"; import { InfrastructureError } from "../InfrastructureError"; +export type MapperParamsType = Record; + export interface ISequelizeMapper< TModel extends Model, TModelAttributes, TEntity extends Entity, > { - mapToDomain(source: TModel, params?: Record): TEntity; - - mapArrayToDomain( - source: TModel[], - params?: Record - ): Collection; - + mapToDomain(source: TModel, params?: MapperParamsType): TEntity; + mapArrayToDomain(source: TModel[], params?: MapperParamsType): Collection; mapArrayAndCountToDomain( source: TModel[], totalCount: number, - params?: Record + params?: MapperParamsType ): Collection; - mapToPersistence( - source: TEntity, - params?: Record - ): TModelAttributes; - + mapToPersistence(source: TEntity, params?: MapperParamsType): TModelAttributes; mapCollectionToPersistence( source: ICollection, - params?: Record + params?: MapperParamsType ): TModelAttributes[]; } @@ -43,64 +33,49 @@ export abstract class SequelizeMapper< { public constructor(protected props: any) {} - public mapToDomain(source: TModel, params?: Record): TEntity { + public mapToDomain(source: TModel, params?: MapperParamsType): TEntity { return this.toDomainMappingImpl(source, params); } - public mapArrayToDomain( - source: TModel[], - params?: Record - ): Collection { - return this.mapArrayAndCountToDomain( - source, - source ? source.length : 0, - params - ); + public mapArrayToDomain(source: TModel[], params?: MapperParamsType): Collection { + return this.mapArrayAndCountToDomain(source, source ? source.length : 0, params); } public mapArrayAndCountToDomain( source: TModel[], totalCount: number, - params?: Record + params?: MapperParamsType ): Collection { const items = source - ? source.map((value, index: number) => - this.toDomainMappingImpl!(value, { index, ...params }) - ) + ? source.map((value, index: number) => this.toDomainMappingImpl!(value, { index, ...params })) : []; + return new Collection(items, totalCount); } - public mapToPersistence( - source: TEntity, - params?: Record - ): TModelAttributes { + public mapToPersistence(source: TEntity, params?: MapperParamsType): TModelAttributes { return this.toPersistenceMappingImpl(source, params); } public mapCollectionToPersistence( source: ICollection, - params?: Record + params?: MapperParamsType ): TModelAttributes[] { return source.items.map((value: TEntity, index: number) => this.toPersistenceMappingImpl!(value, { index, ...params }) ); } - protected toDomainMappingImpl( - source: TModel, - params?: Record - ): TEntity { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protected toDomainMappingImpl(source: TModel, params?: MapperParamsType): TEntity { throw InfrastructureError.create( InfrastructureError.UNEXCEPTED_ERROR, 'Method "toDomainMappingImpl" not implemented!' ); } - protected toPersistenceMappingImpl( - source: TEntity, - params?: Record - ): TModelAttributes { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protected toPersistenceMappingImpl(source: TEntity, params?: MapperParamsType): TModelAttributes { throw InfrastructureError.create( InfrastructureError.UNEXCEPTED_ERROR, 'Method "toPersistenceMappingImpl" not implemented!' @@ -118,20 +93,24 @@ export abstract class SequelizeMapper< protected mapsValue( row: TModel, key: string, - customMapFn: ( - value: any, - params: Record - ) => Result, - params: Record = { + customMapFn: (value: any, params: MapperParamsType) => Result, + params: MapperParamsType = { defaultValue: null, } ) { - let value = params.defaultValue; + const value = row?.dataValues[key] ?? params.defaultValue; + const valueOrError = customMapFn(value, params); + + if (valueOrError.isFailure) { + this._handleFailure(valueOrError.error, key); + } + + return valueOrError.object; + + /*let value = params.defaultValue; if (!row || typeof row !== "object") { - console.debug( - `Data row has not keys! Key ${key} not exists in data row!` - ); + console.debug(`Data row has not keys! Key ${key} not exists in data row!`); } else if (!Object.hasOwn(row.dataValues, key)) { console.debug(`Key ${key} not exists in data row!`); } else { @@ -143,16 +122,46 @@ export abstract class SequelizeMapper< if (valueOrError.isFailure) { this.handleFailure(valueOrError.error, key); } - return valueOrError.object; + return valueOrError.object;*/ } protected mapsAssociation( row: TModel, associationName: string, customMapper: any, - params: Record = {} + params: MapperParamsType = {} ) { - if (!customMapper) { + if (!customMapper) + throw InfrastructureError.create( + InfrastructureError.UNEXCEPTED_ERROR, + 'Custom mapper undefined at "mapsAssociation"!' + ); + + const { filter, ...otherParams } = params; + let associationRows = row?.dataValues[associationName] ?? []; + + if (filter) + associationRows = Array.isArray(associationRows) + ? associationRows.filter(filter) + : filter(associationRows); + + const customMapFn = Array.isArray(associationRows) + ? customMapper.mapArrayToDomain + : customMapper.mapToDomain; + + if (!customMapFn) + throw InfrastructureError.create( + InfrastructureError.UNEXCEPTED_ERROR, + 'Custom mapper function undefined at "mapsAssociation"!' + ); + + const associatedDataOrError = customMapFn(associationRows, otherParams); + + if (associatedDataOrError.isFailure) + this._handleFailure(associatedDataOrError.error, associationName); + return associatedDataOrError.object; + + /*if (!customMapper) { throw InfrastructureError.create( InfrastructureError.UNEXCEPTED_ERROR, 'Custom mapper undefined at "mapsAssociation"!' @@ -195,13 +204,10 @@ export abstract class SequelizeMapper< if (associatedDataOrError.isFailure) { this.handleFailure(associatedDataOrError.error, associationName); } - return associatedDataOrError.object; - - //const associatedData = row[association.accessors.get](); - //return associatedData; + return associatedDataOrError.object;*/ } - private handleFailure(error: Error, key: string) { + private _handleFailure(error: Error, key: string) { if (error instanceof ValidationError) { this.handleInvalidFieldError(key, error); } else { diff --git a/server/src/contexts/sales/application/CreateDealer.useCase.ts b/server/src/contexts/sales/application/CreateDealer.useCase.ts new file mode 100644 index 0000000..5b2eab5 --- /dev/null +++ b/server/src/contexts/sales/application/CreateDealer.useCase.ts @@ -0,0 +1,143 @@ +import { IUseCase, IUseCaseError, UseCaseError } from "@/contexts/common/application"; +import { IRepositoryManager, Password } from "@/contexts/common/domain"; +import { IInfrastructureError } from "@/contexts/common/infrastructure"; +import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize"; +import { + DomainError, + Email, + ICreateDealer_Request_DTO, + IDomainError, + Name, + Result, + UniqueID, + ensureIdIsValid, + ensureNameIsValid, +} from "@shared/contexts"; +import { Dealer, IDealerRepository } from "../domain"; + +export type CreateDealerResponseOrError = + | Result // Misc errors (value objects) + | Result; // Success! + +export class CreateDealerUseCase + implements IUseCase> +{ + private _adapter: ISequelizeAdapter; + private _repositoryManager: IRepositoryManager; + + constructor(props: { adapter: ISequelizeAdapter; repositoryManager: IRepositoryManager }) { + this._adapter = props.adapter; + this._repositoryManager = props.repositoryManager; + } + + async execute(request: ICreateDealer_Request_DTO) { + const { id, name } = request; + + // Validaciones de datos + const idOrError = ensureIdIsValid(id); + if (idOrError.isFailure) { + const message = idOrError.error.message; //`Dealer ID ${dealerDTO.id} is not valid`; + return Result.fail( + UseCaseError.create(UseCaseError.INVALID_INPUT_DATA, message, [{ path: "id" }]) + ); + } + + const nameOrError = ensureNameIsValid(name); + if (nameOrError.isFailure) { + const message = nameOrError.error.message; //`Dealer ID ${dealerDTO.id} is not valid`; + return Result.fail( + UseCaseError.create(UseCaseError.INVALID_INPUT_DATA, message, [{ path: "name" }]) + ); + } + + // Comprobar que no existe un usuario previo con esos datos + const dealerRepository = this._getDealerRepository(); + + const idExists = await dealerRepository().exists(idOrError.object); + if (idExists) { + const message = `Another dealer with same ID exists`; + return Result.fail( + UseCaseError.create(UseCaseError.RESOURCE_ALREADY_EXITS, message, { + path: "id", + }) + ); + } + + // Crear dealer + const dealerOrError = this._tryCreateDealerInstance(request, idOrError.object); + + if (dealerOrError.isFailure) { + const { error: domainError } = dealerOrError; + let errorCode = ""; + let message = ""; + + switch (domainError.code) { + case DomainError.INVALID_INPUT_DATA: + errorCode = UseCaseError.INVALID_INPUT_DATA; + message = "El usuario tiene algún dato erróneo."; + break; + + default: + errorCode = UseCaseError.UNEXCEPTED_ERROR; + message = domainError.message; + break; + } + + return Result.fail(UseCaseError.create(errorCode, message, domainError)); + } + + return this._saveDealer(dealerOrError.object); + } + + private async _saveDealer(dealer: Dealer) { + // Guardar el contacto + const transaction = this._adapter.startTransaction(); + const dealerRepository = this._getDealerRepository(); + let dealerRepo: IDealerRepository; + + try { + await transaction.complete(async (t) => { + dealerRepo = dealerRepository({ transaction: t }); + await dealerRepo.create(dealer); + }); + + return Result.ok(dealer); + } catch (error: unknown) { + const _error = error as IInfrastructureError; + return Result.fail(UseCaseError.create(UseCaseError.REPOSITORY_ERROR, _error.message)); + } + } + + private _tryCreateDealerInstance( + dealerDTO: ICreateDealer_Request_DTO, + dealerId: UniqueID + ): Result { + const nameOrError = Name.create(dealerDTO.name); + if (nameOrError.isFailure) { + return Result.fail(nameOrError.error); + } + + const emailOrError = Email.create(dealerDTO.email); + if (emailOrError.isFailure) { + return Result.fail(emailOrError.error); + } + + const passwordOrError = Password.createFromPlainTextPassword(dealerDTO.password); + if (passwordOrError.isFailure) { + return Result.fail(passwordOrError.error); + } + + return Dealer.create( + { + name: nameOrError.object, + email: emailOrError.object, + password: passwordOrError.object, + }, + dealerId + ); + } + + private _getDealerRepository() { + return this._repositoryManager.getRepository("Dealer"); + } +} diff --git a/server/src/contexts/sales/application/DeleteDealer.useCase.ts b/server/src/contexts/sales/application/DeleteDealer.useCase.ts new file mode 100644 index 0000000..5c6ab13 --- /dev/null +++ b/server/src/contexts/sales/application/DeleteDealer.useCase.ts @@ -0,0 +1,55 @@ +import { + IUseCase, + IUseCaseError, + IUseCaseRequest, + UseCaseError, +} from "@/contexts/common/application/useCases"; +import { IRepositoryManager } from "@/contexts/common/domain"; +import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize"; +import { Result, UniqueID } from "@shared/contexts"; +import { IDealerRepository } from "../domain"; + +export interface IDeleteDealerUseCaseRequest extends IUseCaseRequest { + id: UniqueID; +} + +export type DeleteDealerResponseOrError = + | Result // Misc errors (value objects) + | Result; // Success! + +export class DeleteDealerUseCase + implements IUseCase> +{ + private _adapter: ISequelizeAdapter; + private _repositoryManager: IRepositoryManager; + + constructor(props: { adapter: ISequelizeAdapter; repositoryManager: IRepositoryManager }) { + this._adapter = props.adapter; + this._repositoryManager = props.repositoryManager; + } + + private getRepositoryByName(name: string) { + return this._repositoryManager.getRepository(name); + } + + async execute(request: IDeleteDealerUseCaseRequest): Promise { + const { id: dealerId } = request; + + const transaction = this._adapter.startTransaction(); + const dealerRepoBuilder = this.getRepositoryByName("Dealer"); + + try { + await transaction.complete(async (t) => { + const invoiceRepo = dealerRepoBuilder({ transaction: t }); + await invoiceRepo.removeById(dealerId); + }); + + return Result.ok(); + } catch (error: unknown) { + //const _error = error as IInfrastructureError; + return Result.fail( + UseCaseError.create(UseCaseError.REPOSITORY_ERROR, "Error al eliminar el usuario") + ); + } + } +} diff --git a/server/src/contexts/sales/application/GetDealer.useCase.ts b/server/src/contexts/sales/application/GetDealer.useCase.ts new file mode 100644 index 0000000..78afe86 --- /dev/null +++ b/server/src/contexts/sales/application/GetDealer.useCase.ts @@ -0,0 +1,71 @@ +import { + IUseCase, + IUseCaseError, + IUseCaseRequest, + UseCaseError, +} from "@/contexts/common/application/useCases"; +import { IRepositoryManager } from "@/contexts/common/domain"; +import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize"; +import { Result, UniqueID } from "@shared/contexts"; +import { IDealerRepository } from "../domain"; + +import { IInfrastructureError } from "@/contexts/common/infrastructure"; +import { Dealer } from "../domain/entities/Dealer"; + +export interface IGetDealerUseCaseRequest extends IUseCaseRequest { + id: UniqueID; +} + +export type GetDealerResponseOrError = + | Result // Misc errors (value objects) + | Result; // Success! + +export class GetDealerUseCase + implements IUseCase> +{ + private _adapter: ISequelizeAdapter; + private _repositoryManager: IRepositoryManager; + + constructor(props: { adapter: ISequelizeAdapter; repositoryManager: IRepositoryManager }) { + this._adapter = props.adapter; + this._repositoryManager = props.repositoryManager; + } + + private getRepositoryByName(name: string) { + return this._repositoryManager.getRepository(name); + } + + async execute(request: IGetDealerUseCaseRequest): Promise { + const { id } = request; + + // Validación de datos + // No hay en este caso + + return await this.findDealer(id); + } + + private async findDealer(id: UniqueID) { + const transaction = this._adapter.startTransaction(); + const dealerRepoBuilder = this.getRepositoryByName("Dealer"); + + let dealer: Dealer | null = null; + + try { + await transaction.complete(async (t) => { + const dealerRepo = dealerRepoBuilder({ transaction: t }); + dealer = await dealerRepo.getById(id); + }); + + if (!dealer) { + return Result.fail(UseCaseError.create(UseCaseError.NOT_FOUND_ERROR, "Dealer not found")); + } + + return Result.ok(dealer!); + } catch (error: unknown) { + const _error = error as IInfrastructureError; + return Result.fail( + UseCaseError.create(UseCaseError.REPOSITORY_ERROR, "Error al consultar el usuario", _error) + ); + } + } +} diff --git a/server/src/contexts/sales/application/ListDealers.useCase.ts b/server/src/contexts/sales/application/ListDealers.useCase.ts new file mode 100644 index 0000000..e599200 --- /dev/null +++ b/server/src/contexts/sales/application/ListDealers.useCase.ts @@ -0,0 +1,57 @@ +import { IUseCase, IUseCaseError, UseCaseError } from "@/contexts/common/application/useCases"; +import { IRepositoryManager } from "@/contexts/common/domain"; +import { Collection, ICollection, IQueryCriteria, Result } from "@shared/contexts"; + +import { IInfrastructureError } from "@/contexts/common/infrastructure"; +import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize"; +import { Dealer } from "../domain"; +import { IDealerRepository } from "../domain/repository"; + +export interface IListDealersParams { + queryCriteria: IQueryCriteria; +} + +export type ListDealersResult = + | Result // Misc errors (value objects) + | Result, never>; // Success! + +export class ListDealersUseCase + implements IUseCase> +{ + private _adapter: ISequelizeAdapter; + private _repositoryManager: IRepositoryManager; + + constructor(props: { adapter: ISequelizeAdapter; repositoryManager: IRepositoryManager }) { + this._adapter = props.adapter; + this._repositoryManager = props.repositoryManager; + } + + private getRepositoryByName(name: string) { + return this._repositoryManager.getRepository(name); + } + + async execute(params: Partial): Promise { + const { queryCriteria } = params; + + return this.findDealers(queryCriteria); + } + + private async findDealers(queryCriteria) { + const transaction = this._adapter.startTransaction(); + const dealerRepoBuilder = this.getRepositoryByName("Dealer"); + + let dealers: ICollection = new Collection(); + + try { + await transaction.complete(async (t) => { + dealers = await dealerRepoBuilder({ transaction: t }).findAll(queryCriteria); + }); + return Result.ok(dealers); + } catch (error: unknown) { + const _error = error as IInfrastructureError; + return Result.fail( + UseCaseError.create(UseCaseError.REPOSITORY_ERROR, "Error al listar los usurios", _error) + ); + } + } +} diff --git a/server/src/contexts/sales/application/UpdateDealer.useCase.ts b/server/src/contexts/sales/application/UpdateDealer.useCase.ts new file mode 100644 index 0000000..6699d6e --- /dev/null +++ b/server/src/contexts/sales/application/UpdateDealer.useCase.ts @@ -0,0 +1,134 @@ +import { + IUseCase, + IUseCaseError, + IUseCaseRequest, + UseCaseError, +} from "@/contexts/common/application"; +import { IRepositoryManager, Password } from "@/contexts/common/domain"; +import { IInfrastructureError } from "@/contexts/common/infrastructure"; +import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize"; +import { + DomainError, + Email, + IDomainError, + IUpdateDealer_Request_DTO, + Name, + Result, + UniqueID, +} from "@shared/contexts"; +import { Dealer, IDealerRepository } from "../domain"; + +export interface IUpdateDealerUseCaseRequest extends IUseCaseRequest { + id: UniqueID; + dealerDTO: IUpdateDealer_Request_DTO; +} + +export type UpdateDealerResponseOrError = + | Result // Misc errors (value objects) + | Result; // Success! + +export class UpdateDealerUseCase + implements IUseCase> +{ + private _adapter: ISequelizeAdapter; + private _repositoryManager: IRepositoryManager; + + constructor(props: { adapter: ISequelizeAdapter; repositoryManager: IRepositoryManager }) { + this._adapter = props.adapter; + this._repositoryManager = props.repositoryManager; + } + + async execute(request: IUpdateDealerUseCaseRequest): Promise { + const { id, dealerDTO } = request; + const dealerRepository = this._getDealerRepository(); + + // Comprobar que existe el dealer + const idExists = await dealerRepository().exists(id); + if (!idExists) { + const message = `Dealer ID not found`; + return Result.fail( + UseCaseError.create(UseCaseError.NOT_FOUND_ERROR, message, { + path: "id", + }) + ); + } + + // Crear usuario + const dealerOrError = this._tryCreateDealerInstance(dealerDTO, id); + + if (dealerOrError.isFailure) { + const { error: domainError } = dealerOrError; + let errorCode = ""; + let message = ""; + + switch (domainError.code) { + // Errores manuales + case DomainError.INVALID_INPUT_DATA: + errorCode = UseCaseError.INVALID_INPUT_DATA; + message = "El usuario tiene algún dato erróneo."; + break; + + default: + errorCode = UseCaseError.UNEXCEPTED_ERROR; + message = domainError.message; + break; + } + + return Result.fail(UseCaseError.create(errorCode, message, domainError)); + } + + return this._updateDealer(dealerOrError.object); + } + + private async _updateDealer(dealer: Dealer) { + // Guardar el contacto + const transaction = this._adapter.startTransaction(); + const dealerRepository = this._getDealerRepository(); + let dealerRepo: IDealerRepository; + + try { + await transaction.complete(async (t) => { + dealerRepo = dealerRepository({ transaction: t }); + await dealerRepo.update(dealer); + }); + + return Result.ok(dealer); + } catch (error: unknown) { + const _error = error as IInfrastructureError; + return Result.fail(UseCaseError.create(UseCaseError.REPOSITORY_ERROR, _error.message)); + } + } + + private _tryCreateDealerInstance( + dealerDTO: IUpdateDealer_Request_DTO, + dealerId: UniqueID + ): Result { + const nameOrError = Name.create(dealerDTO.name); + if (nameOrError.isFailure) { + return Result.fail(nameOrError.error); + } + + const emailOrError = Email.create(dealerDTO.email); + if (emailOrError.isFailure) { + return Result.fail(emailOrError.error); + } + + const passwordOrError = Password.createFromPlainTextPassword(dealerDTO.password); + if (passwordOrError.isFailure) { + return Result.fail(passwordOrError.error); + } + + return Dealer.create( + { + name: nameOrError.object, + email: emailOrError.object, + password: passwordOrError.object, + }, + dealerId + ); + } + + private _getDealerRepository() { + return this._repositoryManager.getRepository("Dealer"); + } +} diff --git a/server/src/contexts/sales/application/index.ts b/server/src/contexts/sales/application/index.ts new file mode 100644 index 0000000..76b60f6 --- /dev/null +++ b/server/src/contexts/sales/application/index.ts @@ -0,0 +1,5 @@ +export * from "./CreateDealer.useCase"; +export * from "./DeleteDealer.useCase"; +export * from "./GetDealer.useCase"; +export * from "./ListDealers.useCase"; +export * from "./UpdateDealer.useCase"; diff --git a/server/src/contexts/sales/domain/entities/Dealer.ts b/server/src/contexts/sales/domain/entities/Dealer.ts new file mode 100644 index 0000000..7bcb351 --- /dev/null +++ b/server/src/contexts/sales/domain/entities/Dealer.ts @@ -0,0 +1,21 @@ +import { AggregateRoot, IDomainError, Name, Result, UniqueID } from "@shared/contexts"; + +export interface IDealerProps { + name: Name; +} + +export interface IDealer { + id: UniqueID; + name: Name; +} + +export class Dealer extends AggregateRoot implements IDealer { + public static create(props: IDealerProps, id?: UniqueID): Result { + const user = new Dealer(props, id); + return Result.ok(user); + } + + get name(): Name { + return this.props.name; + } +} diff --git a/server/src/contexts/sales/domain/entities/index.ts b/server/src/contexts/sales/domain/entities/index.ts new file mode 100644 index 0000000..2c95664 --- /dev/null +++ b/server/src/contexts/sales/domain/entities/index.ts @@ -0,0 +1 @@ +export * from "./Dealer"; diff --git a/server/src/contexts/sales/domain/index.ts b/server/src/contexts/sales/domain/index.ts new file mode 100644 index 0000000..6347a2b --- /dev/null +++ b/server/src/contexts/sales/domain/index.ts @@ -0,0 +1,2 @@ +export * from "./entities"; +export * from "./repository"; diff --git a/server/src/contexts/sales/domain/repository/DealerRepository.interface.ts b/server/src/contexts/sales/domain/repository/DealerRepository.interface.ts new file mode 100644 index 0000000..f0e9d8a --- /dev/null +++ b/server/src/contexts/sales/domain/repository/DealerRepository.interface.ts @@ -0,0 +1,14 @@ +import { IRepository } from "@/contexts/common/domain"; +import { ICollection, IQueryCriteria, UniqueID } from "@shared/contexts"; +import { Dealer } from "../entities"; + +export interface IDealerRepository extends IRepository { + exists(id: UniqueID): Promise; + create(dealer: Dealer): Promise; + update(dealer: Dealer): Promise; + + getById(id: UniqueID): Promise; + findAll(queryCriteria?: IQueryCriteria): Promise>; + + removeById(id: UniqueID): Promise; +} diff --git a/server/src/contexts/sales/domain/repository/index.ts b/server/src/contexts/sales/domain/repository/index.ts new file mode 100644 index 0000000..81580e3 --- /dev/null +++ b/server/src/contexts/sales/domain/repository/index.ts @@ -0,0 +1 @@ +export * from "./DealerRepository.interface"; diff --git a/server/src/contexts/sales/infrastructure/Dealer.repository.ts b/server/src/contexts/sales/infrastructure/Dealer.repository.ts new file mode 100644 index 0000000..d0d3fe1 --- /dev/null +++ b/server/src/contexts/sales/infrastructure/Dealer.repository.ts @@ -0,0 +1,85 @@ +import { ISequelizeAdapter, SequelizeRepository } from "@/contexts/common/infrastructure/sequelize"; +import { ICollection, IQueryCriteria, UniqueID } from "@shared/contexts"; +import { Transaction } from "sequelize"; + +import { IDealerRepository } from "../domain"; +import { Dealer } from "../domain/entities"; +import { ISalesContext } from "./Sales.context"; +import { IDealerMapper, createDealerMapper } from "./mappers"; + +export type QueryParams = { + pagination: Record; + filters: Record; +}; + +export class DealerRepository extends SequelizeRepository implements IDealerRepository { + protected mapper: IDealerMapper; + + public constructor(props: { + mapper: IDealerMapper; + adapter: ISequelizeAdapter; + transaction: Transaction; + }) { + const { adapter, mapper, transaction } = props; + super({ adapter, transaction }); + this.mapper = mapper; + } + + public async exists(id: UniqueID): Promise { + return this._exists("Dealer_Model", "id", id.toPrimitive()); + } + + public async create(user: Dealer): Promise { + const userData = this.mapper.mapToPersistence(user); + await this._save("Dealer_Model", user.id, userData); + } + + public async update(user: Dealer): Promise { + const userData = this.mapper.mapToPersistence(user); + + // borrando y luego creando + // await this.removeById(user.id, true); + await this._save("Dealer_Model", user.id, userData, {}); + } + + public async getById(id: UniqueID): Promise { + const rawDealer: any = await this._getById("Dealer_Model", id); + + if (!rawDealer === true) { + return null; + } + + return this.mapper.mapToDomain(rawDealer); + } + + public async findAll(queryCriteria?: IQueryCriteria): Promise> { + const { rows, count } = await this._findAll( + "Dealer_Model", + queryCriteria + /*{ + include: [], // esto es para quitar las asociaciones al hacer la consulta + }*/ + ); + + return this.mapper.mapArrayAndCountToDomain(rows, count); + } + + public async removeById(id: UniqueID, force: boolean = false): Promise { + return this._removeById("Dealer_Model", id); + } +} + +export const registerDealerRepository = (context: ISalesContext) => { + const adapter = context.adapter; + const repoManager = context.repositoryManager; + + repoManager.registerRepository("Dealer", (params = { transaction: null }) => { + const { transaction } = params; + + return new DealerRepository({ + transaction, + adapter, + mapper: createDealerMapper(context), + }); + }); +}; diff --git a/server/src/contexts/sales/infrastructure/Sales.context.ts b/server/src/contexts/sales/infrastructure/Sales.context.ts new file mode 100644 index 0000000..b013360 --- /dev/null +++ b/server/src/contexts/sales/infrastructure/Sales.context.ts @@ -0,0 +1,32 @@ +import { IRepositoryManager, RepositoryManager } from "@/contexts/common/domain"; +import { + ISequelizeAdapter, + createSequelizeAdapter, +} from "@/contexts/common/infrastructure/sequelize"; + +export interface ISalesContext { + adapter: ISequelizeAdapter; + repositoryManager: IRepositoryManager; + //services: IApplicationService; +} + +export class SalesContext { + private static instance: SalesContext | null = null; + + public static getInstance(): ISalesContext { + if (!SalesContext.instance) { + SalesContext.instance = new SalesContext({ + adapter: createSequelizeAdapter(), + repositoryManager: RepositoryManager.getInstance(), + }); + } + + return SalesContext.instance.context; + } + + private context: ISalesContext; + + private constructor(context: ISalesContext) { + this.context = context; + } +} diff --git a/server/src/contexts/sales/infrastructure/express/controllers/createDealer/CreateDealer.controller.ts b/server/src/contexts/sales/infrastructure/express/controllers/createDealer/CreateDealer.controller.ts new file mode 100644 index 0000000..4f033ae --- /dev/null +++ b/server/src/contexts/sales/infrastructure/express/controllers/createDealer/CreateDealer.controller.ts @@ -0,0 +1,119 @@ +import { IUseCaseError, UseCaseError } from "@/contexts/common/application"; +import { IServerError } from "@/contexts/common/domain/errors"; +import { IInfrastructureError, InfrastructureError } from "@/contexts/common/infrastructure"; +import { ExpressController } from "@/contexts/common/infrastructure/express"; +import { CreateDealerUseCase } from "@/contexts/sales/application"; +import { Dealer } from "@/contexts/sales/domain/entities"; +import { + ICreateDealer_Request_DTO, + ICreateDealer_Response_DTO, + ensureCreateDealer_Request_DTOIsValid, +} from "@shared/contexts"; +import { ISalesContext } from "../../../Sales.context"; +import { ICreateDealerPresenter } from "./presenter"; + +export class CreateDealerController extends ExpressController { + private useCase: CreateDealerUseCase; + private presenter: ICreateDealerPresenter; + private context: ISalesContext; + + constructor( + props: { + useCase: CreateDealerUseCase; + presenter: ICreateDealerPresenter; + }, + context: ISalesContext + ) { + super(); + + const { useCase, presenter } = props; + this.useCase = useCase; + this.presenter = presenter; + this.context = context; + } + + async executeImpl() { + try { + const dealerDTO: ICreateDealer_Request_DTO = this.req.body; + + // Validaciones de DTO + const dealerDTOOrError = ensureCreateDealer_Request_DTOIsValid(dealerDTO); + + if (dealerDTOOrError.isFailure) { + const errorMessage = "Dealer data not valid"; + const infraError = InfrastructureError.create( + InfrastructureError.INVALID_INPUT_DATA, + errorMessage, + dealerDTOOrError.error + ); + return this.invalidInputError(errorMessage, infraError); + } + + // Llamar al caso de uso + const result = await this.useCase.execute(dealerDTO); + + if (result.isFailure) { + return this._handleExecuteError(result.error); + } + + const dealer = result.object; + + return this.created(this.presenter.map(dealer, this.context)); + } catch (e: unknown) { + return this.fail(e as IServerError); + } + } + + private _handleExecuteError(error: IUseCaseError) { + let errorMessage: string; + let infraError: IInfrastructureError; + + switch (error.code) { + case UseCaseError.INVALID_INPUT_DATA: + errorMessage = "Dealer data not valid"; + infraError = InfrastructureError.create( + InfrastructureError.INVALID_INPUT_DATA, + errorMessage, + error + ); + return this.invalidInputError(errorMessage, infraError); + break; + + case UseCaseError.RESOURCE_ALREADY_EXITS: + errorMessage = "Dealer already exists"; + + infraError = InfrastructureError.create( + InfrastructureError.RESOURCE_ALREADY_REGISTERED, + errorMessage, + error + ); + return this.conflictError(errorMessage, infraError); + break; + + case UseCaseError.REPOSITORY_ERROR: + errorMessage = "Error saving dealer"; + infraError = InfrastructureError.create( + InfrastructureError.UNEXCEPTED_ERROR, + errorMessage, + error + ); + return this.conflictError(errorMessage, infraError); + break; + + case UseCaseError.UNEXCEPTED_ERROR: + errorMessage = error.message; + + infraError = InfrastructureError.create( + InfrastructureError.UNEXCEPTED_ERROR, + errorMessage, + error + ); + return this.internalServerError(errorMessage, infraError); + break; + + default: + errorMessage = error.message; + return this.clientError(errorMessage); + } + } +} diff --git a/server/src/contexts/sales/infrastructure/express/controllers/createDealer/index.ts b/server/src/contexts/sales/infrastructure/express/controllers/createDealer/index.ts new file mode 100644 index 0000000..b65684d --- /dev/null +++ b/server/src/contexts/sales/infrastructure/express/controllers/createDealer/index.ts @@ -0,0 +1,23 @@ +import { CreateDealerUseCase } from "@/contexts/sales/application"; +import Express from "express"; +import { registerDealerRepository } from "../../../Dealer.repository"; +import { ISalesContext } from "../../../Sales.context"; +import { CreateDealerController } from "./CreateDealer.controller"; +import { CreateDealerPresenter } from "./presenter"; + +export const createDealerController = ( + req: Express.Request, + res: Express.Response, + next: Express.NextFunction +) => { + const context: ISalesContext = res.locals.context; + + registerDealerRepository(context); + return new CreateDealerController( + { + useCase: new CreateDealerUseCase(context), + presenter: CreateDealerPresenter, + }, + context + ).execute(req, res, next); +}; diff --git a/server/src/contexts/sales/infrastructure/express/controllers/createDealer/presenter/CreateDealer.presenter.ts b/server/src/contexts/sales/infrastructure/express/controllers/createDealer/presenter/CreateDealer.presenter.ts new file mode 100644 index 0000000..bc0d0d2 --- /dev/null +++ b/server/src/contexts/sales/infrastructure/express/controllers/createDealer/presenter/CreateDealer.presenter.ts @@ -0,0 +1,16 @@ +import { Dealer } from "@/contexts/sales/domain/entities"; +import { ISalesContext } from "@/contexts/sales/infrastructure/Sales.context"; +import { ICreateDealer_Response_DTO } from "@shared/contexts"; + +export interface ICreateDealerPresenter { + map: (dealer: Dealer, context: ISalesContext) => ICreateDealer_Response_DTO; +} + +export const CreateDealerPresenter: ICreateDealerPresenter = { + map: (dealer: Dealer, context: ISalesContext): ICreateDealer_Response_DTO => { + return { + id: dealer.id.toString(), + name: dealer.name.toString(), + }; + }, +}; diff --git a/server/src/contexts/sales/infrastructure/express/controllers/createDealer/presenter/index.ts b/server/src/contexts/sales/infrastructure/express/controllers/createDealer/presenter/index.ts new file mode 100644 index 0000000..dcb0b70 --- /dev/null +++ b/server/src/contexts/sales/infrastructure/express/controllers/createDealer/presenter/index.ts @@ -0,0 +1 @@ +export * from "./CreateDealer.presenter"; diff --git a/server/src/contexts/sales/infrastructure/express/controllers/deleteDealer/DeleteDealer.controller.ts b/server/src/contexts/sales/infrastructure/express/controllers/deleteDealer/DeleteDealer.controller.ts new file mode 100644 index 0000000..262ada3 --- /dev/null +++ b/server/src/contexts/sales/infrastructure/express/controllers/deleteDealer/DeleteDealer.controller.ts @@ -0,0 +1,84 @@ +import { IUseCaseError, UseCaseError } from "@/contexts/common/application/useCases"; +import { IServerError } from "@/contexts/common/domain/errors"; +import { IInfrastructureError, InfrastructureError } from "@/contexts/common/infrastructure"; +import { ExpressController } from "@/contexts/common/infrastructure/express"; +import { DeleteDealerUseCase } from "@/contexts/sales/application"; +import { ensureIdIsValid } from "@shared/contexts"; +import { ISalesContext } from "../../../Sales.context"; + +export class DeleteDealerController extends ExpressController { + private useCase: DeleteDealerUseCase; + private context: ISalesContext; + + constructor(props: { useCase: DeleteDealerUseCase }, context: ISalesContext) { + super(); + + const { useCase } = props; + this.useCase = useCase; + this.context = context; + } + + async executeImpl(): Promise { + try { + const { dealerId } = this.req.params; + + // Validar ID + const dealerIdOrError = ensureIdIsValid(dealerId); + if (dealerIdOrError.isFailure) { + const errorMessage = "Dealer ID is not valid"; + const infraError = InfrastructureError.create( + InfrastructureError.INVALID_INPUT_DATA, + errorMessage, + dealerIdOrError.error + ); + return this.invalidInputError(errorMessage, infraError); + } + + // Llamar al caso de uso + const result = await this.useCase.execute({ + id: dealerIdOrError.object, + }); + + if (result.isFailure) { + return this._handleExecuteError(result.error); + } + return this.noContent(); + } catch (e: unknown) { + return this.fail(e as IServerError); + } + } + + private _handleExecuteError(error: IUseCaseError) { + let errorMessage: string; + let infraError: IInfrastructureError; + + switch (error.code) { + case UseCaseError.NOT_FOUND_ERROR: + errorMessage = "Dealer not found"; + + infraError = InfrastructureError.create( + InfrastructureError.RESOURCE_NOT_FOUND_ERROR, + errorMessage, + error + ); + + return this.notFoundError(errorMessage, infraError); + break; + + case UseCaseError.UNEXCEPTED_ERROR: + errorMessage = error.message; + + infraError = InfrastructureError.create( + InfrastructureError.UNEXCEPTED_ERROR, + errorMessage, + error + ); + return this.internalServerError(errorMessage, infraError); + break; + + default: + errorMessage = error.message; + return this.clientError(errorMessage); + } + } +} diff --git a/server/src/contexts/sales/infrastructure/express/controllers/deleteDealer/index.ts b/server/src/contexts/sales/infrastructure/express/controllers/deleteDealer/index.ts new file mode 100644 index 0000000..ef389e1 --- /dev/null +++ b/server/src/contexts/sales/infrastructure/express/controllers/deleteDealer/index.ts @@ -0,0 +1,21 @@ +import { DeleteDealerUseCase } from "@/contexts/sales/application"; +import Express from "express"; +import { registerDealerRepository } from "../../../Dealer.repository"; +import { ISalesContext } from "../../../Sales.context"; +import { DeleteDealerController } from "./DeleteDealer.controller"; + +export const deleteDealerController = ( + req: Express.Request, + res: Express.Response, + next: Express.NextFunction +) => { + const context: ISalesContext = res.locals.context; + + registerDealerRepository(context); + return new DeleteDealerController( + { + useCase: new DeleteDealerUseCase(context), + }, + context + ).execute(req, res, next); +}; diff --git a/server/src/contexts/sales/infrastructure/express/controllers/getDealer/GetDealer.controller.ts b/server/src/contexts/sales/infrastructure/express/controllers/getDealer/GetDealer.controller.ts new file mode 100644 index 0000000..0382113 --- /dev/null +++ b/server/src/contexts/sales/infrastructure/express/controllers/getDealer/GetDealer.controller.ts @@ -0,0 +1,97 @@ +import { IUseCaseError, UseCaseError } from "@/contexts/common/application/useCases"; +import { ExpressController } from "@/contexts/common/infrastructure/express"; + +import { IServerError } from "@/contexts/common/domain/errors"; +import { IInfrastructureError, InfrastructureError } from "@/contexts/common/infrastructure"; +import { GetDealerUseCase } from "@/contexts/sales/application"; +import { Dealer } from "@/contexts/sales/domain"; +import { IGetDealerResponse_DTO, ensureIdIsValid } from "@shared/contexts"; +import { ISalesContext } from "../../../Sales.context"; +import { IGetDealerPresenter } from "./presenter/GetDealer.presenter"; + +export class GetDealerController extends ExpressController { + private useCase: GetDealerUseCase; + private presenter: IGetDealerPresenter; + private context: ISalesContext; + + constructor( + props: { + useCase: GetDealerUseCase; + presenter: IGetDealerPresenter; + }, + context: ISalesContext + ) { + super(); + + const { useCase, presenter } = props; + this.useCase = useCase; + this.presenter = presenter; + this.context = context; + } + + async executeImpl(): Promise { + const { dealerId } = this.req.params; + + // Validar ID + const dealerIdOrError = ensureIdIsValid(dealerId); + if (dealerIdOrError.isFailure) { + const errorMessage = "Dealer ID is not valid"; + const infraError = InfrastructureError.create( + InfrastructureError.INVALID_INPUT_DATA, + errorMessage, + dealerIdOrError.error + ); + return this.invalidInputError(errorMessage, infraError); + } + + try { + const result = await this.useCase.execute({ + id: dealerIdOrError.object, + }); + + if (result.isFailure) { + return this._handleExecuteError(result.error); + } + + const dealer = result.object; + + return this.ok(this.presenter.map(dealer, this.context)); + } catch (e: unknown) { + return this.fail(e as IServerError); + } + } + + private _handleExecuteError(error: IUseCaseError) { + let errorMessage: string; + let infraError: IInfrastructureError; + + switch (error.code) { + case UseCaseError.NOT_FOUND_ERROR: + errorMessage = "Dealer not found"; + + infraError = InfrastructureError.create( + InfrastructureError.RESOURCE_NOT_FOUND_ERROR, + errorMessage, + error + ); + + return this.notFoundError(errorMessage, infraError); + break; + + case UseCaseError.UNEXCEPTED_ERROR: + errorMessage = error.message; + + infraError = InfrastructureError.create( + InfrastructureError.UNEXCEPTED_ERROR, + errorMessage, + error + ); + return this.internalServerError(errorMessage, infraError); + break; + + default: + errorMessage = error.message; + return this.clientError(errorMessage); + } + } +} diff --git a/server/src/contexts/sales/infrastructure/express/controllers/getDealer/index.ts b/server/src/contexts/sales/infrastructure/express/controllers/getDealer/index.ts new file mode 100644 index 0000000..7f098a1 --- /dev/null +++ b/server/src/contexts/sales/infrastructure/express/controllers/getDealer/index.ts @@ -0,0 +1,24 @@ +import { GetDealerUseCase } from "@/contexts/sales/application"; +import Express from "express"; +import { registerDealerRepository } from "../../../Dealer.repository"; +import { ISalesContext } from "../../../Sales.context"; +import { GetDealerController } from "./GetDealer.controller"; +import { GetDealerPresenter } from "./presenter"; + +export const getDealerController = ( + req: Express.Request, + res: Express.Response, + next: Express.NextFunction +) => { + const context: ISalesContext = res.locals.context; + + registerDealerRepository(context); + + return new GetDealerController( + { + useCase: new GetDealerUseCase(context), + presenter: GetDealerPresenter, + }, + context + ).execute(req, res, next); +}; diff --git a/server/src/contexts/sales/infrastructure/express/controllers/getDealer/presenter/GetDealer.presenter.ts b/server/src/contexts/sales/infrastructure/express/controllers/getDealer/presenter/GetDealer.presenter.ts new file mode 100644 index 0000000..0abc9c7 --- /dev/null +++ b/server/src/contexts/sales/infrastructure/express/controllers/getDealer/presenter/GetDealer.presenter.ts @@ -0,0 +1,15 @@ +import { Dealer } from "../../../../../domain"; +import { ISalesContext } from "../../../../Sales.context"; + +export interface IGetDealerPresenter { + map: (dealer: Dealer, context: ISalesContext) => IGetDealerResponse_DTO; +} + +export const GetDealerPresenter: IGetDealerPresenter = { + map: (dealer: Dealer, context: ISalesContext): IGetDealerResponse_DTO => { + return { + id: dealer.id.toString(), + name: dealer.name.toString(), + }; + }, +}; diff --git a/server/src/contexts/sales/infrastructure/express/controllers/getDealer/presenter/index.ts b/server/src/contexts/sales/infrastructure/express/controllers/getDealer/presenter/index.ts new file mode 100644 index 0000000..3ead8e9 --- /dev/null +++ b/server/src/contexts/sales/infrastructure/express/controllers/getDealer/presenter/index.ts @@ -0,0 +1 @@ +export * from "./GetDealer.presenter"; diff --git a/server/src/contexts/sales/infrastructure/express/controllers/index.ts b/server/src/contexts/sales/infrastructure/express/controllers/index.ts new file mode 100644 index 0000000..62c9249 --- /dev/null +++ b/server/src/contexts/sales/infrastructure/express/controllers/index.ts @@ -0,0 +1,5 @@ +export * from "./createDealer"; +export * from "./deleteDealer"; +export * from "./getDealer"; +export * from "./listDealers"; +export * from "./updateDealer"; diff --git a/server/src/contexts/sales/infrastructure/express/controllers/listDealers/ListDealers.controller.ts b/server/src/contexts/sales/infrastructure/express/controllers/listDealers/ListDealers.controller.ts new file mode 100644 index 0000000..ba2d8ac --- /dev/null +++ b/server/src/contexts/sales/infrastructure/express/controllers/listDealers/ListDealers.controller.ts @@ -0,0 +1,84 @@ +import Joi from "joi"; + +import { QueryCriteriaService } from "@/contexts/common/application/services"; +import { IServerError } from "@/contexts/common/domain/errors"; +import { ExpressController } from "@/contexts/common/infrastructure/express"; +import { ListDealersResult, ListDealersUseCase } from "@/contexts/sales/application"; +import { Dealer } from "@/contexts/sales/domain"; +import { + ICollection, + IListDealers_Response_DTO, + IListResponse_DTO, + IQueryCriteria, + Result, + RuleValidator, +} from "@shared/contexts"; +import { ISalesContext } from "../../../Sales.context"; +import { IListDealersPresenter } from "./presenter"; + +export class ListDealersController extends ExpressController { + private useCase: ListDealersUseCase; + private presenter: IListDealersPresenter; + private context: ISalesContext; + + constructor( + props: { + useCase: ListDealersUseCase; + presenter: IListDealersPresenter; + }, + context: ISalesContext + ) { + super(); + + const { useCase, presenter } = props; + this.useCase = useCase; + this.presenter = presenter; + this.context = context; + } + + protected validateQuery(query): Result { + const schema = Joi.object({ + page: Joi.number().optional(), + limit: Joi.number().optional(), + $sort_by: Joi.string().optional(), + $filters: Joi.string().optional(), + q: Joi.string().optional(), + }).optional(); + + return RuleValidator.validate(schema, query); + } + + async executeImpl() { + const queryOrError = this.validateQuery(this.req.query); + if (queryOrError.isFailure) { + return this.clientError(queryOrError.error.message); + } + + const queryParams = queryOrError.object; + + try { + const queryCriteria: IQueryCriteria = QueryCriteriaService.parse(queryParams); + + console.log(queryCriteria); + + const result: ListDealersResult = await this.useCase.execute({ + queryCriteria, + }); + + if (result.isFailure) { + return this.clientError(result.error.message); + } + + const dealers = >result.object; + + return this.ok>( + this.presenter.mapArray(dealers, this.context, { + page: queryCriteria.pagination.offset, + limit: queryCriteria.pagination.limit, + }) + ); + } catch (e: unknown) { + return this.fail(e as IServerError); + } + } +} diff --git a/server/src/contexts/sales/infrastructure/express/controllers/listDealers/index.ts b/server/src/contexts/sales/infrastructure/express/controllers/listDealers/index.ts new file mode 100644 index 0000000..5536f3a --- /dev/null +++ b/server/src/contexts/sales/infrastructure/express/controllers/listDealers/index.ts @@ -0,0 +1,23 @@ +import { ListDealersUseCase } from "@/contexts/sales/application"; +import Express from "express"; +import { registerDealerRepository } from "../../../Dealer.repository"; +import { ISalesContext } from "../../../Sales.context"; +import { ListDealersController } from "./ListDealers.controller"; +import { listDealersPresenter } from "./presenter/ListDealers.presenter"; + +export const listDealersController = ( + req: Express.Request, + res: Express.Response, + next: Express.NextFunction +) => { + const context: ISalesContext = res.locals.context; + + registerDealerRepository(context); + return new ListDealersController( + { + useCase: new ListDealersUseCase(context), + presenter: listDealersPresenter, + }, + context + ).execute(req, res, next); +}; diff --git a/server/src/contexts/sales/infrastructure/express/controllers/listDealers/presenter/ListDealers.presenter.ts b/server/src/contexts/sales/infrastructure/express/controllers/listDealers/presenter/ListDealers.presenter.ts new file mode 100644 index 0000000..68d114f --- /dev/null +++ b/server/src/contexts/sales/infrastructure/express/controllers/listDealers/presenter/ListDealers.presenter.ts @@ -0,0 +1,50 @@ +import { Dealer } from "@/contexts/sales/domain"; +import { ISalesContext } from "@/contexts/sales/infrastructure/Sales.context"; +import { ICollection, IListDealers_Response_DTO, IListResponse_DTO } from "@shared/contexts"; + +export interface IListDealersPresenter { + map: (dealer: Dealer, context: ISalesContext) => IListDealers_Response_DTO; + + mapArray: ( + dealers: ICollection, + context: ISalesContext, + params: { + page: number; + limit: number; + } + ) => IListResponse_DTO; +} + +export const listDealersPresenter: IListDealersPresenter = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + map: (dealer: Dealer, context: ISalesContext): IListDealers_Response_DTO => { + return { + id: dealer.id.toString(), + name: dealer.name.toString(), + }; + }, + + mapArray: ( + dealers: ICollection, + context: ISalesContext, + params: { + page: number; + limit: number; + } + ): IListResponse_DTO => { + const { page, limit } = params; + + const totalCount = dealers.totalCount ?? 0; + const items = dealers.items.map((dealer: Dealer) => listDealersPresenter.map(dealer, context)); + + const result = { + page, + per_page: limit, + total_pages: Math.ceil(totalCount / limit), + total_items: totalCount, + items, + }; + + return result; + }, +}; diff --git a/server/src/contexts/sales/infrastructure/express/controllers/listDealers/presenter/index.ts b/server/src/contexts/sales/infrastructure/express/controllers/listDealers/presenter/index.ts new file mode 100644 index 0000000..e173238 --- /dev/null +++ b/server/src/contexts/sales/infrastructure/express/controllers/listDealers/presenter/index.ts @@ -0,0 +1 @@ +export * from "./ListDealers.presenter"; diff --git a/server/src/contexts/sales/infrastructure/express/controllers/updateDealer/UpdateDealer.controller.ts b/server/src/contexts/sales/infrastructure/express/controllers/updateDealer/UpdateDealer.controller.ts new file mode 100644 index 0000000..b69fd6d --- /dev/null +++ b/server/src/contexts/sales/infrastructure/express/controllers/updateDealer/UpdateDealer.controller.ts @@ -0,0 +1,138 @@ +import { IUseCaseError, UseCaseError } from "@/contexts/common/application"; +import { IServerError } from "@/contexts/common/domain/errors"; +import { IInfrastructureError, InfrastructureError } from "@/contexts/common/infrastructure"; +import { ExpressController } from "@/contexts/common/infrastructure/express"; +import { UpdateDealerResponseOrError, UpdateDealerUseCase } from "@/contexts/sales/application"; +import { Dealer } from "@/contexts/sales/domain"; +import { + IUpdateDealer_Request_DTO, + IUpdateDealer_Response_DTO, + ensureIdIsValid, + ensureUpdateDealer_Request_DTOIsValid, +} from "@shared/contexts"; +import { ISalesContext } from "../../../Sales.context"; +import { IUpdateDealerPresenter } from "./presenter/UpdateDealer.presenter"; + +export class UpdateDealerController extends ExpressController { + private useCase: UpdateDealerUseCase; + private presenter: IUpdateDealerPresenter; + private context: ISalesContext; + + constructor( + props: { + useCase: UpdateDealerUseCase; + presenter: IUpdateDealerPresenter; + }, + context: ISalesContext + ) { + super(); + + const { useCase, presenter } = props; + this.useCase = useCase; + this.presenter = presenter; + this.context = context; + } + + async executeImpl() { + try { + const { dealerId } = this.req.params; + const dealerDTO: IUpdateDealer_Request_DTO = this.req.body; + + // Validar ID + const dealerIdOrError = ensureIdIsValid(dealerId); + if (dealerIdOrError.isFailure) { + const errorMessage = "Dealer ID is not valid"; + const infraError = InfrastructureError.create( + InfrastructureError.INVALID_INPUT_DATA, + errorMessage, + dealerIdOrError.error + ); + return this.invalidInputError(errorMessage, infraError); + } + + // Validar DTO de datos + const dealerDTOOrError = ensureUpdateDealer_Request_DTOIsValid(dealerDTO); + + if (dealerDTOOrError.isFailure) { + const errorMessage = "Dealer data not valid"; + const infraError = InfrastructureError.create( + InfrastructureError.INVALID_INPUT_DATA, + errorMessage, + dealerDTOOrError.error + ); + return this.invalidInputError(errorMessage, infraError); + } + + // Llamar al caso de uso + const result: UpdateDealerResponseOrError = await this.useCase.execute({ + id: dealerIdOrError.object, + dealerDTO, + }); + + if (result.isFailure) { + return this._handleExecuteError(result.error); + } + + const dealer = result.object; + + return this.ok(this.presenter.map(dealer, this.context)); + } catch (e: unknown) { + return this.fail(e as IServerError); + } + } + + private _handleExecuteError(error: IUseCaseError) { + let errorMessage: string; + let infraError: IInfrastructureError; + + switch (error.code) { + case UseCaseError.NOT_FOUND_ERROR: + errorMessage = "Dealer not found"; + + infraError = InfrastructureError.create( + InfrastructureError.RESOURCE_NOT_FOUND_ERROR, + errorMessage, + error + ); + + return this.notFoundError(errorMessage, infraError); + break; + + case UseCaseError.INVALID_INPUT_DATA: + errorMessage = "Dealer data not valid"; + + infraError = InfrastructureError.create( + InfrastructureError.INVALID_INPUT_DATA, + "Datos del cliente a actulizar erróneos", + error + ); + return this.invalidInputError(errorMessage, infraError); + break; + + case UseCaseError.REPOSITORY_ERROR: + errorMessage = "Error updating dealer"; + infraError = InfrastructureError.create( + InfrastructureError.UNEXCEPTED_ERROR, + errorMessage, + error + ); + return this.conflictError(errorMessage, infraError); + break; + + case UseCaseError.UNEXCEPTED_ERROR: + errorMessage = error.message; + + infraError = InfrastructureError.create( + InfrastructureError.UNEXCEPTED_ERROR, + errorMessage, + error + ); + return this.internalServerError(errorMessage, infraError); + break; + + default: + errorMessage = error.message; + return this.clientError(errorMessage); + } + } +} diff --git a/server/src/contexts/sales/infrastructure/express/controllers/updateDealer/index.ts b/server/src/contexts/sales/infrastructure/express/controllers/updateDealer/index.ts new file mode 100644 index 0000000..47d7a11 --- /dev/null +++ b/server/src/contexts/sales/infrastructure/express/controllers/updateDealer/index.ts @@ -0,0 +1,23 @@ +import { UpdateDealerUseCase } from "@/contexts/sales/application"; +import Express from "express"; +import { registerDealerRepository } from "../../../Dealer.repository"; +import { ISalesContext } from "../../../Sales.context"; +import { UpdateDealerController } from "./UpdateDealer.controller"; +import { UpdateDealerPresenter } from "./presenter/UpdateDealer.presenter"; + +export const updateDealerController = ( + req: Express.Request, + res: Express.Response, + next: Express.NextFunction +) => { + const context: ISalesContext = res.locals.context; + + registerDealerRepository(context); + return new UpdateDealerController( + { + useCase: new UpdateDealerUseCase(context), + presenter: UpdateDealerPresenter, + }, + context + ).execute(req, res, next); +}; diff --git a/server/src/contexts/sales/infrastructure/express/controllers/updateDealer/presenter/UpdateDealer.presenter.ts b/server/src/contexts/sales/infrastructure/express/controllers/updateDealer/presenter/UpdateDealer.presenter.ts new file mode 100644 index 0000000..1d08571 --- /dev/null +++ b/server/src/contexts/sales/infrastructure/express/controllers/updateDealer/presenter/UpdateDealer.presenter.ts @@ -0,0 +1,16 @@ +import { Dealer } from "@/contexts/sales/domain"; +import { ISalesContext } from "@/contexts/sales/infrastructure/Sales.context"; +import { IUpdateDealer_Response_DTO } from "@shared/contexts"; + +export interface IUpdateDealerPresenter { + map: (dealer: Dealer, context: ISalesContext) => IUpdateDealer_Response_DTO; +} + +export const UpdateDealerPresenter: IUpdateDealerPresenter = { + map: (dealer: Dealer, context: ISalesContext): IUpdateDealer_Response_DTO => { + return { + id: dealer.id.toString(), + name: dealer.name.toString(), + }; + }, +}; diff --git a/server/src/contexts/sales/infrastructure/express/controllers/updateDealer/presenter/index.ts b/server/src/contexts/sales/infrastructure/express/controllers/updateDealer/presenter/index.ts new file mode 100644 index 0000000..d43f693 --- /dev/null +++ b/server/src/contexts/sales/infrastructure/express/controllers/updateDealer/presenter/index.ts @@ -0,0 +1 @@ +export * from "./UpdateDealer.presenter"; diff --git a/server/src/contexts/sales/infrastructure/express/index.ts b/server/src/contexts/sales/infrastructure/express/index.ts new file mode 100644 index 0000000..9bf9b1b --- /dev/null +++ b/server/src/contexts/sales/infrastructure/express/index.ts @@ -0,0 +1 @@ +export * from "./routes"; diff --git a/server/src/contexts/sales/infrastructure/express/routes.ts b/server/src/contexts/sales/infrastructure/express/routes.ts new file mode 100644 index 0000000..cc78139 --- /dev/null +++ b/server/src/contexts/sales/infrastructure/express/routes.ts @@ -0,0 +1,21 @@ +import { applyMiddleware } from "@/contexts/common/infrastructure/express"; +import Express from "express"; +import { + createDealerController, + deleteDealerController, + getDealerController, + updateDealerController, +} from "./controllers"; +import { listDealersController } from "./controllers/listDealers"; + +export const DealerRouter = (appRouter: Express.Router) => { + const dealerRoutes: Express.Router = Express.Router({ mergeParams: true }); + + dealerRoutes.get("/", applyMiddleware("isAdminUser"), listDealersController); + dealerRoutes.get("/:dealerId", applyMiddleware("isAdminUser"), getDealerController); + dealerRoutes.post("/", applyMiddleware("isAdminUser"), createDealerController); + dealerRoutes.put("/:dealerId", applyMiddleware("isAdminUser"), updateDealerController); + dealerRoutes.delete("/:dealerId", applyMiddleware("isAdminUser"), deleteDealerController); + + appRouter.use("/dealers", dealerRoutes); +}; diff --git a/server/src/contexts/sales/infrastructure/index.ts b/server/src/contexts/sales/infrastructure/index.ts new file mode 100644 index 0000000..dbbd57a --- /dev/null +++ b/server/src/contexts/sales/infrastructure/index.ts @@ -0,0 +1 @@ +export * from "./Sales.context"; diff --git a/server/src/contexts/sales/infrastructure/mappers/dealer.mapper.ts b/server/src/contexts/sales/infrastructure/mappers/dealer.mapper.ts new file mode 100644 index 0000000..77f35a1 --- /dev/null +++ b/server/src/contexts/sales/infrastructure/mappers/dealer.mapper.ts @@ -0,0 +1,55 @@ +import { + ISequelizeMapper, + MapperParamsType, + SequelizeMapper, +} from "@/contexts/common/infrastructure"; +import { Name, UniqueID } from "@shared/contexts"; +import { Dealer, IDealerProps } from "../../domain/entities"; +import { ISalesContext } from "../Sales.context"; +import { DealerCreationAttributes, DealerStatus, Dealer_Model } from "../sequelize"; + +export interface IDealerMapper + extends ISequelizeMapper {} + +class DealerMapper + extends SequelizeMapper + implements IDealerMapper +{ + public constructor(props: { context: ISalesContext }) { + super(props); + } + + protected toDomainMappingImpl(source: Dealer_Model, params: any): Dealer { + const props: IDealerProps = { + name: this.mapsValue(source, "name", Name.create), + }; + + const id = this.mapsValue(source, "id", UniqueID.create); + const userOrError = Dealer.create(props, id); + + if (userOrError.isFailure) { + throw userOrError.error; + } + + return userOrError.object; + } + + protected toPersistenceMappingImpl(source: Dealer, params?: MapperParamsType | undefined) { + return { + id: source.id.toPrimitive(), + id_contact: undefined, + name: source.name.toPrimitive(), + contact_information: "", + default_payment_method: "", + default_notes: "", + default_legal_terms: "", + default_quote_validity: "", + status: DealerStatus.ACTIVE, + }; + } +} + +export const createDealerMapper = (context: ISalesContext): IDealerMapper => + new DealerMapper({ + context, + }); diff --git a/server/src/contexts/sales/infrastructure/mappers/index.ts b/server/src/contexts/sales/infrastructure/mappers/index.ts new file mode 100644 index 0000000..5cbe81c --- /dev/null +++ b/server/src/contexts/sales/infrastructure/mappers/index.ts @@ -0,0 +1 @@ +export * from "./dealer.mapper"; diff --git a/server/src/contexts/sales/infrastructure/sequelize/dealer.model.ts b/server/src/contexts/sales/infrastructure/sequelize/dealer.model.ts new file mode 100644 index 0000000..674e758 --- /dev/null +++ b/server/src/contexts/sales/infrastructure/sequelize/dealer.model.ts @@ -0,0 +1,103 @@ +import { + DataTypes, + InferAttributes, + InferCreationAttributes, + Model, + Op, + Sequelize, +} from "sequelize"; + +export enum DealerStatus { + ACTIVE = "active", + BLOCKED = "blocked", +} + +export type DealerCreationAttributes = InferCreationAttributes; + +export class Dealer_Model extends Model< + InferAttributes, + InferCreationAttributes +> { + // To avoid table creation + /*static async sync(): Promise { + return Promise.resolve(); + }*/ + + static associate(connection: Sequelize) {} + + declare id: string; + declare id_contact?: string; // number ?? + declare name: string; + declare contact_information: string; + declare default_payment_method: string; + declare default_notes: string; + declare default_legal_terms: string; + declare default_quote_validity: string; + declare status: DealerStatus; +} + +export default (sequelize: Sequelize) => { + Dealer_Model.init( + { + id: { + type: new DataTypes.UUID(), + primaryKey: true, + }, + + id_contact: { + type: DataTypes.BIGINT().UNSIGNED, + allowNull: true, + }, + + name: { + type: DataTypes.STRING, + allowNull: false, + }, + + contact_information: DataTypes.STRING, + default_payment_method: DataTypes.STRING, + default_notes: DataTypes.STRING, + default_legal_terms: DataTypes.STRING, + default_quote_validity: DataTypes.STRING, + + status: { + type: DataTypes.ENUM(...Object.values(DealerStatus)), + allowNull: false, + }, + }, + { + sequelize, + tableName: "dealers", + + paranoid: true, // softs deletes + timestamps: true, + //version: true, + + createdAt: "created_at", + updatedAt: "updated_at", + deletedAt: "deleted_at", + + indexes: [ + { name: "id_contact_idx", fields: ["id_contact"] }, + { name: "status_idx", fields: ["status"] }, + ], + + whereMergeStrategy: "and", // <- cómo tratar el merge de un scope + scopes: { + quickSearch(value) { + return { + where: { + [Op.or]: { + name: { + [Op.like]: `%${value}%`, + }, + }, + }, + }; + }, + }, + } + ); + + return Dealer_Model; +}; diff --git a/server/src/contexts/sales/infrastructure/sequelize/index.ts b/server/src/contexts/sales/infrastructure/sequelize/index.ts new file mode 100644 index 0000000..440b0b2 --- /dev/null +++ b/server/src/contexts/sales/infrastructure/sequelize/index.ts @@ -0,0 +1 @@ +export * from "./dealer.model"; diff --git a/server/src/contexts/users/infrastructure/mappers/user.mapper.ts b/server/src/contexts/users/infrastructure/mappers/user.mapper.ts index a6badc3..8433109 100644 --- a/server/src/contexts/users/infrastructure/mappers/user.mapper.ts +++ b/server/src/contexts/users/infrastructure/mappers/user.mapper.ts @@ -6,13 +6,13 @@ import { import { Email, Name, UniqueID } from "@shared/contexts"; import { IUserProps, User } from "../../domain"; import { IUserContext } from "../User.context"; -import { TCreationUser_Attributes, User_Model } from "../sequelize/user.model"; +import { UserCreationAttributes, User_Model } from "../sequelize/user.model"; export interface IUserMapper - extends ISequelizeMapper {} + extends ISequelizeMapper {} class UserMapper - extends SequelizeMapper + extends SequelizeMapper implements IUserMapper { public constructor(props: { context: IUserContext }) { diff --git a/server/src/contexts/users/infrastructure/sequelize/user.model.ts b/server/src/contexts/users/infrastructure/sequelize/user.model.ts index 11c0a43..b9243cd 100644 --- a/server/src/contexts/users/infrastructure/sequelize/user.model.ts +++ b/server/src/contexts/users/infrastructure/sequelize/user.model.ts @@ -7,7 +7,7 @@ import { Sequelize, } from "sequelize"; -export type TCreationUser_Attributes = InferCreationAttributes; +export type UserCreationAttributes = InferCreationAttributes; export class User_Model extends Model< InferAttributes, diff --git a/server/src/infrastructure/express/api/v1.ts b/server/src/infrastructure/express/api/v1.ts index 16f5028..68b7ebf 100644 --- a/server/src/infrastructure/express/api/v1.ts +++ b/server/src/infrastructure/express/api/v1.ts @@ -1,6 +1,7 @@ import { AuthRouter } from "@/contexts/auth"; import { CatalogRouter } from "@/contexts/catalog"; import { createMiddlewareMap } from "@/contexts/common/infrastructure/express"; +import { DealerRouter } from "@/contexts/sales/infrastructure/express"; import { UserRouter } from "@/contexts/users"; import Express from "express"; import { createContextMiddleware } from "./context.middleware"; @@ -12,28 +13,21 @@ export const v1Routes = () => { res.send("Hello world!"); }); - routes.use( - ( - req: Express.Request, - res: Express.Response, - next: Express.NextFunction, - ) => { - res.locals["context"] = createContextMiddleware(); - res.locals["middlewares"] = createMiddlewareMap(); + routes.use((req: Express.Request, res: Express.Response, next: Express.NextFunction) => { + res.locals["context"] = createContextMiddleware(); + res.locals["middlewares"] = createMiddlewareMap(); - return next(); - }, - ); + return next(); + }); routes.use((req, res, next) => { - console.log( - `[${new Date().toLocaleTimeString()}] Incoming request to ${req.path}`, - ); + console.log(`[${new Date().toLocaleTimeString()}] Incoming request to ${req.path}`); next(); }); AuthRouter(routes); UserRouter(routes); CatalogRouter(routes); + DealerRouter(routes); return routes; }; diff --git a/shared/lib/contexts/index.ts b/shared/lib/contexts/index.ts index 0260577..c5e9dc3 100644 --- a/shared/lib/contexts/index.ts +++ b/shared/lib/contexts/index.ts @@ -2,4 +2,5 @@ export * from "./common"; export * from "./auth"; export * from "./catalog"; +export * from "./sales"; export * from "./users"; diff --git a/shared/lib/contexts/sales/application/dto/CreateDealer.dto/ICreateDealer_Request.dto.ts b/shared/lib/contexts/sales/application/dto/CreateDealer.dto/ICreateDealer_Request.dto.ts new file mode 100644 index 0000000..c49a1bb --- /dev/null +++ b/shared/lib/contexts/sales/application/dto/CreateDealer.dto/ICreateDealer_Request.dto.ts @@ -0,0 +1,22 @@ +import Joi from "joi"; +import { Result, RuleValidator } from "../../../../common"; + +export interface ICreateDealer_Request_DTO { + id: string; + name: string; +} + +export function ensureCreateDealer_Request_DTOIsValid(dealerDTO: ICreateDealer_Request_DTO) { + const schema = Joi.object({ + id: Joi.string(), + name: Joi.string(), + }).unknown(true); + + const result = RuleValidator.validate(schema, dealerDTO); + + if (result.isFailure) { + return Result.fail(result.error); + } + + return Result.ok(true); +} diff --git a/shared/lib/contexts/sales/application/dto/CreateDealer.dto/ICreateDealer_Response.dto.ts b/shared/lib/contexts/sales/application/dto/CreateDealer.dto/ICreateDealer_Response.dto.ts new file mode 100644 index 0000000..68a9d89 --- /dev/null +++ b/shared/lib/contexts/sales/application/dto/CreateDealer.dto/ICreateDealer_Response.dto.ts @@ -0,0 +1,4 @@ +export interface ICreateDealer_Response_DTO { + id: string; + name: string; +} diff --git a/shared/lib/contexts/sales/application/dto/CreateDealer.dto/index.ts b/shared/lib/contexts/sales/application/dto/CreateDealer.dto/index.ts new file mode 100644 index 0000000..c0dedde --- /dev/null +++ b/shared/lib/contexts/sales/application/dto/CreateDealer.dto/index.ts @@ -0,0 +1,2 @@ +export * from "./ICreateDealer_Request.dto"; +export * from "./ICreateDealer_Response.dto"; diff --git a/shared/lib/contexts/sales/application/dto/CreateReseller.dto/ICreateReseller_Request.dto.ts b/shared/lib/contexts/sales/application/dto/CreateReseller.dto/ICreateReseller_Request.dto.ts new file mode 100644 index 0000000..c49a1bb --- /dev/null +++ b/shared/lib/contexts/sales/application/dto/CreateReseller.dto/ICreateReseller_Request.dto.ts @@ -0,0 +1,22 @@ +import Joi from "joi"; +import { Result, RuleValidator } from "../../../../common"; + +export interface ICreateDealer_Request_DTO { + id: string; + name: string; +} + +export function ensureCreateDealer_Request_DTOIsValid(dealerDTO: ICreateDealer_Request_DTO) { + const schema = Joi.object({ + id: Joi.string(), + name: Joi.string(), + }).unknown(true); + + const result = RuleValidator.validate(schema, dealerDTO); + + if (result.isFailure) { + return Result.fail(result.error); + } + + return Result.ok(true); +} diff --git a/shared/lib/contexts/sales/application/dto/GetDealer.dto/IGetDealer_Response.dto.ts b/shared/lib/contexts/sales/application/dto/GetDealer.dto/IGetDealer_Response.dto.ts new file mode 100644 index 0000000..8229b63 --- /dev/null +++ b/shared/lib/contexts/sales/application/dto/GetDealer.dto/IGetDealer_Response.dto.ts @@ -0,0 +1,4 @@ +export interface IGetDealerResponse_DTO { + id: string; + name: string; +} diff --git a/shared/lib/contexts/sales/application/dto/GetDealer.dto/index.ts b/shared/lib/contexts/sales/application/dto/GetDealer.dto/index.ts new file mode 100644 index 0000000..1f3f017 --- /dev/null +++ b/shared/lib/contexts/sales/application/dto/GetDealer.dto/index.ts @@ -0,0 +1 @@ +export * from "./IGetDealer_Response.dto"; diff --git a/shared/lib/contexts/sales/application/dto/IListDealers.dto/IListDealers_Response.dto.ts b/shared/lib/contexts/sales/application/dto/IListDealers.dto/IListDealers_Response.dto.ts new file mode 100644 index 0000000..d72398a --- /dev/null +++ b/shared/lib/contexts/sales/application/dto/IListDealers.dto/IListDealers_Response.dto.ts @@ -0,0 +1,4 @@ +export interface IListDealers_Response_DTO { + id: string; + name: string; +} diff --git a/shared/lib/contexts/sales/application/dto/IListDealers.dto/index.ts b/shared/lib/contexts/sales/application/dto/IListDealers.dto/index.ts new file mode 100644 index 0000000..131a27d --- /dev/null +++ b/shared/lib/contexts/sales/application/dto/IListDealers.dto/index.ts @@ -0,0 +1 @@ +export * from "./IListDealers_Response.dto"; diff --git a/shared/lib/contexts/sales/application/dto/UpdateDealer.dto/IUpdateDealer_Request.dto.ts b/shared/lib/contexts/sales/application/dto/UpdateDealer.dto/IUpdateDealer_Request.dto.ts new file mode 100644 index 0000000..1bfb660 --- /dev/null +++ b/shared/lib/contexts/sales/application/dto/UpdateDealer.dto/IUpdateDealer_Request.dto.ts @@ -0,0 +1,20 @@ +import Joi from "joi"; +import { Result, RuleValidator } from "../../../../common"; + +export interface IUpdateDealer_Request_DTO { + name: string; +} + +export function ensureUpdateDealer_Request_DTOIsValid(dealerDTO: IUpdateDealer_Request_DTO) { + const schema = Joi.object({ + name: Joi.string(), + }).unknown(true); + + const result = RuleValidator.validate(schema, dealerDTO); + + if (result.isFailure) { + return Result.fail(result.error); + } + + return Result.ok(true); +} diff --git a/shared/lib/contexts/sales/application/dto/UpdateDealer.dto/IUpdateDealer_Response.dto.ts b/shared/lib/contexts/sales/application/dto/UpdateDealer.dto/IUpdateDealer_Response.dto.ts new file mode 100644 index 0000000..15596b0 --- /dev/null +++ b/shared/lib/contexts/sales/application/dto/UpdateDealer.dto/IUpdateDealer_Response.dto.ts @@ -0,0 +1,4 @@ +export interface IUpdateDealer_Response_DTO { + id: string; + name: string; +} diff --git a/shared/lib/contexts/sales/application/dto/UpdateDealer.dto/index.ts b/shared/lib/contexts/sales/application/dto/UpdateDealer.dto/index.ts new file mode 100644 index 0000000..c764d91 --- /dev/null +++ b/shared/lib/contexts/sales/application/dto/UpdateDealer.dto/index.ts @@ -0,0 +1,2 @@ +export * from "./IUpdateDealer_Request.dto"; +export * from "./IUpdateDealer_Response.dto"; diff --git a/shared/lib/contexts/sales/application/dto/index.ts b/shared/lib/contexts/sales/application/dto/index.ts new file mode 100644 index 0000000..e82d5f0 --- /dev/null +++ b/shared/lib/contexts/sales/application/dto/index.ts @@ -0,0 +1,4 @@ +export * from "./CreateDealer.dto"; +export * from "./GetDealer.dto"; +export * from "./IListDealers.dto"; +export * from "./UpdateDealer.dto"; diff --git a/shared/lib/contexts/sales/application/index.ts b/shared/lib/contexts/sales/application/index.ts new file mode 100644 index 0000000..0392b1b --- /dev/null +++ b/shared/lib/contexts/sales/application/index.ts @@ -0,0 +1 @@ +export * from "./dto"; diff --git a/shared/lib/contexts/sales/index.ts b/shared/lib/contexts/sales/index.ts new file mode 100644 index 0000000..f4fe054 --- /dev/null +++ b/shared/lib/contexts/sales/index.ts @@ -0,0 +1 @@ +export * from "./application"; diff --git a/shared/tsconfig.json b/shared/tsconfig.json deleted file mode 100644 index 1bd71bc..0000000 --- a/shared/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "baseUrl": "./lib/*" /* Base directory to resolve non-absolute module names. */, - "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */, - "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true - } -}