From d405ce40d67f9f36c9469b2ce9daa08cacf96a45 Mon Sep 17 00:00:00 2001 From: David Arranz Date: Mon, 17 Jun 2024 18:54:30 +0200 Subject: [PATCH] . --- client/src/app/auth/LoginPage.tsx | 6 +- client/src/app/settings/SettingsActions.ts | 51 ++++ client/src/app/settings/SettingsContext.tsx | 9 + client/src/app/settings/edit.tsx | 274 ++++++++++++++---- client/src/app/settings/hooks/index.ts | 1 + client/src/app/settings/hooks/useSettings.tsx | 35 +++ .../src/app/settings/useSettingsContext.tsx | 9 + client/src/lib/hooks/useDataSource/useOne.tsx | 12 +- client/src/locales/es.json | 37 ++- client/tsconfig.json | 5 +- .../profile/application/GetProfile.useCase.ts | 74 +++++ .../application/UpdateProfile.useCase.ts | 106 +++++++ .../src/contexts/profile/application/index.ts | 1 + .../profile/application/profileServices.ts | 13 + .../profile/domain/entities/Profile.ts | 45 +++ .../contexts/profile/domain/entities/index.ts | 1 + server/src/contexts/profile/domain/index.ts | 2 + .../repository/ProfileRepository.interface.ts | 9 + .../profile/domain/repository/index.ts | 1 + .../infrastructure/Profile.repository.ts | 57 ++++ .../getProfile/GetProfile.controller.ts | 13 +- .../express/controllers/getProfile/index.ts | 12 +- .../presenter/GetProfile.presenter.ts | 20 ++ .../getProfile/presenter/GetUser.presenter.ts | 26 -- .../controllers/getProfile/presenter/index.ts | 2 +- .../updateProfile/UpdateProfile.controller.ts | 135 +++++++++ .../controllers/updateProfile/index.ts | 17 ++ .../presenter/UpdateUser.presenter.ts | 20 ++ .../updateProfile/presenter/index.ts | 1 + .../profile/infrastructure/mappers/index.ts | 1 + .../infrastructure/mappers/profile.mapper.ts | 57 ++++ .../profile/infrastructure/sequelize/index.ts | 1 + .../infrastructure/sequelize/profile.model.ts | 54 ++++ .../application/Dealer/GetDealer.useCase.ts | 19 +- .../Dealer/UpdateDealer.useCase.ts | 17 ++ .../application/Dealer/dealerServices.ts | 23 ++ .../sales/application/Dealer/index.ts | 2 + .../contexts/sales/domain/entities/Dealer.ts | 66 ++++- .../sales/domain/entities/DealerRole.ts | 3 + .../sales/domain/entities/DealerStatus.ts | 88 ++++++ .../contexts/sales/domain/entities/index.ts | 1 + .../infrastructure/mappers/dealer.mapper.ts | 37 ++- .../infrastructure/sequelize/dealer.model.ts | 9 +- .../express/api/routes/profile.routes.ts | 3 +- .../common/domain/entities/KeyValueMap.ts | 35 +++ .../common/domain/entities/ValueObject.ts | 2 +- .../contexts/common/domain/entities/index.ts | 1 + .../IGetProfile_Response.dto.ts | 5 - .../IUpdateProfile_Request.dto.ts | 28 ++ .../IUpdateProfile_Response.dto.ts | 8 + .../dto/UpdateProfile.dto/index.ts | 2 + .../contexts/profile/application/dto/index.ts | 1 + .../IUpdateDealer_Request.dto.ts | 12 + 53 files changed, 1316 insertions(+), 153 deletions(-) create mode 100644 client/src/app/settings/SettingsActions.ts create mode 100644 client/src/app/settings/SettingsContext.tsx create mode 100644 client/src/app/settings/hooks/index.ts create mode 100644 client/src/app/settings/hooks/useSettings.tsx create mode 100644 client/src/app/settings/useSettingsContext.tsx create mode 100644 server/src/contexts/profile/application/GetProfile.useCase.ts create mode 100644 server/src/contexts/profile/application/UpdateProfile.useCase.ts create mode 100644 server/src/contexts/profile/application/index.ts create mode 100644 server/src/contexts/profile/application/profileServices.ts create mode 100644 server/src/contexts/profile/domain/entities/Profile.ts create mode 100644 server/src/contexts/profile/domain/entities/index.ts create mode 100644 server/src/contexts/profile/domain/index.ts create mode 100644 server/src/contexts/profile/domain/repository/ProfileRepository.interface.ts create mode 100644 server/src/contexts/profile/domain/repository/index.ts create mode 100644 server/src/contexts/profile/infrastructure/Profile.repository.ts create mode 100644 server/src/contexts/profile/infrastructure/express/controllers/getProfile/presenter/GetProfile.presenter.ts delete mode 100644 server/src/contexts/profile/infrastructure/express/controllers/getProfile/presenter/GetUser.presenter.ts create mode 100644 server/src/contexts/profile/infrastructure/express/controllers/updateProfile/UpdateProfile.controller.ts create mode 100644 server/src/contexts/profile/infrastructure/express/controllers/updateProfile/index.ts create mode 100644 server/src/contexts/profile/infrastructure/express/controllers/updateProfile/presenter/UpdateUser.presenter.ts create mode 100644 server/src/contexts/profile/infrastructure/express/controllers/updateProfile/presenter/index.ts create mode 100644 server/src/contexts/profile/infrastructure/mappers/index.ts create mode 100644 server/src/contexts/profile/infrastructure/mappers/profile.mapper.ts create mode 100644 server/src/contexts/profile/infrastructure/sequelize/index.ts create mode 100644 server/src/contexts/profile/infrastructure/sequelize/profile.model.ts create mode 100644 server/src/contexts/sales/application/Dealer/dealerServices.ts create mode 100644 server/src/contexts/sales/domain/entities/DealerRole.ts create mode 100644 server/src/contexts/sales/domain/entities/DealerStatus.ts create mode 100644 shared/lib/contexts/common/domain/entities/KeyValueMap.ts create mode 100644 shared/lib/contexts/profile/application/dto/UpdateProfile.dto/IUpdateProfile_Request.dto.ts create mode 100644 shared/lib/contexts/profile/application/dto/UpdateProfile.dto/IUpdateProfile_Response.dto.ts create mode 100644 shared/lib/contexts/profile/application/dto/UpdateProfile.dto/index.ts diff --git a/client/src/app/auth/LoginPage.tsx b/client/src/app/auth/LoginPage.tsx index bffaa77..cc50426 100644 --- a/client/src/app/auth/LoginPage.tsx +++ b/client/src/app/auth/LoginPage.tsx @@ -15,18 +15,18 @@ import { } from "@/ui"; import { joiResolver } from "@hookform/resolvers/joi"; import { ILogin_DTO } from "@shared/contexts"; +import { t } from "i18next"; import Joi from "joi"; import { AlertCircleIcon } from "lucide-react"; import { useState } from "react"; import { SubmitHandler, useForm } from "react-hook-form"; -import { Trans, useTranslation } from "react-i18next"; +import { Trans } from "react-i18next"; import { Link } from "react-router-dom"; import SpanishJoiMessages from "../../spanish-joi-messages.json"; type LoginDataForm = ILogin_DTO; export const LoginPage = () => { - const { t } = useTranslation(); const [loading, setLoading] = useState(false); const { mutate: login } = useLogin({ onSuccess: (data) => { @@ -121,7 +121,7 @@ export const LoginPage = () => { - + {form.formState.errors.root?.message} diff --git a/client/src/app/settings/SettingsActions.ts b/client/src/app/settings/SettingsActions.ts new file mode 100644 index 0000000..d5f0f05 --- /dev/null +++ b/client/src/app/settings/SettingsActions.ts @@ -0,0 +1,51 @@ +import { useOne } from '@/lib/hooks/useDataSource'; +import { IDataSource } from '@/lib/hooks/useDataSource/DataSource'; + +export type SuccessNotificationResponse = { + message: string; + description?: string; +}; + +export type PermissionResponse = unknown; + +export type IdentityResponse = unknown; + +export type CatalogActionCheckResponse = { + authenticated: boolean; + redirectTo?: string; + logout?: boolean; + error?: Error; +}; + +export type CatalogActionOnErrorResponse = { + redirectTo?: string; + logout?: boolean; + error?: Error; +}; + +export type CatalogActionResponse = { + success: boolean; + redirectTo?: string; + error?: Error; + [key: string]: unknown; + successNotification?: SuccessNotificationResponse; +}; + +export interface ISettingsActions { + getSettings: ( + dataSource: IDataSource, + ): Promise => { + + return useOne( + + ) + return dataProvider.getList({ + resource: "invoices", + quickSearchTerm, + pagination, + }); + }; + + + } +} diff --git a/client/src/app/settings/SettingsContext.tsx b/client/src/app/settings/SettingsContext.tsx new file mode 100644 index 0000000..26c8f2b --- /dev/null +++ b/client/src/app/settings/SettingsContext.tsx @@ -0,0 +1,9 @@ +import { PropsWithChildren, createContext } from "react"; + +export interface ISettingsContextState {} + +export const SettingsContext = createContext(null); + +export const SettingsProvider = ({ children }: PropsWithChildren) => { + return {children}; +}; diff --git a/client/src/app/settings/edit.tsx b/client/src/app/settings/edit.tsx index 584c419..6b2ad75 100644 --- a/client/src/app/settings/edit.tsx +++ b/client/src/app/settings/edit.tsx @@ -1,4 +1,8 @@ +import { FormTextAreaField } from "@/components"; import { + Alert, + AlertDescription, + AlertTitle, Button, Card, CardContent, @@ -6,67 +10,227 @@ import { CardFooter, CardHeader, CardTitle, - Input, + Form, } from "@/ui"; -import { Checkbox } from "@radix-ui/react-checkbox"; +import { t } from "i18next"; +import { AlertCircleIcon } from "lucide-react"; +import { useState } from "react"; +import { SubmitHandler, useForm } from "react-hook-form"; +import { Trans } from "react-i18next"; import { Link } from "react-router-dom"; +import { useSettings } from "./hooks"; + +type SettingsDataForm = { + contact_information: string; + default_payment_method: string; + default_notes: string; + default_legal_terms: string; + default_quote_validity: string; +}; export const SettingsEditor = () => { + const [loading, setLoading] = useState(false); + + const { + query: { data }, + mutation: { mutate }, + } = useSettings(); + + console.log(data); + + const form = useForm({ + mode: "onBlur", + defaultValues: { + contact_information: data?.contact_information ?? "", + default_payment_method: data?.default_payment_method ?? "", + default_notes: data?.default_notes ?? "", + default_legal_terms: data?.default_legal_terms ?? "", + default_quote_validity: data?.default_quote_validity ?? "", + }, + /*resolver: joiResolver( + Joi.object({ + email: Joi.string() + .email({ tlds: { allow: false } }) + .required(), + password: Joi.string().min(4).alphanum().required(), + }), + { + messages: SpanishJoiMessages, + } + ),*/ + }); + + const onSubmit: SubmitHandler = async (data) => { + try { + setLoading(true); + console.log(data); + mutate(data); + } finally { + setLoading(false); + } + }; + return ( -
- -
- - - Store Name - Used to identify your store in the marketplace. - - -
- -
-
- - - -
- - - Plugins Directory - - The directory within your project, in which your plugins are located. - - - -
- -
- - -
-
-
- - - -
-
-
+
+ +
+ {form.formState.errors.root?.message && ( + + + + + + {form.formState.errors.root?.message} + + )} + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ ); }; diff --git a/client/src/app/settings/hooks/index.ts b/client/src/app/settings/hooks/index.ts new file mode 100644 index 0000000..dbaf4ac --- /dev/null +++ b/client/src/app/settings/hooks/index.ts @@ -0,0 +1 @@ +export * from "./useSettings"; diff --git a/client/src/app/settings/hooks/useSettings.tsx b/client/src/app/settings/hooks/useSettings.tsx new file mode 100644 index 0000000..04edf42 --- /dev/null +++ b/client/src/app/settings/hooks/useSettings.tsx @@ -0,0 +1,35 @@ +import { useOne, useSave } from "@/lib/hooks/useDataSource"; +import { useDataSource } from "@/lib/hooks/useDataSource/useDataSource"; +import { useQueryKey } from "@/lib/hooks/useQueryKey"; +import { IGetProfileResponse_DTO } from "@shared/contexts"; + +export type UseSettingsGetParamsType = { + enabled?: boolean; + queryOptions?: Record; +}; + +export const useSettings = (params?: UseSettingsGetParamsType) => { + const dataSource = useDataSource(); + const keys = useQueryKey(); + + return { + query: useOne({ + queryKey: keys().data().resource("settings").action("one").id("").params().get(), + queryFn: () => + dataSource.getOne({ + resource: "profile", + id: "", + }), + ...params, + }), + mutation: useSave({ + mutationKey: keys().data().resource("settings").action("one").id("").params().get(), + mutationFn: (data) => + dataSource.updateOne({ + resource: "profile", + data, + id: "", + }), + }), + }; +}; diff --git a/client/src/app/settings/useSettingsContext.tsx b/client/src/app/settings/useSettingsContext.tsx new file mode 100644 index 0000000..228304c --- /dev/null +++ b/client/src/app/settings/useSettingsContext.tsx @@ -0,0 +1,9 @@ +import { useContext } from "react"; +import { SettingsContext } from "./SettingsContext"; + +export const useSettingsContext = () => { + const context = useContext(SettingsContext); + if (context === null) + throw new Error("useSettingsContext must be used within a SettingsProvider"); + return context; +}; diff --git a/client/src/lib/hooks/useDataSource/useOne.tsx b/client/src/lib/hooks/useDataSource/useOne.tsx index 4f94c8a..435d7f6 100644 --- a/client/src/lib/hooks/useDataSource/useOne.tsx +++ b/client/src/lib/hooks/useDataSource/useOne.tsx @@ -21,12 +21,14 @@ export type UseOneQueryOptions = { queryOptions?: Record; } & UseLoadingOvertimeOptionsProps; -export type UseOneQueryResult = - UseQueryResult & { - isEmpty: boolean; - } & UseLoadingOvertimeReturnType; +export type UseOneQueryResult = UseQueryResult< + TUseOneQueryData, + TUseOneQueryError +> & { + isEmpty: boolean; +} & UseLoadingOvertimeReturnType; -export function useOne({ +export function useOne({ queryKey, queryFn, enabled, diff --git a/client/src/locales/es.json b/client/src/locales/es.json index f7e5d95..06a270a 100644 --- a/client/src/locales/es.json +++ b/client/src/locales/es.json @@ -4,6 +4,7 @@ "cancel": "Cancelar", "no": "No", "yes": "Sí", + "save": "Guardar", "accept": "Aceptar", "hide": "Ocultar", "sort_asc": "Asc", @@ -18,7 +19,8 @@ "go_to_prev_page": "Ir a la página anterior", "go_to_next_page": "Ir a la página siguiente", "go_to_last_page": "Ir a la última página", - "reset_filter": "Quitar el filtro" + "reset_filter": "Quitar el filtro", + "error": "Error" }, "main_menu": { "home": "Inicio", @@ -46,8 +48,7 @@ "forgotten_password": "¿Has olvidado tu contraseña?", "become_dealer": "¿Quieres ser distribuidor de Uecko?", "contact_us": "Contacta con nosotros", - "login": "Entrar", - "error": "Error" + "login": "Entrar" }, "dashboard": { "welcome": "Bienvenido" @@ -64,7 +65,35 @@ } }, "settings": { - "title": "Ajustes" + "title": "Ajustes", + "quotes": { + "title": "Cotizaciones", + "contact_information": { + "label": "Información de contacto", + "placeholder": "placeholder", + "desc": "Información de contacto" + }, + "default_payment_method": { + "label": "Forma de pago", + "placeholder": "placeholder", + "desc": "desc" + }, + "default_notes": { + "label": "Notas", + "placeholder": "", + "desc": "desc" + }, + "default_legal_terms": { + "label": "Cláusulas legales", + "placeholder": "", + "desc": "desc" + }, + "default_quote_validity": { + "label": "Validez por defecto", + "placeholder": "", + "desc": "desc" + } + } } } } diff --git a/client/tsconfig.json b/client/tsconfig.json index f75cea2..247d680 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -28,9 +28,6 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, - "include": [ - "src", - "../shared/lib/contexts/common/domain/entities/QueryCriteria/Pagination/defaults.ts" - ], + "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/server/src/contexts/profile/application/GetProfile.useCase.ts b/server/src/contexts/profile/application/GetProfile.useCase.ts new file mode 100644 index 0000000..6109a5d --- /dev/null +++ b/server/src/contexts/profile/application/GetProfile.useCase.ts @@ -0,0 +1,74 @@ +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 { IInfrastructureError } from "@/contexts/common/infrastructure"; +import { IProfileRepository, Profile } from "../domain"; + +export interface IGetProfileUseCaseRequest extends IUseCaseRequest { + userId: UniqueID; +} + +export type GetProfileResponseOrError = + | Result // Misc errors (value objects) + | Result; // Success! + +export class GetProfileUseCase + 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: IGetProfileUseCaseRequest): Promise { + const { userId } = request; + + // Validación de datos + // No hay en este caso + + return await this._getProfileDealer(userId); + } + + private async _getProfileDealer(userId: UniqueID) { + const transaction = this._adapter.startTransaction(); + const dealerRepoBuilder = this.getRepositoryByName("Profile"); + + let profile: Profile | null = null; + + try { + await transaction.complete(async (t) => { + const dealerRepo = dealerRepoBuilder({ transaction: t }); + profile = await dealerRepo.getById(userId); + }); + + if (!profile) { + return Result.fail(UseCaseError.create(UseCaseError.NOT_FOUND_ERROR, "Profile not found")); + } + + return Result.ok(profile!); + } catch (error: unknown) { + const _error = error as IInfrastructureError; + return Result.fail( + UseCaseError.create(UseCaseError.REPOSITORY_ERROR, "Error al consultar el usuario", _error) + ); + } + } + + private _getProfileRepository() { + return this._repositoryManager.getRepository("Profile"); + } +} diff --git a/server/src/contexts/profile/application/UpdateProfile.useCase.ts b/server/src/contexts/profile/application/UpdateProfile.useCase.ts new file mode 100644 index 0000000..1eb7417 --- /dev/null +++ b/server/src/contexts/profile/application/UpdateProfile.useCase.ts @@ -0,0 +1,106 @@ +import { + IUseCase, + IUseCaseError, + IUseCaseRequest, + UseCaseError, +} from "@/contexts/common/application"; +import { IRepositoryManager } from "@/contexts/common/domain"; +import { IInfrastructureError } from "@/contexts/common/infrastructure"; +import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize"; +import { DomainError, IUpdateProfile_Request_DTO, Result, UniqueID } from "@shared/contexts"; +import { IProfileRepository, Profile } from "../domain"; + +export interface IUpdateProfileUseCaseRequest extends IUseCaseRequest { + id: UniqueID; + profileDTO: IUpdateProfile_Request_DTO; +} + +export type UpdateProfileResponseOrError = + | Result // Misc errors (value objects) + | Result; // Success! + +export class UpdateProfileUseCase + 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: IUpdateProfileUseCaseRequest): Promise { + const { id, profileDTO } = request; + const profileRepository = this._getProfileRepository(); + + // Comprobar que existe el profile + const idExists = await profileRepository().exists(id); + if (!idExists) { + const message = `Profile not found`; + return Result.fail( + UseCaseError.create(UseCaseError.NOT_FOUND_ERROR, message, { + path: "id", + }) + ); + } + + // Crear perfil con datos actualizados + const profileOrError = Profile.create( + { + contactInformation: profileDTO.contact_information, + defaultPaymentMethod: profileDTO.default_payment_method, + defaultLegalTerms: profileDTO.default_legal_terms, + defaultNotes: profileDTO.default_notes, + defaultQuoteValidity: profileDTO.default_quote_validity, + }, + id + ); + + if (profileOrError.isFailure) { + const { error: domainError } = profileOrError; + let errorCode = ""; + let message = ""; + + switch (domainError.code) { + // Errores manuales + case DomainError.INVALID_INPUT_DATA: + errorCode = UseCaseError.INVALID_INPUT_DATA; + message = "The profile has some incorrect data"; + break; + + default: + errorCode = UseCaseError.UNEXCEPTED_ERROR; + message = domainError.message; + break; + } + + return Result.fail(UseCaseError.create(errorCode, message, domainError)); + } + + // Guardar los cambios + return this._saveProfile(profileOrError.object); + } + + private async _saveProfile(updatedProfile: Profile) { + // Guardar el contacto + const transaction = this._adapter.startTransaction(); + const profileRepository = this._getProfileRepository(); + + try { + await transaction.complete(async (t) => { + const profileRepo = profileRepository({ transaction: t }); + await profileRepo.update(updatedProfile); + }); + + return Result.ok(updatedProfile); + } catch (error: unknown) { + const _error = error as IInfrastructureError; + return Result.fail(UseCaseError.create(UseCaseError.REPOSITORY_ERROR, _error.message)); + } + } + + private _getProfileRepository() { + return this._repositoryManager.getRepository("Profile"); + } +} diff --git a/server/src/contexts/profile/application/index.ts b/server/src/contexts/profile/application/index.ts new file mode 100644 index 0000000..621ed6d --- /dev/null +++ b/server/src/contexts/profile/application/index.ts @@ -0,0 +1 @@ +export * from "./UpdateProfile.useCase"; diff --git a/server/src/contexts/profile/application/profileServices.ts b/server/src/contexts/profile/application/profileServices.ts new file mode 100644 index 0000000..496e76f --- /dev/null +++ b/server/src/contexts/profile/application/profileServices.ts @@ -0,0 +1,13 @@ +import { IAdapter, RepositoryBuilder } from "@/contexts/common/domain"; +import { UniqueID } from "@shared/contexts"; +import { IProfileRepository, Profile } from "../domain"; + +export const findProfileByID = async ( + id: UniqueID, + adapter: IAdapter, + repository: RepositoryBuilder +): Promise => { + return await adapter + .startTransaction() + .complete(async (t) => repository({ transaction: t }).getById(id)); +}; diff --git a/server/src/contexts/profile/domain/entities/Profile.ts b/server/src/contexts/profile/domain/entities/Profile.ts new file mode 100644 index 0000000..55ce5d0 --- /dev/null +++ b/server/src/contexts/profile/domain/entities/Profile.ts @@ -0,0 +1,45 @@ +import { AggregateRoot, IDomainError, Result, UniqueID } from "@shared/contexts"; + +export interface IProfileProps { + contactInformation: string; + defaultPaymentMethod: string; + defaultNotes: string; + defaultLegalTerms: string; + defaultQuoteValidity: string; +} + +export interface IProfile { + id: UniqueID; + contactInformation: string; + defaultPaymentMethod: string; + defaultNotes: string; + defaultLegalTerms: string; + defaultQuoteValidity: string; +} + +export class Profile extends AggregateRoot implements IProfile { + public static create(props: IProfileProps, id?: UniqueID): Result { + const profile = new Profile(props, id); + return Result.ok(profile); + } + + get contactInformation(): string { + return this.props.contactInformation; + } + + get defaultPaymentMethod(): string { + return this.props.defaultPaymentMethod; + } + + get defaultNotes(): string { + return this.props.defaultNotes; + } + + get defaultLegalTerms(): string { + return this.props.defaultLegalTerms; + } + + get defaultQuoteValidity(): string { + return this.props.defaultQuoteValidity; + } +} diff --git a/server/src/contexts/profile/domain/entities/index.ts b/server/src/contexts/profile/domain/entities/index.ts new file mode 100644 index 0000000..b6f21ff --- /dev/null +++ b/server/src/contexts/profile/domain/entities/index.ts @@ -0,0 +1 @@ +export * from "./Profile"; diff --git a/server/src/contexts/profile/domain/index.ts b/server/src/contexts/profile/domain/index.ts new file mode 100644 index 0000000..6347a2b --- /dev/null +++ b/server/src/contexts/profile/domain/index.ts @@ -0,0 +1,2 @@ +export * from "./entities"; +export * from "./repository"; diff --git a/server/src/contexts/profile/domain/repository/ProfileRepository.interface.ts b/server/src/contexts/profile/domain/repository/ProfileRepository.interface.ts new file mode 100644 index 0000000..1ab7de4 --- /dev/null +++ b/server/src/contexts/profile/domain/repository/ProfileRepository.interface.ts @@ -0,0 +1,9 @@ +import { IRepository } from "@/contexts/common/domain"; +import { UniqueID } from "@shared/contexts"; +import { Profile } from "../entities"; + +export interface IProfileRepository extends IRepository { + exists(id: UniqueID): Promise; + getById(id: UniqueID): Promise; + update(profile: Profile): Promise; +} diff --git a/server/src/contexts/profile/domain/repository/index.ts b/server/src/contexts/profile/domain/repository/index.ts new file mode 100644 index 0000000..5c7a1c4 --- /dev/null +++ b/server/src/contexts/profile/domain/repository/index.ts @@ -0,0 +1 @@ +export * from "./ProfileRepository.interface"; diff --git a/server/src/contexts/profile/infrastructure/Profile.repository.ts b/server/src/contexts/profile/infrastructure/Profile.repository.ts new file mode 100644 index 0000000..76fad2e --- /dev/null +++ b/server/src/contexts/profile/infrastructure/Profile.repository.ts @@ -0,0 +1,57 @@ +import { ISequelizeAdapter, SequelizeRepository } from "@/contexts/common/infrastructure/sequelize"; +import { UniqueID } from "@shared/contexts"; +import { Transaction } from "sequelize"; +import { IProfileContext } from "."; +import { IProfileRepository, Profile } from "../domain"; +import { IProfileMapper, createProfileMapper } from "./mappers"; + +export class ProfileRepository extends SequelizeRepository implements IProfileRepository { + protected mapper: IProfileMapper; + + public constructor(props: { + mapper: IProfileMapper; + adapter: ISequelizeAdapter; + transaction: Transaction; + }) { + const { adapter, mapper, transaction } = props; + super({ adapter, transaction }); + this.mapper = mapper; + } + + public async exists(id: UniqueID): Promise { + return this._exists("Profile_Model", "id", id.toPrimitive()); + } + + public async getById(id: UniqueID): Promise { + const rawProfile: any = await this._getById("Profile_Model", id); + + if (!rawProfile === true) { + return null; + } + + return this.mapper.mapToDomain(rawProfile); + } + + public async update(profile: Profile): Promise { + const userData = this.mapper.mapToPersistence(profile); + + // borrando y luego creando + // await this.removeById(user.id, true); + await this._save("Dealer_Model", profile.id, userData, {}); + } +} + +export const registerProfileRepository = (context: IProfileContext) => { + const adapter = context.adapter; + const repoManager = context.repositoryManager; + + repoManager.registerRepository("Profile", (params = { transaction: null }) => { + const { transaction } = params; + + return new ProfileRepository({ + transaction, + adapter, + mapper: createProfileMapper(context), + }); + }); +}; diff --git a/server/src/contexts/profile/infrastructure/express/controllers/getProfile/GetProfile.controller.ts b/server/src/contexts/profile/infrastructure/express/controllers/getProfile/GetProfile.controller.ts index f2ca0f8..437ded5 100644 --- a/server/src/contexts/profile/infrastructure/express/controllers/getProfile/GetProfile.controller.ts +++ b/server/src/contexts/profile/infrastructure/express/controllers/getProfile/GetProfile.controller.ts @@ -1,23 +1,22 @@ import { IUseCaseError, UseCaseError } from "@/contexts/common/application/useCases"; import { ExpressController } from "@/contexts/common/infrastructure/express"; import { User } from "@/contexts/users/domain/entities/User"; -import { IGetUserResponse_DTO } from "@shared/contexts"; +import { IGetProfileResponse_DTO } from "@shared/contexts"; import { IServerError } from "@/contexts/common/domain/errors"; import { IInfrastructureError, InfrastructureError } from "@/contexts/common/infrastructure"; -import { GetDealerByUserUseCase } from "@/contexts/sales/application"; -import { Dealer } from "@/contexts/sales/domain"; +import { GetProfileUseCase } from "@/contexts/profile/application/GetProfile.useCase"; import { IProfileContext } from "../../../Profile.context"; import { IGetProfilePresenter } from "./presenter"; export class GetProfileController extends ExpressController { - private useCase: GetDealerByUserUseCase; + private useCase: GetProfileUseCase; private presenter: IGetProfilePresenter; private context: IProfileContext; constructor( props: { - useCase: GetDealerByUserUseCase; + useCase: GetProfileUseCase; presenter: IGetProfilePresenter; }, context: IProfileContext @@ -51,9 +50,9 @@ export class GetProfileController extends ExpressController { return this._handleExecuteError(result.error); } - const dealer = result.object; + const profile = result.object; - return this.ok(this.presenter.map(user, dealer, this.context)); + return this.ok(this.presenter.map(profile, this.context)); } catch (e: unknown) { return this.fail(e as IServerError); } diff --git a/server/src/contexts/profile/infrastructure/express/controllers/getProfile/index.ts b/server/src/contexts/profile/infrastructure/express/controllers/getProfile/index.ts index 580e101..6a1d948 100644 --- a/server/src/contexts/profile/infrastructure/express/controllers/getProfile/index.ts +++ b/server/src/contexts/profile/infrastructure/express/controllers/getProfile/index.ts @@ -1,16 +1,16 @@ -import { GetDealerByUserUseCase } from "@/contexts/sales/application"; -import { registerDealerRepository } from "@/contexts/sales/infrastructure/Dealer.repository"; +import { GetProfileUseCase } from "@/contexts/profile/application/GetProfile.useCase"; import { IProfileContext } from "../../../Profile.context"; +import { registerProfileRepository } from "../../../Profile.repository"; import { GetProfileController } from "./GetProfile.controller"; -import { GetUserPresenter } from "./presenter"; +import { GetProfilePresenter } from "./presenter"; export const createGetProfileController = (context: IProfileContext) => { - registerDealerRepository(context); + registerProfileRepository(context); return new GetProfileController( { - useCase: new GetDealerByUserUseCase(context), - presenter: GetUserPresenter, + useCase: new GetProfileUseCase(context), + presenter: GetProfilePresenter, }, context ); diff --git a/server/src/contexts/profile/infrastructure/express/controllers/getProfile/presenter/GetProfile.presenter.ts b/server/src/contexts/profile/infrastructure/express/controllers/getProfile/presenter/GetProfile.presenter.ts new file mode 100644 index 0000000..f548300 --- /dev/null +++ b/server/src/contexts/profile/infrastructure/express/controllers/getProfile/presenter/GetProfile.presenter.ts @@ -0,0 +1,20 @@ +import { Profile } from "@/contexts/profile/domain"; +import { IProfileContext } from "@/contexts/profile/infrastructure/Profile.context"; +import { IGetProfileResponse_DTO } from "@shared/contexts"; + +export interface IGetProfilePresenter { + map: (profile: Profile, context: IProfileContext) => IGetProfileResponse_DTO; +} + +export const GetProfilePresenter: IGetProfilePresenter = { + map: (profile: Profile, context: IProfileContext): IGetProfileResponse_DTO => { + return { + id: profile.id.toString(), + contact_information: profile.contactInformation, + default_payment_method: profile.defaultPaymentMethod, + default_notes: profile.defaultNotes, + default_legal_terms: profile.defaultLegalTerms, + default_quote_validity: profile.defaultQuoteValidity, + }; + }, +}; diff --git a/server/src/contexts/profile/infrastructure/express/controllers/getProfile/presenter/GetUser.presenter.ts b/server/src/contexts/profile/infrastructure/express/controllers/getProfile/presenter/GetUser.presenter.ts deleted file mode 100644 index a1da9ff..0000000 --- a/server/src/contexts/profile/infrastructure/express/controllers/getProfile/presenter/GetUser.presenter.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { IProfileContext } from "@/contexts/profile/infrastructure/Profile.context"; -import { Dealer } from "@/contexts/sales/domain"; -import { User } from "@/contexts/users/domain"; -import { IGetProfileResponse_DTO } from "@shared/contexts"; - -export interface IGetProfilePresenter { - map: (user: User, dealer: Dealer, context: IProfileContext) => IGetProfileResponse_DTO; -} - -export const GetUserPresenter: IGetProfilePresenter = { - map: (user: User, dealer: Dealer, context: IProfileContext): IGetProfileResponse_DTO => { - return { - id: user.id.toString(), - name: user.name.toString(), - email: user.email.toString(), - language: "es", - roles: user.getRoles().map((rol) => rol.toString()), - contact_information: "", - default_payment_method: "", - default_notes: "", - default_legal_terms: "", - default_quote_validity: "", - status: "active", - }; - }, -}; diff --git a/server/src/contexts/profile/infrastructure/express/controllers/getProfile/presenter/index.ts b/server/src/contexts/profile/infrastructure/express/controllers/getProfile/presenter/index.ts index 6d28f82..253e145 100644 --- a/server/src/contexts/profile/infrastructure/express/controllers/getProfile/presenter/index.ts +++ b/server/src/contexts/profile/infrastructure/express/controllers/getProfile/presenter/index.ts @@ -1 +1 @@ -export * from "./GetUser.presenter"; +export * from "./GetProfile.presenter"; diff --git a/server/src/contexts/profile/infrastructure/express/controllers/updateProfile/UpdateProfile.controller.ts b/server/src/contexts/profile/infrastructure/express/controllers/updateProfile/UpdateProfile.controller.ts new file mode 100644 index 0000000..f3fe862 --- /dev/null +++ b/server/src/contexts/profile/infrastructure/express/controllers/updateProfile/UpdateProfile.controller.ts @@ -0,0 +1,135 @@ +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 { UpdateProfileUseCase } from "@/contexts/profile/application"; +import { User } from "@/contexts/users/domain"; +import { + IUpdateProfileResponse_DTO, + IUpdateProfile_Request_DTO, + ensureUpdateProfile_Request_DTOIsValid, +} from "@shared/contexts"; +import { IProfileContext } from "../../../Profile.context"; +import { IUpdateProfilePresenter } from "./presenter"; + +export class UpdateProfileController extends ExpressController { + private useCase: UpdateProfileUseCase; + private presenter: IUpdateProfilePresenter; + private context: IProfileContext; + + constructor( + props: { + useCase: UpdateProfileUseCase; + presenter: IUpdateProfilePresenter; + }, + context: IProfileContext + ) { + super(); + + const { useCase, presenter } = props; + this.useCase = useCase; + this.presenter = presenter; + this.context = context; + } + + async executeImpl() { + const user = this.req.user; + + if (!user) { + const errorMessage = "Unexpected missing Profile data"; + const infraError = InfrastructureError.create( + InfrastructureError.UNEXCEPTED_ERROR, + errorMessage + ); + return this.internalServerError(errorMessage, infraError); + } + + try { + const profileDTO: IUpdateProfile_Request_DTO = this.req.body; + + // Validar DTO de datos + const ProfileDTOOrError = ensureUpdateProfile_Request_DTOIsValid(profileDTO); + + if (ProfileDTOOrError.isFailure) { + const errorMessage = "Profile data not valid"; + const infraError = InfrastructureError.create( + InfrastructureError.INVALID_INPUT_DATA, + errorMessage, + ProfileDTOOrError.error + ); + return this.invalidInputError(errorMessage, infraError); + } + + // Llamar al caso de uso + const result = await this.useCase.execute({ + id: user.id, + profileDTO, + }); + + if (result.isFailure) { + return this._handleExecuteError(result.error); + } + + const profile = result.object; + + return this.ok(this.presenter.map(profile, 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 = "Profile has no associated profile"; + + infraError = InfrastructureError.create( + InfrastructureError.RESOURCE_NOT_FOUND_ERROR, + errorMessage, + error + ); + + return this.notFoundError(errorMessage, infraError); + break; + + case UseCaseError.INVALID_INPUT_DATA: + errorMessage = "Profile data not valid"; + + infraError = InfrastructureError.create( + InfrastructureError.INVALID_INPUT_DATA, + "Datos a actualizar erróneos", + error + ); + return this.invalidInputError(errorMessage, infraError); + break; + + case UseCaseError.REPOSITORY_ERROR: + errorMessage = "Error updating profile"; + 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/profile/infrastructure/express/controllers/updateProfile/index.ts b/server/src/contexts/profile/infrastructure/express/controllers/updateProfile/index.ts new file mode 100644 index 0000000..f61cfb9 --- /dev/null +++ b/server/src/contexts/profile/infrastructure/express/controllers/updateProfile/index.ts @@ -0,0 +1,17 @@ +import { UpdateProfileUseCase } from "@/contexts/profile/application"; +import { registerDealerRepository } from "@/contexts/sales/infrastructure/Dealer.repository"; +import { IProfileContext } from "../../../Profile.context"; +import { UpdateProfileController } from "./UpdateProfile.controller"; +import { UpdateProfilePresenter } from "./presenter"; + +export const createUpdateProfileController = (context: IProfileContext) => { + registerDealerRepository(context); + + return new UpdateProfileController( + { + useCase: new UpdateProfileUseCase(context), + presenter: UpdateProfilePresenter, + }, + context + ); +}; diff --git a/server/src/contexts/profile/infrastructure/express/controllers/updateProfile/presenter/UpdateUser.presenter.ts b/server/src/contexts/profile/infrastructure/express/controllers/updateProfile/presenter/UpdateUser.presenter.ts new file mode 100644 index 0000000..fc2f45c --- /dev/null +++ b/server/src/contexts/profile/infrastructure/express/controllers/updateProfile/presenter/UpdateUser.presenter.ts @@ -0,0 +1,20 @@ +import { Profile } from "@/contexts/profile/domain"; +import { IProfileContext } from "@/contexts/profile/infrastructure/Profile.context"; +import { IUpdateProfileResponse_DTO } from "@shared/contexts"; + +export interface IUpdateProfilePresenter { + map: (profile: Profile, context: IProfileContext) => IUpdateProfileResponse_DTO; +} + +export const UpdateProfilePresenter: IUpdateProfilePresenter = { + map: (profile: Profile, context: IProfileContext): IUpdateProfileResponse_DTO => { + return { + id: profile.id.toString(), + contact_information: profile.contactInformation, + default_payment_method: profile.defaultPaymentMethod, + default_notes: profile.defaultNotes, + default_legal_terms: profile.defaultLegalTerms, + default_quote_validity: profile.defaultQuoteValidity, + }; + }, +}; diff --git a/server/src/contexts/profile/infrastructure/express/controllers/updateProfile/presenter/index.ts b/server/src/contexts/profile/infrastructure/express/controllers/updateProfile/presenter/index.ts new file mode 100644 index 0000000..4869d6e --- /dev/null +++ b/server/src/contexts/profile/infrastructure/express/controllers/updateProfile/presenter/index.ts @@ -0,0 +1 @@ +export * from "./UpdateUser.presenter"; diff --git a/server/src/contexts/profile/infrastructure/mappers/index.ts b/server/src/contexts/profile/infrastructure/mappers/index.ts new file mode 100644 index 0000000..48e932e --- /dev/null +++ b/server/src/contexts/profile/infrastructure/mappers/index.ts @@ -0,0 +1 @@ +export * from "./profile.mapper"; diff --git a/server/src/contexts/profile/infrastructure/mappers/profile.mapper.ts b/server/src/contexts/profile/infrastructure/mappers/profile.mapper.ts new file mode 100644 index 0000000..50a37b0 --- /dev/null +++ b/server/src/contexts/profile/infrastructure/mappers/profile.mapper.ts @@ -0,0 +1,57 @@ +import { + ISequelizeMapper, + MapperParamsType, + SequelizeMapper, +} from "@/contexts/common/infrastructure"; +import { UniqueID } from "@shared/contexts"; +import { IProfileProps, Profile } from "../../domain"; +import { IProfileContext } from "../Profile.context"; +import { ProfileCreationAttributes, Profile_Model } from "../sequelize"; + +export interface IProfileMapper + extends ISequelizeMapper {} + +class ProfileMapper + extends SequelizeMapper + implements IProfileMapper +{ + public constructor(props: { context: IProfileContext }) { + super(props); + } + + protected toDomainMappingImpl(source: Profile_Model, params: any): Profile { + const props: IProfileProps = { + contactInformation: source.contact_information, + defaultPaymentMethod: source.default_payment_method, + defaultNotes: source.default_notes, + defaultLegalTerms: source.default_legal_terms, + defaultQuoteValidity: source.default_quote_validity, + }; + + const id = this.mapsValue(source, "id", UniqueID.create); + const userOrError = Profile.create(props, id); + + if (userOrError.isFailure) { + throw userOrError.error; + } + + return userOrError.object; + } + + protected toPersistenceMappingImpl(source: Profile, params?: MapperParamsType | undefined) { + return { + id: source.id.toPrimitive(), + + contact_information: source.contactInformation, + default_payment_method: source.defaultPaymentMethod, + default_notes: source.defaultNotes, + default_legal_terms: source.defaultLegalTerms, + default_quote_validity: source.defaultQuoteValidity, + }; + } +} + +export const createProfileMapper = (context: IProfileContext): IProfileMapper => + new ProfileMapper({ + context, + }); diff --git a/server/src/contexts/profile/infrastructure/sequelize/index.ts b/server/src/contexts/profile/infrastructure/sequelize/index.ts new file mode 100644 index 0000000..c24024b --- /dev/null +++ b/server/src/contexts/profile/infrastructure/sequelize/index.ts @@ -0,0 +1 @@ +export * from "./profile.model"; diff --git a/server/src/contexts/profile/infrastructure/sequelize/profile.model.ts b/server/src/contexts/profile/infrastructure/sequelize/profile.model.ts new file mode 100644 index 0000000..1e36deb --- /dev/null +++ b/server/src/contexts/profile/infrastructure/sequelize/profile.model.ts @@ -0,0 +1,54 @@ +import { DataTypes, InferAttributes, InferCreationAttributes, Model, Sequelize } from "sequelize"; + +export type ProfileCreationAttributes = InferCreationAttributes; + +export class Profile_Model extends Model< + InferAttributes, + InferCreationAttributes +> { + // To avoid table creation + /*static async sync(): Promise { + return Promise.resolve(); + }*/ + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + static associate(connection: Sequelize) {} + + declare id: string; + declare contact_information: string; + declare default_payment_method: string; + declare default_notes: string; + declare default_legal_terms: string; + declare default_quote_validity: string; +} + +export default (sequelize: Sequelize) => { + Profile_Model.init( + { + id: { + type: new DataTypes.UUID(), + primaryKey: true, + }, + + contact_information: DataTypes.STRING, + default_payment_method: DataTypes.STRING, + default_notes: DataTypes.STRING, + default_legal_terms: DataTypes.STRING, + default_quote_validity: DataTypes.STRING, + }, + { + sequelize, + tableName: "dealers", + + paranoid: true, // softs deletes + timestamps: true, + //version: true, + + createdAt: false, + updatedAt: "updated_at", + deletedAt: false, + } + ); + + return Profile_Model; +}; diff --git a/server/src/contexts/sales/application/Dealer/GetDealer.useCase.ts b/server/src/contexts/sales/application/Dealer/GetDealer.useCase.ts index 7862e0a..ee24b35 100644 --- a/server/src/contexts/sales/application/Dealer/GetDealer.useCase.ts +++ b/server/src/contexts/sales/application/Dealer/GetDealer.useCase.ts @@ -7,10 +7,9 @@ import { import { IRepositoryManager } from "@/contexts/common/domain"; import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize"; import { Result, UniqueID } from "@shared/contexts"; -import { IDealerRepository } from "../../domain"; +import { Dealer, IDealerRepository } from "../../domain"; import { IInfrastructureError } from "@/contexts/common/infrastructure"; -import { Dealer } from "../../domain/entities/Dealer"; export interface IGetDealerUseCaseRequest extends IUseCaseRequest { id: UniqueID; @@ -31,28 +30,24 @@ export class GetDealerUseCase 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); + return await this._findDealer(id); } - private async findDealer(id: UniqueID) { + private async _findDealer(id: UniqueID) { const transaction = this._adapter.startTransaction(); - const dealerRepoBuilder = this.getRepositoryByName("Dealer"); + const dealerRepository = this._getDealerRepository(); let dealer: Dealer | null = null; try { await transaction.complete(async (t) => { - const dealerRepo = dealerRepoBuilder({ transaction: t }); + const dealerRepo = dealerRepository({ transaction: t }); dealer = await dealerRepo.getById(id); }); @@ -68,4 +63,8 @@ export class GetDealerUseCase ); } } + + private _getDealerRepository() { + return this._repositoryManager.getRepository("Dealer"); + } } diff --git a/server/src/contexts/sales/application/Dealer/UpdateDealer.useCase.ts b/server/src/contexts/sales/application/Dealer/UpdateDealer.useCase.ts index 9b6d47e..601d315 100644 --- a/server/src/contexts/sales/application/Dealer/UpdateDealer.useCase.ts +++ b/server/src/contexts/sales/application/Dealer/UpdateDealer.useCase.ts @@ -11,6 +11,8 @@ import { DomainError, IDomainError, IUpdateDealer_Request_DTO, + KeyValueMap, + Language, Name, Result, UniqueID, @@ -107,9 +109,24 @@ export class UpdateDealerUseCase return Result.fail(nameOrError.error); } + const languageOrError = Language.createFromCode(dealerDTO.language); + if (languageOrError.isFailure) { + return Result.fail(languageOrError.error); + } + return Dealer.create( { name: nameOrError.object, + logo: "", + language: languageOrError.object, + + additionalInfo: KeyValueMap.create([ + ["contact_information", dealerDTO.contact_information], + ["default_payment_method", dealerDTO.default_payment_method], + ["default_notes", dealerDTO.default_notes], + ["default_legal_terms", dealerDTO.default_legal_terms], + ["default_quote_validity", dealerDTO.default_quote_validity], + ]).object, }, dealerId ); diff --git a/server/src/contexts/sales/application/Dealer/dealerServices.ts b/server/src/contexts/sales/application/Dealer/dealerServices.ts new file mode 100644 index 0000000..a84bef7 --- /dev/null +++ b/server/src/contexts/sales/application/Dealer/dealerServices.ts @@ -0,0 +1,23 @@ +import { IAdapter, RepositoryBuilder } from "@/contexts/common/domain"; +import { Dealer, IDealerRepository } from "@/contexts/sales/domain"; +import { UniqueID } from "@shared/contexts"; + +export const existsDealerByID = async ( + id: UniqueID, + adapter: IAdapter, + repository: RepositoryBuilder +): Promise => { + return await adapter + .startTransaction() + .complete(async (t) => repository({ transaction: t }).exists(id)); +}; + +export const findDealerByID = async ( + id: UniqueID, + adapter: IAdapter, + repository: RepositoryBuilder +): Promise => { + return await adapter + .startTransaction() + .complete(async (t) => repository({ transaction: t }).getById(id)); +}; diff --git a/server/src/contexts/sales/application/Dealer/index.ts b/server/src/contexts/sales/application/Dealer/index.ts index 64b29ca..aead955 100644 --- a/server/src/contexts/sales/application/Dealer/index.ts +++ b/server/src/contexts/sales/application/Dealer/index.ts @@ -4,3 +4,5 @@ export * from "./GetDealer.useCase"; export * from "./GetDealerByUser.useCase"; export * from "./ListDealers.useCase"; export * from "./UpdateDealer.useCase"; + +export * from "./dealerServices"; diff --git a/server/src/contexts/sales/domain/entities/Dealer.ts b/server/src/contexts/sales/domain/entities/Dealer.ts index be413e2..2d8b12b 100644 --- a/server/src/contexts/sales/domain/entities/Dealer.ts +++ b/server/src/contexts/sales/domain/entities/Dealer.ts @@ -1,14 +1,34 @@ -import { AggregateRoot, IDomainError, Name, Result, UniqueID } from "@shared/contexts"; +import { + AggregateRoot, + IDomainError, + KeyValueMap, + Language, + Name, + Result, + UniqueID, +} from "@shared/contexts"; +import { DealerRole } from "./DealerRole"; +import { DealerStatus } from "./DealerStatus"; export interface IDealerProps { - name: Name; user_id: UniqueID; + name: Name; + logo: string; + language: Language; + roles: DealerRole[]; + additionalInfo: KeyValueMap; + status: DealerStatus; } export interface IDealer { id: UniqueID; - name: Name; user_id: UniqueID; + name: Name; + language: Language; + + additionalInfo: KeyValueMap; + status: DealerStatus; + getRoles: () => DealerRole[]; } export class Dealer extends AggregateRoot implements IDealer { @@ -17,11 +37,47 @@ export class Dealer extends AggregateRoot implements IDealer { return Result.ok(user); } - get name(): Name { - return this.props.name; + private roles: DealerRole[]; + + constructor(props: IDealerProps, id?: UniqueID) { + const { roles } = props; + super(props, id); + this.roles = roles; } get user_id(): UniqueID { return this.props.user_id; } + + get name(): Name { + return this.props.name; + } + + get language(): Language { + return this.props.language; + } + + get status(): DealerStatus { + return this.props.status; + } + + get additionalInfo(): KeyValueMap { + return this.props.additionalInfo; + } + + get isUser(): boolean { + return this._hasRole(DealerRole.ROLE_USER); + } + + get isAdmin(): boolean { + return this._hasRole(DealerRole.ROLE_ADMIN); + } + + public getRoles(): DealerRole[] { + return this.roles; + } + + private _hasRole(role: DealerRole): boolean { + return (this.roles || []).some((r) => r.equals(role)); + } } diff --git a/server/src/contexts/sales/domain/entities/DealerRole.ts b/server/src/contexts/sales/domain/entities/DealerRole.ts new file mode 100644 index 0000000..121b33e --- /dev/null +++ b/server/src/contexts/sales/domain/entities/DealerRole.ts @@ -0,0 +1,3 @@ +import { AuthUserRole } from "@/contexts/auth/domain"; + +export class DealerRole extends AuthUserRole {} diff --git a/server/src/contexts/sales/domain/entities/DealerStatus.ts b/server/src/contexts/sales/domain/entities/DealerStatus.ts new file mode 100644 index 0000000..11edbac --- /dev/null +++ b/server/src/contexts/sales/domain/entities/DealerStatus.ts @@ -0,0 +1,88 @@ +import { + DomainError, + IValueObjectOptions, + Result, + RuleValidator, + ValueObject, + handleDomainError, +} from "@shared/contexts"; +import Joi from "joi"; + +export enum DEALER_STATUS { + ACTIVE = "active", + BLOQUED = "bloqued", + DISABLED = "disabled", +} +export interface IDealerStatusOptions extends IValueObjectOptions {} + +export class DealerStatus extends ValueObject { + public static readonly DEALER_STATUS = DEALER_STATUS; + + protected static validate(value: string, options: IValueObjectOptions) { + const rule = Joi.string() + .valid(DEALER_STATUS.ACTIVE, DEALER_STATUS.BLOQUED, DEALER_STATUS.DISABLED) + .label(options.label ? options.label : "status"); + + return RuleValidator.validate(rule, value); + } + + private static sanitize(status: string): string { + return String(status).trim().toLowerCase(); + } + + public static createActive(): DealerStatus { + return new DealerStatus(DEALER_STATUS.ACTIVE); + } + + public static createBloqued(): DealerStatus { + return new DealerStatus(DEALER_STATUS.BLOQUED); + } + + public static createDisabled(): DealerStatus { + return new DealerStatus(DEALER_STATUS.DISABLED); + } + + public static create(status: string, options: IDealerStatusOptions = {}) { + const _options = { + label: "status", + ...options, + }; + const validationResult = DealerStatus.validate(status, _options); + + if (validationResult.isFailure) { + return Result.fail( + handleDomainError(DomainError.INVALID_INPUT_DATA, validationResult.error.message, _options) + ); + } + + return Result.ok(new DealerStatus(DealerStatus.sanitize(validationResult.object))); + } + + get value(): string { + return String(this.props); + } + + public toString(): string { + return String(this.props); + } + + public toPrimitive(): string { + return this.toString(); + } + + public isActive(): boolean { + return this.equals(DealerStatus.createActive()); + } + + public isBloqued(): boolean { + return this.equals(DealerStatus.createBloqued()); + } + + public isDisabled(): boolean { + return this.equals(DealerStatus.createDisabled()); + } + + public isEmpty(): boolean { + return this.props === "undefined"; + } +} diff --git a/server/src/contexts/sales/domain/entities/index.ts b/server/src/contexts/sales/domain/entities/index.ts index 6e33f5e..2da3d7c 100644 --- a/server/src/contexts/sales/domain/entities/index.ts +++ b/server/src/contexts/sales/domain/entities/index.ts @@ -1,4 +1,5 @@ export * from "./Dealer"; +export * from "./DealerStatus"; export * from "./Quote"; export * from "./QuoteItem"; export * from "./QuoteStatus"; diff --git a/server/src/contexts/sales/infrastructure/mappers/dealer.mapper.ts b/server/src/contexts/sales/infrastructure/mappers/dealer.mapper.ts index 6685283..3483f8e 100644 --- a/server/src/contexts/sales/infrastructure/mappers/dealer.mapper.ts +++ b/server/src/contexts/sales/infrastructure/mappers/dealer.mapper.ts @@ -3,10 +3,11 @@ import { MapperParamsType, SequelizeMapper, } from "@/contexts/common/infrastructure"; -import { Name, UniqueID } from "@shared/contexts"; -import { Dealer, IDealerProps } from "../../domain/entities"; +import { KeyValueMap, Language, Name, UniqueID } from "@shared/contexts"; +import { Dealer, DealerStatus, IDealerProps } from "../../domain/entities"; +import { DealerRole } from "../../domain/entities/DealerRole"; import { ISalesContext } from "../Sales.context"; -import { DEALER_STATUS, DealerCreationAttributes, Dealer_Model } from "../sequelize"; +import { DealerCreationAttributes, Dealer_Model } from "../sequelize"; export interface IDealerMapper extends ISequelizeMapper {} @@ -21,8 +22,19 @@ class DealerMapper protected toDomainMappingImpl(source: Dealer_Model, params: any): Dealer { const props: IDealerProps = { - name: this.mapsValue(source, "name", Name.create), user_id: this.mapsValue(source, "user_id", UniqueID.create), + logo: "", + name: this.mapsValue(source, "name", Name.create), + status: this.mapsValue(source, "status", DealerStatus.create), + roles: this.mapsValue(source, "roles", DealerRole.create), + language: this.mapsValue(source, "language", Language.createFromCode), + additionalInfo: KeyValueMap.create([ + ["contact_information", source.contact_information], + ["default_payment_method", source.default_payment_method], + ["default_notes", source.default_notes], + ["default_legal_terms", source.default_legal_terms], + ["default_quote_validity", source.default_quote_validity], + ]).object, }; const id = this.mapsValue(source, "id", UniqueID.create); @@ -39,15 +51,16 @@ class DealerMapper return { id: source.id.toPrimitive(), user_id: source.user_id.toPrimitive(), - contact_id: undefined, + //contact_id: undefined, + logo: "", name: source.name.toPrimitive(), - contact_information: "", - default_payment_method: "", - default_notes: "", - default_legal_terms: "", - default_quote_validity: "", - status: DEALER_STATUS.STATUS_ACTIVE, - language: "", + status: source.status.toPrimitive(), + language: source.language.toPrimitive(), + contact_information: source.additionalInfo.get("contact_information")?.toString() ?? "", + default_payment_method: source.additionalInfo.get("default_payment_method")?.toString() ?? "", + default_notes: source.additionalInfo.get("default_notes")?.toString() ?? "", + default_legal_terms: source.additionalInfo.get("default_legal_terms")?.toString() ?? "", + default_quote_validity: source.additionalInfo.get("default_quote_validity")?.toString() ?? "", }; } } diff --git a/server/src/contexts/sales/infrastructure/sequelize/dealer.model.ts b/server/src/contexts/sales/infrastructure/sequelize/dealer.model.ts index 4e4bb6b..a9e72be 100644 --- a/server/src/contexts/sales/infrastructure/sequelize/dealer.model.ts +++ b/server/src/contexts/sales/infrastructure/sequelize/dealer.model.ts @@ -10,11 +10,6 @@ import { } from "sequelize"; import { Quote_Model } from "./quote.model"; -export enum DEALER_STATUS { - STATUS_ACTIVE = "active", - STATUS_BLOCKED = "blocked", -} - export type DealerCreationAttributes = InferCreationAttributes< Dealer_Model, { omit: "user" | "quotes" } @@ -54,7 +49,7 @@ export class Dealer_Model extends Model< declare default_notes: string; declare default_legal_terms: string; declare default_quote_validity: string; - declare status: DEALER_STATUS; + declare status: string; declare language: string; declare user?: NonAttribute; @@ -87,7 +82,7 @@ export default (sequelize: Sequelize) => { language: DataTypes.STRING, status: { - type: DataTypes.ENUM(...Object.values(DEALER_STATUS)), + type: DataTypes.STRING, allowNull: false, }, }, diff --git a/server/src/infrastructure/express/api/routes/profile.routes.ts b/server/src/infrastructure/express/api/routes/profile.routes.ts index 923df61..f567236 100644 --- a/server/src/infrastructure/express/api/routes/profile.routes.ts +++ b/server/src/infrastructure/express/api/routes/profile.routes.ts @@ -1,6 +1,5 @@ import { checkUser } from "@/contexts/auth"; import { createGetProfileController } from "@/contexts/profile/infrastructure"; -import { createUpdateUserController } from "@/contexts/users"; import Express from "express"; export const profileRouter = (appRouter: Express.Router) => { @@ -17,7 +16,7 @@ export const profileRouter = (appRouter: Express.Router) => { "/", checkUser, (req: Express.Request, res: Express.Response, next: Express.NextFunction) => - createUpdateUserController(res.locals["context"]).execute(req, res, next) + createUpdateProfileController(res.locals["context"]).execute(req, res, next) ); appRouter.use("/profile", profileRoutes); diff --git a/shared/lib/contexts/common/domain/entities/KeyValueMap.ts b/shared/lib/contexts/common/domain/entities/KeyValueMap.ts new file mode 100644 index 0000000..f8d772d --- /dev/null +++ b/shared/lib/contexts/common/domain/entities/KeyValueMap.ts @@ -0,0 +1,35 @@ +import { Result } from "./Result"; +import { Primitive, ValueObject } from "./ValueObject"; + +type KeyValueMapProps = Map; + +export class KeyValueMap extends ValueObject { + public static create(entries?: [string, Primitive][]) { + const map = new Map(entries); + return Result.ok(new KeyValueMap(map)); + } + + get(key: string): Primitive | undefined { + return this.props.get(key); + } + + set(key: string, value: Primitive): KeyValueMap { + const newMap = new Map(this.props); + newMap.set(key, value); + return new KeyValueMap(newMap); + } + + delete(key: string): KeyValueMap { + const newMap = new Map(this.props); + newMap.delete(key); + return new KeyValueMap(newMap); + } + + public toPrimitive(): string { + return JSON.stringify(Object.fromEntries(this.props.entries())); + } + + public entries(): [string, Primitive][] { + return Array.from(this.props.entries()); + } +} diff --git a/shared/lib/contexts/common/domain/entities/ValueObject.ts b/shared/lib/contexts/common/domain/entities/ValueObject.ts index a84c483..a897429 100644 --- a/shared/lib/contexts/common/domain/entities/ValueObject.ts +++ b/shared/lib/contexts/common/domain/entities/ValueObject.ts @@ -1,6 +1,6 @@ import { shallowEqual } from "shallow-equal-object"; -type Primitive = string | boolean | number; +export type Primitive = string | boolean | number; export interface IValueObjectOptions { label?: string; diff --git a/shared/lib/contexts/common/domain/entities/index.ts b/shared/lib/contexts/common/domain/entities/index.ts index 569c43e..ef38a3e 100644 --- a/shared/lib/contexts/common/domain/entities/index.ts +++ b/shared/lib/contexts/common/domain/entities/index.ts @@ -5,6 +5,7 @@ export * from "./Currency"; export * from "./Description"; export * from "./Email"; export * from "./Entity"; +export * from "./KeyValueMap"; export * from "./Language"; export * from "./Measure"; export * from "./MoneyValue"; diff --git a/shared/lib/contexts/profile/application/dto/GetProfile.dto/IGetProfile_Response.dto.ts b/shared/lib/contexts/profile/application/dto/GetProfile.dto/IGetProfile_Response.dto.ts index 266c846..dc4889b 100644 --- a/shared/lib/contexts/profile/application/dto/GetProfile.dto/IGetProfile_Response.dto.ts +++ b/shared/lib/contexts/profile/application/dto/GetProfile.dto/IGetProfile_Response.dto.ts @@ -1,13 +1,8 @@ export interface IGetProfileResponse_DTO { id: string; - name: string; - email: string; - language: string; - roles: string[]; contact_information: string; default_payment_method: string; default_notes: string; default_legal_terms: string; default_quote_validity: string; - status: string; } diff --git a/shared/lib/contexts/profile/application/dto/UpdateProfile.dto/IUpdateProfile_Request.dto.ts b/shared/lib/contexts/profile/application/dto/UpdateProfile.dto/IUpdateProfile_Request.dto.ts new file mode 100644 index 0000000..a69ce8b --- /dev/null +++ b/shared/lib/contexts/profile/application/dto/UpdateProfile.dto/IUpdateProfile_Request.dto.ts @@ -0,0 +1,28 @@ +import Joi from "joi"; +import { Result, RuleValidator } from "../../../../common"; + +export interface IUpdateProfile_Request_DTO { + contact_information: string; + default_payment_method: string; + default_notes: string; + default_legal_terms: string; + default_quote_validity: string; +} + +export function ensureUpdateProfile_Request_DTOIsValid(userDTO: IUpdateProfile_Request_DTO) { + const schema = Joi.object({ + contact_information: Joi.string(), + default_payment_method: Joi.string(), + default_notes: Joi.string(), + default_legal_terms: Joi.string(), + default_quote_validity: Joi.string(), + }).unknown(true); + + const result = RuleValidator.validate(schema, userDTO); + + if (result.isFailure) { + return Result.fail(result.error); + } + + return Result.ok(true); +} diff --git a/shared/lib/contexts/profile/application/dto/UpdateProfile.dto/IUpdateProfile_Response.dto.ts b/shared/lib/contexts/profile/application/dto/UpdateProfile.dto/IUpdateProfile_Response.dto.ts new file mode 100644 index 0000000..2cef31f --- /dev/null +++ b/shared/lib/contexts/profile/application/dto/UpdateProfile.dto/IUpdateProfile_Response.dto.ts @@ -0,0 +1,8 @@ +export interface IUpdateProfileResponse_DTO { + id: string; + contact_information: string; + default_payment_method: string; + default_notes: string; + default_legal_terms: string; + default_quote_validity: string; +} diff --git a/shared/lib/contexts/profile/application/dto/UpdateProfile.dto/index.ts b/shared/lib/contexts/profile/application/dto/UpdateProfile.dto/index.ts new file mode 100644 index 0000000..90e9cc0 --- /dev/null +++ b/shared/lib/contexts/profile/application/dto/UpdateProfile.dto/index.ts @@ -0,0 +1,2 @@ +export * from "./IUpdateProfile_Request.dto"; +export * from "./IUpdateProfile_Response.dto"; diff --git a/shared/lib/contexts/profile/application/dto/index.ts b/shared/lib/contexts/profile/application/dto/index.ts index 1a73968..44faa5f 100644 --- a/shared/lib/contexts/profile/application/dto/index.ts +++ b/shared/lib/contexts/profile/application/dto/index.ts @@ -1 +1,2 @@ export * from "./GetProfile.dto"; +export * from "./UpdateProfile.dto"; diff --git a/shared/lib/contexts/sales/application/dto/Dealer/UpdateDealer.dto/IUpdateDealer_Request.dto.ts b/shared/lib/contexts/sales/application/dto/Dealer/UpdateDealer.dto/IUpdateDealer_Request.dto.ts index 1aa76f7..c4c7408 100644 --- a/shared/lib/contexts/sales/application/dto/Dealer/UpdateDealer.dto/IUpdateDealer_Request.dto.ts +++ b/shared/lib/contexts/sales/application/dto/Dealer/UpdateDealer.dto/IUpdateDealer_Request.dto.ts @@ -3,11 +3,23 @@ import { Result, RuleValidator } from "../../../../../common"; export interface IUpdateDealer_Request_DTO { name: string; + language: string; + contact_information: string; + default_payment_method: string; + default_notes: string; + default_legal_terms: string; + default_quote_validity: string; } export function ensureUpdateDealer_Request_DTOIsValid(dealerDTO: IUpdateDealer_Request_DTO) { const schema = Joi.object({ name: Joi.string(), + language: Joi.string(), + contact_information: Joi.string(), + default_payment_method: Joi.string(), + default_notes: Joi.string(), + default_legal_terms: Joi.string(), + default_quote_validity: Joi.string(), }).unknown(true); const result = RuleValidator.validate(schema, dealerDTO);