This commit is contained in:
David Arranz 2024-06-17 18:54:30 +02:00
parent f77b6adb99
commit d405ce40d6
53 changed files with 1316 additions and 153 deletions

View File

@ -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 = () => {
<Alert variant='destructive'>
<AlertCircleIcon className='w-4 h-4' />
<AlertTitle>
<Trans i18nKey='login_page.error' />
<Trans i18nKey='common.error' />
</AlertTitle>
<AlertDescription>{form.formState.errors.root?.message}</AlertDescription>
</Alert>

View File

@ -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<IGetProfileResponse_DTO> => {
return useOne(
)
return dataProvider.getList({
resource: "invoices",
quickSearchTerm,
pagination,
});
};
}
}

View File

@ -0,0 +1,9 @@
import { PropsWithChildren, createContext } from "react";
export interface ISettingsContextState {}
export const SettingsContext = createContext<ISettingsContextState | null>(null);
export const SettingsProvider = ({ children }: PropsWithChildren) => {
return <SettingsContext.Provider value={{}}>{children}</SettingsContext.Provider>;
};

View File

@ -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<SettingsDataForm>({
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<SettingsDataForm> = async (data) => {
try {
setLoading(true);
console.log(data);
mutate(data);
} finally {
setLoading(false);
}
};
return (
<div className='mx-auto grid w-full max-w-6xl items-start gap-6 md:grid-cols-[180px_1fr] lg:grid-cols-[250px_1fr]'>
<nav className='grid gap-4 text-sm text-muted-foreground' x-chunk='dashboard-04-chunk-0'>
<Link to='#' className='font-semibold text-primary'>
General
</Link>
<Link to='#'>Security</Link>
<Link to='#'>Integrations</Link>
<Link to='#'>Support</Link>
<Link to='#'>Organizations</Link>
<Link to='#'>Advanced</Link>
</nav>
<div className='grid gap-6'>
<Card x-chunk='dashboard-04-chunk-1'>
<CardHeader>
<CardTitle>Store Name</CardTitle>
<CardDescription>Used to identify your store in the marketplace.</CardDescription>
</CardHeader>
<CardContent>
<form>
<Input placeholder='Store Name' />
</form>
</CardContent>
<CardFooter className='px-6 py-4 border-t'>
<Button>Save</Button>
</CardFooter>
</Card>
<Card x-chunk='dashboard-04-chunk-2'>
<CardHeader>
<CardTitle>Plugins Directory</CardTitle>
<CardDescription>
The directory within your project, in which your plugins are located.
</CardDescription>
</CardHeader>
<CardContent>
<form className='flex flex-col gap-4'>
<Input placeholder='Project Name' defaultValue='/content/plugins' />
<div className='flex items-center space-x-2'>
<Checkbox id='include' defaultChecked />
<label
htmlFor='include'
className='text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
>
Allow administrators to change the directory.
</label>
</div>
</form>
</CardContent>
<CardFooter className='px-6 py-4 border-t'>
<Button>Save</Button>
</CardFooter>
</Card>
</div>
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className='mx-auto grid w-full max-w-6xl items-start gap-6 md:grid-cols-[180px_1fr] lg:grid-cols-[250px_1fr]'>
{form.formState.errors.root?.message && (
<Alert variant='destructive'>
<AlertCircleIcon className='w-4 h-4' />
<AlertTitle>
<Trans i18nKey='common.error' />
</AlertTitle>
<AlertDescription>{form.formState.errors.root?.message}</AlertDescription>
</Alert>
)}
<nav className='grid gap-4 text-sm text-muted-foreground'>
<Link to='#' className='font-semibold text-primary'>
<Trans i18nKey='settings.quotes.title' />
</Link>
<Link to='#'>
<Trans i18nKey='settings.quotes.general' />
</Link>
<Link to='#'>Integrations</Link>
<Link to='#'>Support</Link>
<Link to='#'>Organizations</Link>
<Link to='#'>Advanced</Link>
</nav>
<div className='grid gap-6'>
<Card>
<CardHeader>
<CardTitle>
<Trans i18nKey='settings.quotes.contact_information.label' />
</CardTitle>
<CardDescription>
<Trans i18nKey='settings.quotes.contact_information.desc' />
</CardDescription>
</CardHeader>
<CardContent>
<FormTextAreaField
disabled={loading}
placeholder={t("settings.quotes.contact_information.placeholder")}
{...form.register("contact_information", {
required: true,
})}
errors={form.formState.errors}
/>
</CardContent>
<CardFooter className='px-6 py-4 border-t'>
<Button>
<Trans i18nKey='common.save' />
</Button>
</CardFooter>
</Card>
<Card>
<CardHeader>
<CardTitle>
<Trans i18nKey='settings.quotes.default_payment_method.label' />
</CardTitle>
<CardDescription>
<Trans i18nKey='settings.quotes.default_payment_method.desc' />
</CardDescription>
</CardHeader>
<CardContent>
<FormTextAreaField
disabled={loading}
placeholder={t("settings.quotes.default_payment_method.placeholder")}
{...form.register("default_payment_method", {
required: true,
})}
errors={form.formState.errors}
/>
</CardContent>
<CardFooter className='px-6 py-4 border-t'>
<Button>
<Trans i18nKey='common.save' />
</Button>
</CardFooter>
</Card>
<Card>
<CardHeader>
<CardTitle>
<Trans i18nKey='settings.quotes.default_quote_validity.label' />
</CardTitle>
<CardDescription>
<Trans i18nKey='settings.quotes.default_quote_validity.desc' />
</CardDescription>
</CardHeader>
<CardContent>
<FormTextAreaField
disabled={loading}
placeholder={t("settings.quotes.default_quote_validity.placeholder")}
{...form.register("default_quote_validity", {
required: true,
})}
errors={form.formState.errors}
/>
</CardContent>
<CardFooter className='px-6 py-4 border-t'>
<Button>
<Trans i18nKey='common.save' />
</Button>
</CardFooter>
</Card>
<Card>
<CardHeader>
<CardTitle>
<Trans i18nKey='settings.quotes.default_notes.label' />
</CardTitle>
<CardDescription>
<Trans i18nKey='settings.quotes.default_notes.desc' />
</CardDescription>
</CardHeader>
<CardContent>
<FormTextAreaField
disabled={loading}
placeholder={t("settings.quotes.default_notes.placeholder")}
{...form.register("default_notes", {
required: true,
})}
errors={form.formState.errors}
/>
</CardContent>
<CardFooter className='px-6 py-4 border-t'>
<Button>
<Trans i18nKey='common.save' />
</Button>
</CardFooter>
</Card>
<Card>
<CardHeader>
<CardTitle>
<Trans i18nKey='settings.quotes.default_legal_terms.label' />
</CardTitle>
<CardDescription>
<Trans i18nKey='settings.quotes.default_legal_terms.desc' />
</CardDescription>
</CardHeader>
<CardContent>
<FormTextAreaField
disabled={loading}
placeholder={t("settings.quotes.default_legal_terms.placeholder")}
{...form.register("default_legal_terms", {
required: true,
})}
errors={form.formState.errors}
/>
</CardContent>
<CardFooter className='px-6 py-4 border-t'>
<Button>
<Trans i18nKey='common.save' />
</Button>
</CardFooter>
</Card>
</div>
</div>
</form>
</Form>
);
};

View File

@ -0,0 +1 @@
export * from "./useSettings";

View File

@ -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<string, unknown>;
};
export const useSettings = (params?: UseSettingsGetParamsType) => {
const dataSource = useDataSource();
const keys = useQueryKey();
return {
query: useOne<IGetProfileResponse_DTO>({
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: "",
}),
}),
};
};

