From 923bd9222043b9542807af1fffbbf5019fb9e952 Mon Sep 17 00:00:00 2001 From: David Arranz Date: Thu, 16 May 2024 21:40:07 +0200 Subject: [PATCH] . --- .../express/passport/authMiddleware.ts | 2 +- .../auth/infrastructure/express/routes.ts | 3 +- .../users/application/CreateUser.useCase.ts | 366 ++++++++++++++++++ .../users/application/DeleteUser.useCase.ts | 65 ++++ .../users/application/GetUser.useCase.ts | 83 ++++ ...stUsersUseCase.ts => ListUsers.useCase.ts} | 0 .../users/application/UpdateUser.useCase.ts | 337 ++++++++++++++++ .../src/contexts/users/application/index.ts | 6 +- .../repository/UserRepository.interface.ts | 4 + .../users/infrastructure/User.repository.ts | 8 + .../controllers/getUser/GetUser.controller.ts | 106 +++++ .../express/controllers/getUser/index.ts | 16 + .../getUser/presenter/GetUser.presenter.ts | 17 + .../controllers/getUser/presenter/index.ts | 1 + .../express/controllers/index.ts | 1 + .../listUsers/ListUsers.controller.ts | 4 +- .../users/infrastructure/express/routes.ts | 13 +- .../common/application/Common.service.ts | 42 ++ .../lib/contexts/common/application/index.ts | 1 + .../common/domain/entities/UniqueID.ts | 17 +- .../users/application/User.service.ts | 1 + .../dto/GetUser.dto/IGetUser.dto.ts | 5 + .../dto/GetUser.dto/IGetUser_Response.dto.ts | 5 + .../application/dto/GetUser.dto/index.ts | 2 + .../contexts/users/application/dto/index.ts | 1 + .../lib/contexts/users/application/index.ts | 1 + tsconfig.json | 7 +- 27 files changed, 1099 insertions(+), 15 deletions(-) create mode 100644 server/src/contexts/users/application/CreateUser.useCase.ts create mode 100644 server/src/contexts/users/application/DeleteUser.useCase.ts create mode 100644 server/src/contexts/users/application/GetUser.useCase.ts rename server/src/contexts/users/application/{ListUsersUseCase.ts => ListUsers.useCase.ts} (100%) create mode 100644 server/src/contexts/users/application/UpdateUser.useCase.ts create mode 100644 server/src/contexts/users/infrastructure/express/controllers/getUser/GetUser.controller.ts create mode 100644 server/src/contexts/users/infrastructure/express/controllers/getUser/index.ts create mode 100644 server/src/contexts/users/infrastructure/express/controllers/getUser/presenter/GetUser.presenter.ts create mode 100644 server/src/contexts/users/infrastructure/express/controllers/getUser/presenter/index.ts create mode 100644 shared/lib/contexts/common/application/Common.service.ts create mode 100644 shared/lib/contexts/users/application/User.service.ts create mode 100644 shared/lib/contexts/users/application/dto/GetUser.dto/IGetUser.dto.ts create mode 100644 shared/lib/contexts/users/application/dto/GetUser.dto/IGetUser_Response.dto.ts create mode 100644 shared/lib/contexts/users/application/dto/GetUser.dto/index.ts diff --git a/server/src/contexts/auth/infrastructure/express/passport/authMiddleware.ts b/server/src/contexts/auth/infrastructure/express/passport/authMiddleware.ts index 8400ac0..3ac1c35 100644 --- a/server/src/contexts/auth/infrastructure/express/passport/authMiddleware.ts +++ b/server/src/contexts/auth/infrastructure/express/passport/authMiddleware.ts @@ -36,7 +36,7 @@ export const isLoggedUser = compose([ }), (req: Express.Request, res: Express.Response, next: Express.NextFunction) => { const user = req.user; - if (user.isUser) { + if (!user.isUser) { return generateExpressErrorResponse(req, res, httpStatus.UNAUTHORIZED); } next(); diff --git a/server/src/contexts/auth/infrastructure/express/routes.ts b/server/src/contexts/auth/infrastructure/express/routes.ts index b9d7844..93279d2 100644 --- a/server/src/contexts/auth/infrastructure/express/routes.ts +++ b/server/src/contexts/auth/infrastructure/express/routes.ts @@ -2,7 +2,7 @@ import { registerMiddleware } from "@/contexts/common/infrastructure/express"; import Express from "express"; import passport from "passport"; import { createLoginController } from "./controllers"; -import { isLoggedUser } from "./passport"; +import { isAdminUser, isLoggedUser } from "./passport"; /*authRoutes.post( "/login", @@ -41,6 +41,7 @@ export const AuthRouter = (appRouter: Express.Router) => { const authRoutes: Express.Router = Express.Router({ mergeParams: true }); appRouter.use(registerMiddleware("isLoggedUser", isLoggedUser)); + appRouter.use(registerMiddleware("isAdminUser", isAdminUser)); authRoutes.post( "/login", diff --git a/server/src/contexts/users/application/CreateUser.useCase.ts b/server/src/contexts/users/application/CreateUser.useCase.ts new file mode 100644 index 0000000..5731409 --- /dev/null +++ b/server/src/contexts/users/application/CreateUser.useCase.ts @@ -0,0 +1,366 @@ +import { + IUseCase, + IUseCaseError, + UseCaseError, + handleInvalidInputDataFailure, + handleResourceAlreadyExitsFailure, + handleUseCaseError, +} from "@/contexts/common/application/useCases"; +import { IRepositoryManager } from "@/contexts/common/domain"; +import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize"; +import { + AddressTitle, + City, + Collection, + Country, + DomainError, + ICreateUser_DTO, + IDomainError, + Note, + PostalCode, + Province, + Result, + ResultCollection, + Street, + UniqueID, + UserEmail, + UserJobTitle, + UserName, + UserPhone, + UserTIN, + ensureUserIdIsValid, + ensureUserTINIsValid, +} from "@shared/contexts"; + +import { IInfrastructureError } from "@/contexts/common/infrastructure"; +import { + IBillingAddressUser_DTO, + IShippingAddressUser_DTO, +} from "@shared/contexts/users/application/dto/IUserAddressDTO"; +import { + IUserRepository, + User, + UserBillingAddress as UserShippingAddress, +} from "../domain"; +import { UserAddressType } from "../domain/entities/UserAddress"; + +export type CreateUserResponseOrError = + | Result // Misc errors (value objects) + | Result; // Success! + +export class CreateUserUseCase + 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: ICreateUser_DTO): Promise { + const userDTO = request; + + // Validaciones de datos + + const userIdOrError = ensureUserIdIsValid(userDTO.id); + if (userIdOrError.isFailure) { + return handleInvalidInputDataFailure( + "User ID is not valid", + userIdOrError.error, + ); + } + + const idExists = await this.findUserID(userIdOrError.object); + if (idExists) { + return handleResourceAlreadyExitsFailure( + `Another user with ID ${userDTO.id} exists`, + userIdOrError.error, + ); + } + + const tinOrError = ensureUserTINIsValid(userDTO.tin); + if (tinOrError.isFailure) { + return handleInvalidInputDataFailure( + `User TIN is not valid: ${tinOrError.error.message}`, + userIdOrError.error, + ); + } + + const tinExists = + !tinOrError.object.isEmpty() && + (await this.findUserTIN(tinOrError.object)); + + if (tinExists) { + return handleResourceAlreadyExitsFailure( + `User with TIN ${tinOrError.object.toString()} exists`, + userIdOrError.error, + ); + } + + // Crear user + const userOrError = this.tryCreateUserInstance( + userDTO, + userIdOrError.object, + ); + + if (userOrError.isFailure) { + const { error: domainError } = userOrError; + let errorCode = ""; + let message = ""; + + switch (domainError.code) { + case User.ERROR_CUSTOMER_WITHOUT_NAME: + return handleInvalidInputDataFailure( + "El usuario debe ser una compañía o tener nombre y apellidos.", + domainError, + ); + break; + + case DomainError.INVALID_INPUT_DATA: + errorCode = UseCaseError.INVALID_INPUT_DATA; + message = domainError.message; + return handleInvalidInputDataFailure( + "El usuario debe ser una compañía o tener nombre y apellidos.", + domainError, + ); + break; + + default: + errorCode = UseCaseError.UNEXCEPTED_ERROR; + message = domainError.message; + return handleUseCaseError(errorCode, message, domainError); + break; + } + } + + return this.createUser(userOrError.object); + } + + private async createUser(user: User) { + // Guardar el contacto + const transaction = this._adapter.startTransaction(); + const userRepoBuilder = this.getRepositoryByName("User"); + let userRepo: IUserRepository; + + try { + await transaction.complete(async (t) => { + userRepo = userRepoBuilder({ transaction: t }); + await userRepo.create(user); + }); + + return Result.ok(user); + } catch (error: unknown) { + const _error = error as IInfrastructureError; + return Result.fail( + handleUseCaseError( + UseCaseError.REPOSITORY_ERROR, + "Error al guardar el usuario", + _error, + ), + ); + } + } + + private async findUserID(id: UniqueID) { + const userRepoBuilder = this.getRepositoryByName("User"); + return await userRepoBuilder().exists(id); + } + + private async findUserTIN(tin: UserTIN) { + const userRepoBuilder = this.getRepositoryByName("User"); + return await userRepoBuilder().existsWithSameTIN(tin); + } + + private tryCreateUserInstance( + userDTO: ICreateUser_DTO, + userId: UniqueID, + ): Result { + const userTINOrError = ensureUserTINIsValid(userDTO.tin); + if (userTINOrError.isFailure) { + return Result.fail(userTINOrError.error); + } + + const companyNameOrError = UserName.create(userDTO.company_name); + if (companyNameOrError.isFailure) { + return Result.fail(companyNameOrError.error); + } + + const firstNameOrError = UserName.create(userDTO.first_name); + if (firstNameOrError.isFailure) { + return Result.fail(firstNameOrError.error); + } + + const lastNameOrError = UserName.create(userDTO.last_name); + if (lastNameOrError.isFailure) { + return Result.fail(lastNameOrError.error); + } + + const jobTitleOrError = UserJobTitle.create(userDTO.job_title); + if (jobTitleOrError.isFailure) { + return Result.fail(jobTitleOrError.error); + } + + const emailOrError = UserEmail.create(userDTO.email); + if (emailOrError.isFailure) { + return Result.fail(emailOrError.error); + } + + const phoneOrError = UserPhone.create(userDTO.phone); + if (phoneOrError.isFailure) { + return Result.fail(phoneOrError.error); + } + + /*const taxIdOrError = UniqueID.create(userDTO.tax_id); + if (taxIdOrError.isFailure) { + return Result.fail(taxIdOrError.error); + }*/ + + const notesOrError = Note.create(userDTO.notes); + if (notesOrError.isFailure) { + return Result.fail(notesOrError.error); + } + + // Billing address + const billingAddressOrError = this.tryCreateUserAddress( + userDTO.billing_address, + "billing", + ); + if (billingAddressOrError.isFailure) { + return Result.fail(billingAddressOrError.error); + } + + // Shipping address + const shippingAddressesOrError = this.tryCreateUserShippingAddresses( + userDTO.shipping_addresses, + ); + + if (shippingAddressesOrError.isFailure) { + return Result.fail(shippingAddressesOrError.error); + } + + return User.create( + { + tin: userTINOrError.object, + companyName: companyNameOrError.object, + firstName: firstNameOrError.object, + lastName: lastNameOrError.object, + jobTitle: jobTitleOrError.object, + email: emailOrError.object, + phone: phoneOrError.object, + //taxId: taxIdOrError.object, + notes: notesOrError.object, + + billingAddress: billingAddressOrError.object, + shippingAddresses: shippingAddressesOrError.object, + }, + userId, + ); + } + + private tryCreateUserShippingAddresses( + addressesDTO: IShippingAddressUser_DTO[] | undefined, + ) { + const shippingAddressesOrError = new ResultCollection< + UserShippingAddress, + DomainError + >(); + + if (addressesDTO) { + addressesDTO.map((value, index) => { + const result = this.tryCreateUserAddress(value, "shipping"); + if (result.error) { + const { path } = result.error.payload; + const newPath = `shipping_addresses.${index}.${path}`; + + result.error.payload.path = newPath; + } + shippingAddressesOrError.add(result); + }); + } + + if (shippingAddressesOrError.hasSomeFaultyResult()) { + return Result.fail(shippingAddressesOrError.getFirstFaultyResult().error); + } + + return Result.ok(new Collection(shippingAddressesOrError.objects)); + } + + private tryCreateUserAddress( + addressDTO: IBillingAddressUser_DTO | IShippingAddressUser_DTO | undefined, + addressType: UserAddressType, + ) { + const titleOrError = AddressTitle.create(addressDTO?.title); + if (titleOrError.isFailure) { + return Result.fail(titleOrError.error); + } + + const streetOrError = Street.create(addressDTO?.street); + if (streetOrError.isFailure) { + return Result.fail(streetOrError.error); + } + + const postalCodeOrError = PostalCode.create(addressDTO?.postal_code); + if (postalCodeOrError.isFailure) { + return Result.fail(postalCodeOrError.error); + } + + const cityOrError = City.create(addressDTO?.city); + if (cityOrError.isFailure) { + return Result.fail(cityOrError.error); + } + + const provinceOrError = Province.create(addressDTO?.province); + if (provinceOrError.isFailure) { + return Result.fail(provinceOrError.error); + } + + const countryOrError = Country.create(addressDTO?.country); + if (countryOrError.isFailure) { + return Result.fail(countryOrError.error); + } + + const emailOrError = UserEmail.create(addressDTO?.email, { + label: "email", + path: "email", + }); + + if (emailOrError.isFailure) { + return Result.fail(emailOrError.error); + } + + const phoneOrError = UserPhone.create(addressDTO?.phone); + if (phoneOrError.isFailure) { + return Result.fail(phoneOrError.error); + } + + const notesOrError = Note.create(addressDTO?.notes); + if (notesOrError.isFailure) { + return Result.fail(notesOrError.error); + } + + const addressProps = { + title: titleOrError.object, + street: streetOrError.object, + city: cityOrError.object, + province: provinceOrError.object, + postalCode: postalCodeOrError.object, + country: countryOrError.object, + email: emailOrError.object, + phone: phoneOrError.object, + notes: notesOrError.object, + }; + + return addressType === "billing" + ? UserShippingAddress.create(addressProps) + : UserShippingAddress.create(addressProps); + } +} diff --git a/server/src/contexts/users/application/DeleteUser.useCase.ts b/server/src/contexts/users/application/DeleteUser.useCase.ts new file mode 100644 index 0000000..e3f55dd --- /dev/null +++ b/server/src/contexts/users/application/DeleteUser.useCase.ts @@ -0,0 +1,65 @@ +import { + IUseCase, + IUseCaseError, + IUseCaseRequest, + UseCaseError, + handleUseCaseError, +} 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 { IUserRepository } from "../domain"; + +export interface IDeleteUserUseCaseRequest extends IUseCaseRequest { + id: UniqueID; +} + +export type DeleteUserResponseOrError = + | Result // Misc errors (value objects) + | Result; // Success! + +export class DeleteUserUseCase + 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: IDeleteUserUseCaseRequest, + ): Promise { + const { id: userId } = request; + + const transaction = this._adapter.startTransaction(); + const userRepoBuilder = this.getRepositoryByName("User"); + + try { + await transaction.complete(async (t) => { + const invoiceRepo = userRepoBuilder({ transaction: t }); + await invoiceRepo.removeById(userId); + }); + + return Result.ok(); + } catch (error: unknown) { + //const _error = error as IInfrastructureError; + return Result.fail( + handleUseCaseError( + UseCaseError.REPOSITORY_ERROR, + "Error al eliminar el usuario", + ), + ); + } + } +} diff --git a/server/src/contexts/users/application/GetUser.useCase.ts b/server/src/contexts/users/application/GetUser.useCase.ts new file mode 100644 index 0000000..4d688b6 --- /dev/null +++ b/server/src/contexts/users/application/GetUser.useCase.ts @@ -0,0 +1,83 @@ +import { + IUseCase, + IUseCaseError, + IUseCaseRequest, + UseCaseError, + handleUseCaseError, +} 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 { IUserRepository } from "../domain"; + +import { IInfrastructureError } from "@/contexts/common/infrastructure"; +import { User } from "../domain/entities/User"; + +export interface IGetUserUseCaseRequest extends IUseCaseRequest { + id: UniqueID; +} + +export type GetUserResponseOrError = + | Result // Misc errors (value objects) + | Result; // Success! + +export class GetUserUseCase + 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: IGetUserUseCaseRequest, + ): Promise { + const { id } = request; + + // Validación de datos + // No hay en este caso + + return await this.findUser(id); + } + + private async findUser(id: UniqueID) { + const transaction = this._adapter.startTransaction(); + const userRepoBuilder = this.getRepositoryByName("User"); + + let user: User | null = null; + + try { + await transaction.complete(async (t) => { + const userRepo = userRepoBuilder({ transaction: t }); + user = await userRepo.getById(id); + }); + + if (!user) { + return Result.fail( + handleUseCaseError(UseCaseError.NOT_FOUND_ERROR, "User not found"), + ); + } + + return Result.ok(user!); + } catch (error: unknown) { + const _error = error as IInfrastructureError; + return Result.fail( + handleUseCaseError( + UseCaseError.REPOSITORY_ERROR, + "Error al consultar el usuario", + _error, + ), + ); + } + } +} diff --git a/server/src/contexts/users/application/ListUsersUseCase.ts b/server/src/contexts/users/application/ListUsers.useCase.ts similarity index 100% rename from server/src/contexts/users/application/ListUsersUseCase.ts rename to server/src/contexts/users/application/ListUsers.useCase.ts diff --git a/server/src/contexts/users/application/UpdateUser.useCase.ts b/server/src/contexts/users/application/UpdateUser.useCase.ts new file mode 100644 index 0000000..8da3c8f --- /dev/null +++ b/server/src/contexts/users/application/UpdateUser.useCase.ts @@ -0,0 +1,337 @@ +import { + IUseCase, + IUseCaseError, + IUseCaseRequest, + UseCaseError, + handleUseCaseError, +} from "@/contexts/common/application/useCases"; +import { IRepositoryManager } from "@/contexts/common/domain"; +import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize"; +import { + AddressTitle, + City, + Collection, + Country, + DomainError, + IDomainError, + IUpdateUser_DTO, + Note, + PostalCode, + Province, + Result, + ResultCollection, + Street, + UniqueID, + UserEmail, + UserJobTitle, + UserName, + UserPhone, + UserTIN, +} from "@shared/contexts"; + +import { IInfrastructureError } from "@/contexts/common/infrastructure"; +import { + IBillingAddressUser_DTO, + IShippingAddressUser_DTO, +} from "@shared/contexts/users/application/dto/IUserAddressDTO"; +import { + IUserRepository, + User, + UserAddressType, + UserShippingAddress, +} from "../domain"; + +export interface IUpdateUserUseCaseRequest extends IUseCaseRequest { + id: UniqueID; + userDTO: IUpdateUser_DTO; +} + +export type UpdateUserResponseOrError = + | Result // Misc errors (value objects) + | Result; // Success! + +export class UpdateUserUseCase + 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: IUpdateUserUseCaseRequest, + ): Promise { + const { id, userDTO } = request; + + // Comprobar que existe el user + const idExists = await this._findUserID(id); + if (!idExists) { + const message = `User with ID ${id.toString()} not found`; + return Result.fail( + handleUseCaseError(UseCaseError.NOT_FOUND_ERROR, message, [ + { path: "id" }, + ]), + ); + } + + // Crear user + const userOrError = this._tryCreateUserInstance(userDTO, id); + + if (userOrError.isFailure) { + const { error: domainError } = userOrError; + let errorCode = ""; + let message = ""; + let payload = {}; + + switch (domainError.code) { + // Errores manuales + case User.ERROR_CUSTOMER_WITHOUT_NAME: + errorCode = UseCaseError.INVALID_INPUT_DATA; + message = "El usuario debe ser una compañía o tener nombre."; + payload = [{ path: "first_name" }, { path: "company_name" }]; + break; + + // Value object error + case DomainError.INVALID_INPUT_DATA: + errorCode = UseCaseError.INVALID_INPUT_DATA; + message = domainError.message; + payload = domainError.payload; + break; + + default: + errorCode = UseCaseError.UNEXCEPTED_ERROR; + message = domainError.message; + payload = domainError.payload; + break; + } + + return Result.fail( + handleUseCaseError(errorCode, message, payload), + ); + } + + return this._updateUser(userOrError.object); + } + + private _tryCreateUserAddress( + addressDTO: IBillingAddressUser_DTO | IShippingAddressUser_DTO | undefined, + addressType: UserAddressType, + ) { + const titleOrError = AddressTitle.create(addressDTO?.title); + if (titleOrError.isFailure) { + return Result.fail(titleOrError.error); + } + + const streetOrError = Street.create(addressDTO?.street); + if (streetOrError.isFailure) { + return Result.fail(streetOrError.error); + } + + const postalCodeOrError = PostalCode.create(addressDTO?.postal_code); + if (postalCodeOrError.isFailure) { + return Result.fail(postalCodeOrError.error); + } + + const cityOrError = City.create(addressDTO?.city); + if (cityOrError.isFailure) { + return Result.fail(cityOrError.error); + } + + const provinceOrError = Province.create(addressDTO?.province); + if (provinceOrError.isFailure) { + return Result.fail(provinceOrError.error); + } + + const countryOrError = Country.create(addressDTO?.country); + if (countryOrError.isFailure) { + return Result.fail(countryOrError.error); + } + + const emailOrError = UserEmail.create(addressDTO?.email); + if (emailOrError.isFailure) { + return Result.fail(emailOrError.error); + } + + const phoneOrError = UserPhone.create(addressDTO?.phone); + if (phoneOrError.isFailure) { + return Result.fail(phoneOrError.error); + } + + const notesOrError = Note.create(addressDTO?.notes); + if (notesOrError.isFailure) { + return Result.fail(notesOrError.error); + } + + const addressProps = { + title: titleOrError.object, + street: streetOrError.object, + city: cityOrError.object, + province: provinceOrError.object, + postalCode: postalCodeOrError.object, + country: countryOrError.object, + email: emailOrError.object, + phone: phoneOrError.object, + notes: notesOrError.object, + }; + + return addressType === "billing" + ? UserShippingAddress.create(addressProps) + : UserShippingAddress.create(addressProps); + } + + private _tryCreateUserShippingAddresses( + addressesDTO: IShippingAddressUser_DTO[] | undefined, + ) { + const shippingAddressesOrError = new ResultCollection< + UserShippingAddress, + DomainError + >(); + + if (addressesDTO) { + addressesDTO.map((value) => { + const result = this._tryCreateUserAddress(value, "shipping"); + shippingAddressesOrError.add(result); + }); + } + + if (shippingAddressesOrError.hasSomeFaultyResult()) { + return Result.fail(shippingAddressesOrError.getFirstFaultyResult().error); + } + + return Result.ok(new Collection(shippingAddressesOrError.objects)); + } + + private _tryCreateUserInstance( + userDTO: IUpdateUser_DTO, + userId: UniqueID, + ): Result { + const userTINOrError = UserTIN.create(userDTO.tin, { + label: "tin", + path: "tin", + }); + if (userTINOrError.isFailure) { + return Result.fail(userTINOrError.error); + } + + const companyNameOrError = UserName.create(userDTO.company_name); + if (companyNameOrError.isFailure) { + return Result.fail(companyNameOrError.error); + } + + const firstNameOrError = UserName.create(userDTO.first_name); + if (firstNameOrError.isFailure) { + return Result.fail(firstNameOrError.error); + } + + const lastNameOrError = UserName.create(userDTO.last_name); + if (lastNameOrError.isFailure) { + return Result.fail(lastNameOrError.error); + } + + const jobTitleOrError = UserJobTitle.create(userDTO.job_title); + if (jobTitleOrError.isFailure) { + return Result.fail(jobTitleOrError.error); + } + + const emailOrError = UserEmail.create(userDTO.email); + if (emailOrError.isFailure) { + return Result.fail(emailOrError.error); + } + + const phoneOrError = UserPhone.create(userDTO.phone); + if (phoneOrError.isFailure) { + return Result.fail(phoneOrError.error); + } + + /*const taxIdOrError = UniqueID.create(userDTO.tax_id); + if (taxIdOrError.isFailure) { + return Result.fail(taxIdOrError.error); + }*/ + + const notesOrError = Note.create(userDTO.notes); + if (notesOrError.isFailure) { + return Result.fail(notesOrError.error); + } + + // Billing address + const billingAddressOrError = this._tryCreateUserAddress( + userDTO.billing_address, + "billing", + ); + if (billingAddressOrError.isFailure) { + return Result.fail(billingAddressOrError.error); + } + + // Shipping address + const shippingAddressesOrError = this._tryCreateUserShippingAddresses( + userDTO.shipping_addresses, + ); + + if (shippingAddressesOrError.isFailure) { + return Result.fail(shippingAddressesOrError.error); + } + + return User.create( + { + tin: userTINOrError.object, + companyName: companyNameOrError.object, + firstName: firstNameOrError.object, + lastName: lastNameOrError.object, + jobTitle: jobTitleOrError.object, + email: emailOrError.object, + phone: phoneOrError.object, + //taxId: taxIdOrError.object, + notes: notesOrError.object, + + billingAddress: billingAddressOrError.object, + shippingAddresses: shippingAddressesOrError.object, + }, + userId, + ); + } + + private async _findUserTIN(tin: UserTIN) { + const userRepoBuilder = this.getRepositoryByName("User"); + return await userRepoBuilder().existsWithSameTIN(tin); + } + + private async _findUserID(id: UniqueID) { + const userRepoBuilder = this.getRepositoryByName("User"); + return await userRepoBuilder().exists(id); + } + + private async _updateUser(user: User) { + // Guardar el contacto + const transaction = this._adapter.startTransaction(); + const userRepoBuilder = this.getRepositoryByName("User"); + + try { + await transaction.complete(async (t) => { + const userRepo = userRepoBuilder({ transaction: t }); + await userRepo.update(user); + }); + + return Result.ok(user); + } catch (error: unknown) { + const _error = error as IInfrastructureError; + return Result.fail( + handleUseCaseError( + UseCaseError.REPOSITORY_ERROR, + "Error al guardar el usuario", + _error, + ), + ); + } + } +} diff --git a/server/src/contexts/users/application/index.ts b/server/src/contexts/users/application/index.ts index 303db18..c44cdae 100644 --- a/server/src/contexts/users/application/index.ts +++ b/server/src/contexts/users/application/index.ts @@ -1 +1,5 @@ -export * from "./ListUsersUseCase"; +export * from "./DeleteUser.useCase"; +export * from "./GetUser.useCase"; +export * from "./ListUsers.useCase"; +//export * from "./CreateUser.useCase"; +//export * from "./UpdateUser.useCase"; diff --git a/server/src/contexts/users/domain/repository/UserRepository.interface.ts b/server/src/contexts/users/domain/repository/UserRepository.interface.ts index 31756ce..76309cb 100644 --- a/server/src/contexts/users/domain/repository/UserRepository.interface.ts +++ b/server/src/contexts/users/domain/repository/UserRepository.interface.ts @@ -5,4 +5,8 @@ import { User } from "../entities"; export interface IUserRepository extends IRepository { getById(id: UniqueID): Promise; findAll(queryCriteria?: IQueryCriteria): Promise>; + + removeById(id: UniqueID): Promise; + + exists(id: UniqueID): Promise; } diff --git a/server/src/contexts/users/infrastructure/User.repository.ts b/server/src/contexts/users/infrastructure/User.repository.ts index 034e11d..45ff810 100644 --- a/server/src/contexts/users/infrastructure/User.repository.ts +++ b/server/src/contexts/users/infrastructure/User.repository.ts @@ -67,6 +67,14 @@ export class UserRepository return this.mapper.mapArrayAndCountToDomain(rows, count); } + + public async removeById(id: UniqueID, force: boolean = false): Promise { + return this._removeById("User_Model", id); + } + + public async exists(id: UniqueID): Promise { + return this._exists("User_Model", "id", id.toString()); + } } export const registerUserRepository = (context: IUserContext) => { diff --git a/server/src/contexts/users/infrastructure/express/controllers/getUser/GetUser.controller.ts b/server/src/contexts/users/infrastructure/express/controllers/getUser/GetUser.controller.ts new file mode 100644 index 0000000..743b320 --- /dev/null +++ b/server/src/contexts/users/infrastructure/express/controllers/getUser/GetUser.controller.ts @@ -0,0 +1,106 @@ +import { + IUseCaseError, + UseCaseError, +} from "@/contexts/common/application/useCases"; +import { ExpressController } from "@/contexts/common/infrastructure/express"; +import { GetUserUseCase } from "@/contexts/users/application"; +import { User } from "@/contexts/users/domain/entities/User"; +import { IGetUser_Response_DTO, ensureIdIsValid } from "@shared/contexts"; + +import { IServerError } from "@/contexts/common/domain/errors"; +import { + IInfrastructureError, + InfrastructureError, + handleInfrastructureError, +} from "@/contexts/common/infrastructure"; +import { IUserContext } from "../../../User.context"; +import { IGetUserPresenter } from "./presenter"; + +export class GetUserController extends ExpressController { + private useCase: GetUserUseCase; + private presenter: IGetUserPresenter; + private context: IUserContext; + + constructor( + props: { + useCase: GetUserUseCase; + presenter: IGetUserPresenter; + }, + context: IUserContext, + ) { + super(); + + const { useCase, presenter } = props; + this.useCase = useCase; + this.presenter = presenter; + this.context = context; + } + + async executeImpl(): Promise { + const { userId } = this.req.params; + + // Validar ID + const userIdOrError = ensureIdIsValid(userId); + if (userIdOrError.isFailure) { + const errorMessage = "User ID is not valid"; + const infraError = handleInfrastructureError( + InfrastructureError.INVALID_INPUT_DATA, + errorMessage, + userIdOrError.error, + ); + return this.invalidInputError(errorMessage, infraError); + } + + try { + const result = await this.useCase.execute({ + id: userIdOrError.object, + }); + + if (result.isFailure) { + return this._handleExecuteError(result.error); + } + + const user = result.object; + + return this.ok( + this.presenter.map(user, 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 = "User not found"; + + infraError = handleInfrastructureError( + InfrastructureError.RESOURCE_NOT_FOUND_ERROR, + errorMessage, + error, + ); + + return this.notFoundError(errorMessage, infraError); + break; + + case UseCaseError.UNEXCEPTED_ERROR: + errorMessage = error.message; + + infraError = handleInfrastructureError( + 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/users/infrastructure/express/controllers/getUser/index.ts b/server/src/contexts/users/infrastructure/express/controllers/getUser/index.ts new file mode 100644 index 0000000..566234b --- /dev/null +++ b/server/src/contexts/users/infrastructure/express/controllers/getUser/index.ts @@ -0,0 +1,16 @@ +import { GetUserUseCase } from "@/contexts/users/application"; +import { IUserContext } from "../../../User.context"; +import { registerUserRepository } from "../../../User.repository"; +import { GetUserController } from "./GetUser.controller"; +import { GetUserPresenter } from "./presenter"; + +export const createGetUserController = (context: IUserContext) => { + registerUserRepository(context); + return new GetUserController( + { + useCase: new GetUserUseCase(context), + presenter: GetUserPresenter, + }, + context, + ); +}; diff --git a/server/src/contexts/users/infrastructure/express/controllers/getUser/presenter/GetUser.presenter.ts b/server/src/contexts/users/infrastructure/express/controllers/getUser/presenter/GetUser.presenter.ts new file mode 100644 index 0000000..07ad501 --- /dev/null +++ b/server/src/contexts/users/infrastructure/express/controllers/getUser/presenter/GetUser.presenter.ts @@ -0,0 +1,17 @@ +import { IGetUser_Response_DTO } from "@shared/contexts"; +import { User } from "../../../../../domain"; +import { IUserContext } from "../../../../User.context"; + +export interface IGetUserPresenter { + map: (user: User, context: IUserContext) => IGetUser_Response_DTO; +} + +export const GetUserPresenter: IGetUserPresenter = { + map: (user: User, context: IUserContext): IGetUser_Response_DTO => { + return { + id: user.id.toString(), + name: user.name.toString(), + email: user.email.toString(), + }; + }, +}; diff --git a/server/src/contexts/users/infrastructure/express/controllers/getUser/presenter/index.ts b/server/src/contexts/users/infrastructure/express/controllers/getUser/presenter/index.ts new file mode 100644 index 0000000..6d28f82 --- /dev/null +++ b/server/src/contexts/users/infrastructure/express/controllers/getUser/presenter/index.ts @@ -0,0 +1 @@ +export * from "./GetUser.presenter"; diff --git a/server/src/contexts/users/infrastructure/express/controllers/index.ts b/server/src/contexts/users/infrastructure/express/controllers/index.ts index dbb5728..501404e 100644 --- a/server/src/contexts/users/infrastructure/express/controllers/index.ts +++ b/server/src/contexts/users/infrastructure/express/controllers/index.ts @@ -1 +1,2 @@ +export * from "./getUser"; export * from "./listUsers"; diff --git a/server/src/contexts/users/infrastructure/express/controllers/listUsers/ListUsers.controller.ts b/server/src/contexts/users/infrastructure/express/controllers/listUsers/ListUsers.controller.ts index 5b68b4b..97633ee 100644 --- a/server/src/contexts/users/infrastructure/express/controllers/listUsers/ListUsers.controller.ts +++ b/server/src/contexts/users/infrastructure/express/controllers/listUsers/ListUsers.controller.ts @@ -73,10 +73,10 @@ export class ListUsersController extends ExpressController { return this.clientError(result.error.message); } - const customers = >result.object; + const users = >result.object; return this.ok>( - this.presenter.mapArray(customers, this.context, { + this.presenter.mapArray(users, this.context, { page: queryCriteria.pagination.offset, limit: queryCriteria.pagination.limit, }), diff --git a/server/src/contexts/users/infrastructure/express/routes.ts b/server/src/contexts/users/infrastructure/express/routes.ts index 5779a06..96a4975 100644 --- a/server/src/contexts/users/infrastructure/express/routes.ts +++ b/server/src/contexts/users/infrastructure/express/routes.ts @@ -1,6 +1,9 @@ import { applyMiddleware } from "@/contexts/common/infrastructure/express"; import Express from "express"; -import { createListUsersController } from "./controllers"; +import { + createGetUserController, + createListUsersController, +} from "./controllers"; export const UserRouter = (appRouter: Express.Router) => { const userRoutes: Express.Router = Express.Router({ mergeParams: true }); @@ -12,12 +15,12 @@ export const UserRouter = (appRouter: Express.Router) => { createListUsersController(res.locals["context"]).execute(req, res, next), ); - /*userRoutes.get( - "/:id", + userRoutes.get( + "/:userId", applyMiddleware("isAdminUser"), (req: Express.Request, res: Express.Response, next: Express.NextFunction) => - createGettUserController(res.locals["context"]).execute(req, res, next), - );*/ + createGetUserController(res.locals["context"]).execute(req, res, next), + ); appRouter.use("/users", userRoutes); }; diff --git a/shared/lib/contexts/common/application/Common.service.ts b/shared/lib/contexts/common/application/Common.service.ts new file mode 100644 index 0000000..aeb8632 --- /dev/null +++ b/shared/lib/contexts/common/application/Common.service.ts @@ -0,0 +1,42 @@ +import { Email, Name, Phone, Result, UniqueID } from ".."; +import { UndefinedOr } from "../../../utilities"; + +export const ensureIdIsValid = (value: string) => + UniqueID.create(value, { generateOnEmpty: false }); + +export const ensureNameIsValid = ( + value: UndefinedOr, + label: string = "name", +) => { + const valueOrError = Name.create(value, { + label, + }); + + return valueOrError.isSuccess + ? Result.ok(valueOrError.object) + : Result.fail(valueOrError.error); +}; + +export const ensureEmailIsValid = ( + value: UndefinedOr, + label: string = "email", +) => { + const valueOrError = Email.create(value, { + label, + }); + + return valueOrError.isSuccess + ? Result.ok(valueOrError.object) + : Result.fail(valueOrError.error); +}; + +export const ensurePhoneIsValid = ( + value: UndefinedOr, + label: string = "phone", +) => { + const valueOrError = Phone.create(value, { label }); + + return valueOrError.isSuccess + ? Result.ok(valueOrError.object) + : Result.fail(valueOrError.error); +}; diff --git a/shared/lib/contexts/common/application/index.ts b/shared/lib/contexts/common/application/index.ts index 0392b1b..1a455e3 100644 --- a/shared/lib/contexts/common/application/index.ts +++ b/shared/lib/contexts/common/application/index.ts @@ -1 +1,2 @@ +export * from "./Common.service"; export * from "./dto"; diff --git a/shared/lib/contexts/common/domain/entities/UniqueID.ts b/shared/lib/contexts/common/domain/entities/UniqueID.ts index 8e3ceee..57a964b 100644 --- a/shared/lib/contexts/common/domain/entities/UniqueID.ts +++ b/shared/lib/contexts/common/domain/entities/UniqueID.ts @@ -17,7 +17,7 @@ export interface IUniqueIDOptions extends INullableValueObjectOptions { export class UniqueID extends NullableValueObject { protected static validate( value: UndefinedOr, - options: IUniqueIDOptions + options: IUniqueIDOptions, ) { const ruleIsEmpty = RuleValidator.RULE_ALLOW_EMPTY.default(""); @@ -43,6 +43,15 @@ export class UniqueID extends NullableValueObject { ...options, }; + if (!value && !_options.generateOnEmpty) { + return Result.fail( + handleDomainError( + DomainError.INVALID_INPUT_DATA, + "ID is null or empty", + ), + ); + } + if (value) { const validationResult = UniqueID.validate(value, _options); @@ -51,13 +60,13 @@ export class UniqueID extends NullableValueObject { handleDomainError( DomainError.INVALID_INPUT_DATA, validationResult.error.message, - _options - ) + _options, + ), ); } return Result.ok( - new UniqueID(UniqueID.sanitize(validationResult.object)) + new UniqueID(UniqueID.sanitize(validationResult.object)), ); } diff --git a/shared/lib/contexts/users/application/User.service.ts b/shared/lib/contexts/users/application/User.service.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/shared/lib/contexts/users/application/User.service.ts @@ -0,0 +1 @@ +export {}; diff --git a/shared/lib/contexts/users/application/dto/GetUser.dto/IGetUser.dto.ts b/shared/lib/contexts/users/application/dto/GetUser.dto/IGetUser.dto.ts new file mode 100644 index 0000000..b50356b --- /dev/null +++ b/shared/lib/contexts/users/application/dto/GetUser.dto/IGetUser.dto.ts @@ -0,0 +1,5 @@ +import { UniqueID } from "../../../../common"; + +export interface IGetUserRequest_DTO { + id: UniqueID; +} diff --git a/shared/lib/contexts/users/application/dto/GetUser.dto/IGetUser_Response.dto.ts b/shared/lib/contexts/users/application/dto/GetUser.dto/IGetUser_Response.dto.ts new file mode 100644 index 0000000..8274fa5 --- /dev/null +++ b/shared/lib/contexts/users/application/dto/GetUser.dto/IGetUser_Response.dto.ts @@ -0,0 +1,5 @@ +export interface IGetUser_Response_DTO { + id: string; + name: string; + email: string; +} diff --git a/shared/lib/contexts/users/application/dto/GetUser.dto/index.ts b/shared/lib/contexts/users/application/dto/GetUser.dto/index.ts new file mode 100644 index 0000000..46a52b7 --- /dev/null +++ b/shared/lib/contexts/users/application/dto/GetUser.dto/index.ts @@ -0,0 +1,2 @@ +export * from "./IGetUser.dto"; +export * from "./IGetUser_Response.dto"; diff --git a/shared/lib/contexts/users/application/dto/index.ts b/shared/lib/contexts/users/application/dto/index.ts index 1a5bfe5..be7970b 100644 --- a/shared/lib/contexts/users/application/dto/index.ts +++ b/shared/lib/contexts/users/application/dto/index.ts @@ -1 +1,2 @@ +export * from "./GetUser.dto"; export * from "./IListUsers.dto"; diff --git a/shared/lib/contexts/users/application/index.ts b/shared/lib/contexts/users/application/index.ts index 0392b1b..ba9e164 100644 --- a/shared/lib/contexts/users/application/index.ts +++ b/shared/lib/contexts/users/application/index.ts @@ -1 +1,2 @@ +export * from "./User.service"; export * from "./dto"; diff --git a/tsconfig.json b/tsconfig.json index 892ca43..ea08ec5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,11 @@ "resolveJsonModule": true }, "references": [{ "path": "./shared/tsconfig.json" }], - "include": ["server/**/*.ts", "client/**/*.ts", "shared/**/*.ts"], + "include": [ + "server/**/*.ts", + "client/**/*.ts", + "shared/**/*.ts", + "server/src/contexts/users/application/CreateUser.useCase.ts" + ], "exclude": ["**/node_modules"] }