View File

@ -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;
};

View File

@ -21,12 +21,14 @@ export type UseOneQueryOptions<TUseOneQueryData, TUseOneQueryError> = {
queryOptions?: Record<string, unknown>;
} & UseLoadingOvertimeOptionsProps;
export type UseOneQueryResult<TUseOneQueryData, TUseOneQueryError> =
UseQueryResult<TUseOneQueryData, TUseOneQueryError> & {
isEmpty: boolean;
} & UseLoadingOvertimeReturnType;
export type UseOneQueryResult<TUseOneQueryData, TUseOneQueryError> = UseQueryResult<
TUseOneQueryData,
TUseOneQueryError
> & {
isEmpty: boolean;
} & UseLoadingOvertimeReturnType;
export function useOne<TUseOneQueryData, TUseOneQueryError>({
export function useOne<TUseOneQueryData, TUseOneQueryError = Error>({
queryKey,
queryFn,
enabled,

View File

@ -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"
}
}
}
}
}

View File

@ -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" }]
}

View File

@ -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<never, IUseCaseError> // Misc errors (value objects)
| Result<Profile, never>; // Success!
export class GetProfileUseCase
implements IUseCase<IGetProfileUseCaseRequest, Promise<GetProfileResponseOrError>>
{
private _adapter: ISequelizeAdapter;
private _repositoryManager: IRepositoryManager;
constructor(props: { adapter: ISequelizeAdapter; repositoryManager: IRepositoryManager }) {
this._adapter = props.adapter;
this._repositoryManager = props.repositoryManager;
}
private getRepositoryByName<T>(name: string) {
return this._repositoryManager.getRepository<T>(name);
}
async execute(request: IGetProfileUseCaseRequest): Promise<GetProfileResponseOrError> {
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<IProfileRepository>("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>(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<IProfileRepository>("Profile");
}
}

View File

@ -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<never, IUseCaseError> // Misc errors (value objects)
| Result<Profile, never>; // Success!
export class UpdateProfileUseCase
implements IUseCase<IUpdateProfileUseCaseRequest, Promise<UpdateProfileResponseOrError>>
{
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<UpdateProfileResponseOrError> {
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<Profile>(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<IProfileRepository>("Profile");
}
}

View File

@ -0,0 +1 @@
export * from "./UpdateProfile.useCase";

View File

@ -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<IProfileRepository>
): Promise<Profile | null> => {
return await adapter
.startTransaction()
.complete(async (t) => repository({ transaction: t }).getById(id));
};

View File

@ -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<IProfileProps> implements IProfile {
public static create(props: IProfileProps, id?: UniqueID): Result<Profile, IDomainError> {
const profile = new Profile(props, id);
return Result.ok<Profile>(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;
}
}

View File

@ -0,0 +1 @@
export * from "./Profile";

View File

@ -0,0 +1,2 @@
export * from "./entities";
export * from "./repository";

View File

@ -0,0 +1,9 @@
import { IRepository } from "@/contexts/common/domain";
import { UniqueID } from "@shared/contexts";
import { Profile } from "../entities";
export interface IProfileRepository extends IRepository<any> {
exists(id: UniqueID): Promise<boolean>;
getById(id: UniqueID): Promise<Profile | null>;
update(profile: Profile): Promise<void>;
}

View File

@ -0,0 +1 @@
export * from "./ProfileRepository.interface";

View File

@ -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<Profile> 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<boolean> {
return this._exists("Profile_Model", "id", id.toPrimitive());
}
public async getById(id: UniqueID): Promise<Profile | null> {
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<void> {
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),
});
});
};

View File

@ -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 = <Dealer>result.object;
const profile = result.object;
return this.ok<IGetUserResponse_DTO>(this.presenter.map(user, dealer, this.context));
return this.ok<IGetProfileResponse_DTO>(this.presenter.map(profile, this.context));
} catch (e: unknown) {
return this.fail(e as IServerError);
}

View File

@ -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
);

View File

@ -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,
};
},
};

View File

@ -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",
};
},
};

View File

@ -1 +1 @@
export * from "./GetUser.presenter";
export * from "./GetProfile.presenter";

View File

@ -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 = <User | undefined>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<IUpdateProfileResponse_DTO>(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);
}
}
}

View File

@ -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
);
};

View File

@ -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,
};
},
};

View File

@ -0,0 +1 @@
export * from "./UpdateUser.presenter";

View File

@ -0,0 +1 @@
export * from "./profile.mapper";

View File

@ -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<Profile_Model, ProfileCreationAttributes, Profile> {}
class ProfileMapper
extends SequelizeMapper<Profile_Model, ProfileCreationAttributes, Profile>
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,
});

View File

@ -0,0 +1 @@
export * from "./profile.model";

View File

@ -0,0 +1,54 @@
import { DataTypes, InferAttributes, InferCreationAttributes, Model, Sequelize } from "sequelize";
export type ProfileCreationAttributes = InferCreationAttributes<Profile_Model>;
export class Profile_Model extends Model<
InferAttributes<Profile_Model>,
InferCreationAttributes<Profile_Model>
> {
// To avoid table creation
/*static async sync(): Promise<any> {
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;
};

View File

@ -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<T>(name: string) {
return this._repositoryManager.getRepository<T>(name);
}
async execute(request: IGetDealerUseCaseRequest): Promise<GetDealerResponseOrError> {
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<IDealerRepository>("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<IDealerRepository>("Dealer");
}
}

View File

@ -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
);

View File

@ -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<IDealerRepository>
): Promise<boolean> => {
return await adapter
.startTransaction()
.complete(async (t) => repository({ transaction: t }).exists(id));
};
export const findDealerByID = async (
id: UniqueID,
adapter: IAdapter,
repository: RepositoryBuilder<IDealerRepository>
): Promise<Dealer | null> => {
return await adapter
.startTransaction()
.complete(async (t) => repository({ transaction: t }).getById(id));
};

View File

@ -4,3 +4,5 @@ export * from "./GetDealer.useCase";
export * from "./GetDealerByUser.useCase";
export * from "./ListDealers.useCase";
export * from "./UpdateDealer.useCase";
export * from "./dealerServices";

View File

@ -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<IDealerProps> implements IDealer {
@ -17,11 +37,47 @@ export class Dealer extends AggregateRoot<IDealerProps> implements IDealer {
return Result.ok<Dealer>(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));
}
}

View File

@ -0,0 +1,3 @@
import { AuthUserRole } from "@/contexts/auth/domain";
export class DealerRole extends AuthUserRole {}

View File

@ -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<string> {
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<string>(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";
}
}

View File

@ -1,4 +1,5 @@
export * from "./Dealer";
export * from "./DealerStatus";
export * from "./Quote";
export * from "./QuoteItem";
export * from "./QuoteStatus";

View File

@ -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<Dealer_Model, DealerCreationAttributes, Dealer> {}
@ -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() ?? "",
};
}
}

View File

@ -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<User_Model>;
@ -87,7 +82,7 @@ export default (sequelize: Sequelize) => {
language: DataTypes.STRING,
status: {
type: DataTypes.ENUM(...Object.values(DEALER_STATUS)),
type: DataTypes.STRING,
allowNull: false,
},
},

View File

@ -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);

View File

@ -0,0 +1,35 @@
import { Result } from "./Result";
import { Primitive, ValueObject } from "./ValueObject";
type KeyValueMapProps = Map<string, Primitive>;
export class KeyValueMap extends ValueObject<KeyValueMapProps> {
public static create(entries?: [string, Primitive][]) {
const map = new Map<string, Primitive>(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());
}
}

View File

@ -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;

View File

@ -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";

View File

@ -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;
}

View File

@ -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<IUpdateProfile_Request_DTO>(schema, userDTO);
if (result.isFailure) {
return Result.fail(result.error);
}
return Result.ok(true);
}

View File

@ -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;
}

View File

@ -0,0 +1,2 @@
export * from "./IUpdateProfile_Request.dto";
export * from "./IUpdateProfile_Response.dto";

View File

@ -1 +1,2 @@
export * from "./GetProfile.dto";
export * from "./UpdateProfile.dto";

View File

@ -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<IUpdateDealer_Request_DTO>(schema, dealerDTO);