- Implement tests for UpdateProfile use case to validate language persistence and behavior when lang_code is omitted.
- Create a new client asset file for future use.
This commit is contained in:
parent
839e1a5222
commit
988790f904
@ -19,6 +19,11 @@ import {
|
|||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
Switch,
|
Switch,
|
||||||
} from "@/ui";
|
} from "@/ui";
|
||||||
|
|
||||||
@ -27,6 +32,7 @@ import { AlertCircleIcon } from "lucide-react";
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Controller, SubmitHandler, useForm } from "react-hook-form";
|
import { Controller, SubmitHandler, useForm } from "react-hook-form";
|
||||||
import { Trans } from "react-i18next";
|
import { Trans } from "react-i18next";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { useUnsavedChangesNotifier } from "@/lib/hooks";
|
import { useUnsavedChangesNotifier } from "@/lib/hooks";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@ -40,11 +46,13 @@ export const SettingsEditor = () => {
|
|||||||
const [activeSection, setActiveSection] = useState("profile");
|
const [activeSection, setActiveSection] = useState("profile");
|
||||||
const { useOne, useUpdate, useUploadLogo } = useSettings();
|
const { useOne, useUpdate, useUploadLogo } = useSettings();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
|
||||||
const { data, status, error: queryError } = useOne();
|
const { data, status, error: queryError } = useOne();
|
||||||
|
|
||||||
const defaultValues = useMemo(
|
const defaultValues = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
lang_code: "es",
|
||||||
contact_information: "",
|
contact_information: "",
|
||||||
default_payment_method: "",
|
default_payment_method: "",
|
||||||
default_notes: "",
|
default_notes: "",
|
||||||
@ -65,7 +73,7 @@ export const SettingsEditor = () => {
|
|||||||
|
|
||||||
const form = useForm<SettingsDataForm>({
|
const form = useForm<SettingsDataForm>({
|
||||||
mode: "onBlur",
|
mode: "onBlur",
|
||||||
values: data?.dealer,
|
values: data ? { ...data.dealer, lang_code: data.lang_code } : undefined,
|
||||||
defaultValues,
|
defaultValues,
|
||||||
/*resolver: joiResolver(
|
/*resolver: joiResolver(
|
||||||
Joi.object({
|
Joi.object({
|
||||||
@ -130,6 +138,9 @@ export const SettingsEditor = () => {
|
|||||||
},
|
},
|
||||||
//onSettled: () => {},
|
//onSettled: () => {},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
if (i18n.language !== data.lang_code) {
|
||||||
|
void i18n.changeLanguage(data.lang_code ?? "es");
|
||||||
|
}
|
||||||
reset(getValues());
|
reset(getValues());
|
||||||
toast({ description: "Ajustes guardados", variant: "success" });
|
toast({ description: "Ajustes guardados", variant: "success" });
|
||||||
},
|
},
|
||||||
@ -189,6 +200,38 @@ export const SettingsEditor = () => {
|
|||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
<div className={cn("grid gap-6", activeSection === "profile" ? "visible" : "hidden")}>
|
<div className={cn("grid gap-6", activeSection === "profile" ? "visible" : "hidden")}>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>
|
||||||
|
<Trans i18nKey='settings.form_fields.language.label' />
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
<Trans i18nKey='settings.form_fields.language.desc' />
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
name='lang_code'
|
||||||
|
render={({ field }) => (
|
||||||
|
<Select value={field.value} onValueChange={field.onChange}>
|
||||||
|
<SelectTrigger aria-label={t("settings.form_fields.language.label")}>
|
||||||
|
<SelectValue placeholder={t("settings.form_fields.language.placeholder")} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value='es'>Español</SelectItem>
|
||||||
|
<SelectItem value='en'>English</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className='px-6 py-4 border-t'>
|
||||||
|
<Button>
|
||||||
|
<Trans i18nKey='common.save' />
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>
|
<CardTitle>
|
||||||
|
|||||||
@ -17,11 +17,13 @@ export type UseSettingsGetParamsType = {
|
|||||||
export const useSettings = (params?: UseSettingsGetParamsType) => {
|
export const useSettings = (params?: UseSettingsGetParamsType) => {
|
||||||
const dataSource = useDataSource();
|
const dataSource = useDataSource();
|
||||||
const keys = useQueryKey();
|
const keys = useQueryKey();
|
||||||
|
const settingsQueryKey = keys().data().resource("settings").action("one").id("me").params().get();
|
||||||
|
const profileQueryKey = keys().auth().action("profile").get();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
useOne: () =>
|
useOne: () =>
|
||||||
useOne<IGetProfileResponse_DTO>({
|
useOne<IGetProfileResponse_DTO>({
|
||||||
queryKey: keys().data().resource("settings").action("one").id("me").params().get(),
|
queryKey: settingsQueryKey,
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
dataSource.getOne({
|
dataSource.getOne({
|
||||||
resource: "profile",
|
resource: "profile",
|
||||||
@ -34,7 +36,7 @@ export const useSettings = (params?: UseSettingsGetParamsType) => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<IUpdateProfileResponse_DTO, TDataSourceError, IUpdateProfile_Request_DTO>({
|
return useMutation<IUpdateProfileResponse_DTO, TDataSourceError, IUpdateProfile_Request_DTO>({
|
||||||
mutationKey: keys().data().resource("settings").action("one").id("me").params().get(),
|
mutationKey: settingsQueryKey,
|
||||||
mutationFn: (data) => {
|
mutationFn: (data) => {
|
||||||
return dataSource.updateOne({
|
return dataSource.updateOne({
|
||||||
resource: "profile",
|
resource: "profile",
|
||||||
@ -42,10 +44,23 @@ export const useSettings = (params?: UseSettingsGetParamsType) => {
|
|||||||
id: "",
|
id: "",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: (updatedDealer, variables) => {
|
||||||
queryClient.invalidateQueries({
|
const updateCachedProfile = (profile: IGetProfileResponse_DTO | undefined) =>
|
||||||
queryKey: ["data", "default", "settings"],
|
profile
|
||||||
});
|
? {
|
||||||
|
...profile,
|
||||||
|
lang_code: variables.lang_code ?? profile.lang_code,
|
||||||
|
dealer: { ...profile.dealer, ...updatedDealer },
|
||||||
|
}
|
||||||
|
: profile;
|
||||||
|
|
||||||
|
queryClient.setQueryData<IGetProfileResponse_DTO>(settingsQueryKey, updateCachedProfile);
|
||||||
|
queryClient.setQueryData<IGetProfileResponse_DTO>(profileQueryKey, updateCachedProfile);
|
||||||
|
|
||||||
|
return Promise.all([
|
||||||
|
queryClient.invalidateQueries({ queryKey: settingsQueryKey }),
|
||||||
|
queryClient.invalidateQueries({ queryKey: profileQueryKey }),
|
||||||
|
]);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { ProtectedRoute } from "./ProtectedRoute";
|
|||||||
|
|
||||||
const useIsLoggedIn = jest.fn();
|
const useIsLoggedIn = jest.fn();
|
||||||
const useGetProfile = jest.fn();
|
const useGetProfile = jest.fn();
|
||||||
|
const changeLanguage = jest.fn();
|
||||||
|
|
||||||
jest.mock("@/lib/hooks", () => ({
|
jest.mock("@/lib/hooks", () => ({
|
||||||
useIsLoggedIn: () => useIsLoggedIn(),
|
useIsLoggedIn: () => useIsLoggedIn(),
|
||||||
@ -16,6 +17,10 @@ jest.mock("@/components", () => ({
|
|||||||
LoadingOverlay: () => <div data-testid='loading' />,
|
LoadingOverlay: () => <div data-testid='loading' />,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock("react-i18next", () => ({
|
||||||
|
useTranslation: () => ({ i18n: { language: "en", changeLanguage } }),
|
||||||
|
}));
|
||||||
|
|
||||||
const renderProtectedRoute = () => {
|
const renderProtectedRoute = () => {
|
||||||
const container = document.createElement("div");
|
const container = document.createElement("div");
|
||||||
document.body.appendChild(container);
|
document.body.appendChild(container);
|
||||||
@ -48,6 +53,7 @@ describe("ProtectedRoute", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
changeLanguage.mockClear();
|
||||||
useIsLoggedIn.mockReturnValue({
|
useIsLoggedIn.mockReturnValue({
|
||||||
isSuccess: true,
|
isSuccess: true,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@ -80,6 +86,7 @@ describe("ProtectedRoute", () => {
|
|||||||
const container = renderProtectedRoute();
|
const container = renderProtectedRoute();
|
||||||
|
|
||||||
expect(container.querySelector("[data-testid='quotes']")).not.toBeNull();
|
expect(container.querySelector("[data-testid='quotes']")).not.toBeNull();
|
||||||
|
expect(changeLanguage).toHaveBeenCalledWith("es");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders an error instead of treating a profile request failure as a logout", () => {
|
it("renders an error instead of treating a profile request failure as a logout", () => {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { ErrorOverlay, LoadingOverlay } from "@/components";
|
import { ErrorOverlay, LoadingOverlay } from "@/components";
|
||||||
import { useGetProfile, useIsLoggedIn } from "@/lib/hooks";
|
import { useGetProfile, useIsLoggedIn } from "@/lib/hooks";
|
||||||
import React, { useId } from "react";
|
import React, { useId } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { Navigate, useLocation } from "react-router-dom";
|
import { Navigate, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
type ProctectRouteProps = {
|
type ProctectRouteProps = {
|
||||||
@ -9,7 +10,7 @@ type ProctectRouteProps = {
|
|||||||
|
|
||||||
export const ProtectedRoute = ({ children }: ProctectRouteProps) => {
|
export const ProtectedRoute = ({ children }: ProctectRouteProps) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
//const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
const id = useId();
|
const id = useId();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -28,12 +29,11 @@ export const ProtectedRoute = ({ children }: ProctectRouteProps) => {
|
|||||||
enabled: isLoggedInSuccess && authenticated,
|
enabled: isLoggedInSuccess && authenticated,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cambiamos el idioma de la aplicación si es necesario
|
React.useEffect(() => {
|
||||||
/*React.useEffect(() => {
|
|
||||||
if (isProfileSuccess && profile?.lang_code && i18n.language !== profile.lang_code) {
|
if (isProfileSuccess && profile?.lang_code && i18n.language !== profile.lang_code) {
|
||||||
i18n.changeLanguage(profile.lang_code);
|
void i18n.changeLanguage(profile.lang_code);
|
||||||
}
|
}
|
||||||
}, [isProfileSuccess, profile?.lang_code, i18n]);*/
|
}, [isProfileSuccess, profile?.lang_code, i18n]);
|
||||||
|
|
||||||
if (isLoggedInLoading || isProfileLoading || (isProfileSuccess && profile === null && isProfileFetching)) {
|
if (isLoggedInLoading || isProfileLoading || (isProfileSuccess && profile === null && isProfileFetching)) {
|
||||||
return <LoadingOverlay />;
|
return <LoadingOverlay />;
|
||||||
|
|||||||
@ -438,6 +438,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"form_fields": {
|
"form_fields": {
|
||||||
|
"language": {
|
||||||
|
"label": "Language",
|
||||||
|
"placeholder": "Select a language",
|
||||||
|
"desc": "Preferred language for the interface"
|
||||||
|
},
|
||||||
"image": {
|
"image": {
|
||||||
"label": "Logotype",
|
"label": "Logotype",
|
||||||
"placeholder": "",
|
"placeholder": "",
|
||||||
|
|||||||
@ -440,6 +440,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"form_fields": {
|
"form_fields": {
|
||||||
|
"language": {
|
||||||
|
"label": "Idioma",
|
||||||
|
"placeholder": "Selecciona un idioma",
|
||||||
|
"desc": "Idioma preferido para la interfaz"
|
||||||
|
},
|
||||||
"logo": {
|
"logo": {
|
||||||
"label": "Logotipo",
|
"label": "Logotipo",
|
||||||
"placeholder": "",
|
"placeholder": "",
|
||||||
|
|||||||
254
dist/client/assets/index-BVZyG6kc.js
vendored
Normal file
254
dist/client/assets/index-BVZyG6kc.js
vendored
Normal file
File diff suppressed because one or more lines are too long
254
dist/client/assets/index-CCfX-UYJ.js
vendored
254
dist/client/assets/index-CCfX-UYJ.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
dist/client/index.html
vendored
2
dist/client/index.html
vendored
@ -8,7 +8,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link href="https://fonts.upset.dev/css2?family=Poppins&display=swap" rel="stylesheet" />
|
<link href="https://fonts.upset.dev/css2?family=Poppins&display=swap" rel="stylesheet" />
|
||||||
<title>Presupuestador Uecko</title>
|
<title>Presupuestador Uecko</title>
|
||||||
<script type="module" crossorigin src="/assets/index-CJmQk4ZY.js"></script>
|
<script type="module" crossorigin src="/assets/index-QDyDWtz5.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-CBtV2-Es.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-CBtV2-Es.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,104 @@
|
|||||||
|
import { Language, UniqueID } from "@shared/contexts";
|
||||||
|
import { createProfileMapper } from "../infrastructure/mappers/profile.mapper";
|
||||||
|
import { createUserMapper } from "@/contexts/users/infrastructure/mappers/user.mapper";
|
||||||
|
import { UpdateProfileUseCase } from "./UpdateProfile.useCase";
|
||||||
|
|
||||||
|
const userId = "770672b9-5a87-4e8e-a8af-8b44a7432e12";
|
||||||
|
|
||||||
|
const profileSource = {
|
||||||
|
default_show_line_discount_column: true,
|
||||||
|
dataValues: {
|
||||||
|
id: "9b3cb7f7-3747-4eb4-bf9e-82f2f1ad03d6",
|
||||||
|
name: "Dealer",
|
||||||
|
status: "actived",
|
||||||
|
lang_code: "es",
|
||||||
|
currency_code: "EUR",
|
||||||
|
logo: "",
|
||||||
|
contact_information: "",
|
||||||
|
default_payment_method: "",
|
||||||
|
default_notes: "",
|
||||||
|
default_legal_terms: "",
|
||||||
|
default_quote_validity: "",
|
||||||
|
default_tax: 2100,
|
||||||
|
default_tax_label: "% IVA",
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const userSource = {
|
||||||
|
roles: ["ROLE_USER"],
|
||||||
|
dataValues: {
|
||||||
|
id: userId,
|
||||||
|
name: "User",
|
||||||
|
email: "user@example.test",
|
||||||
|
password: "$2b$10$abcdefghijklmnopqrstuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu",
|
||||||
|
lang_code: "es",
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const profileDTO = {
|
||||||
|
contact_information: "Contact",
|
||||||
|
default_payment_method: "Transfer",
|
||||||
|
default_notes: "Notes",
|
||||||
|
default_legal_terms: "Terms",
|
||||||
|
default_quote_validity: "30 days",
|
||||||
|
default_tax: { amount: 2100, scale: 2 },
|
||||||
|
default_tax_label: "% IVA",
|
||||||
|
default_show_line_discount_column: true,
|
||||||
|
logo: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("UpdateProfileUseCase language", () => {
|
||||||
|
const profile = createProfileMapper({} as any).mapToDomain(profileSource);
|
||||||
|
const user = createUserMapper({} as any).mapToDomain(userSource);
|
||||||
|
|
||||||
|
const profileRepo = {
|
||||||
|
getByUserId: jest.fn(),
|
||||||
|
update: jest.fn(),
|
||||||
|
};
|
||||||
|
const userRepo = {
|
||||||
|
getById: jest.fn(),
|
||||||
|
update: jest.fn(),
|
||||||
|
};
|
||||||
|
const adapter = {
|
||||||
|
startTransaction: () => ({ complete: async (callback: (transaction: any) => Promise<void>) => callback({}) }),
|
||||||
|
};
|
||||||
|
const repositoryManager = {
|
||||||
|
getRepository: (name: string) => () => (name === "Profile" ? profileRepo : userRepo),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
profileRepo.getByUserId.mockResolvedValue(profile);
|
||||||
|
userRepo.getById.mockResolvedValue(user);
|
||||||
|
profileRepo.update.mockResolvedValue(undefined);
|
||||||
|
userRepo.update.mockResolvedValue(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("persists the selected language on the user", async () => {
|
||||||
|
const useCase = new UpdateProfileUseCase({ adapter: adapter as any, repositoryManager: repositoryManager as any });
|
||||||
|
|
||||||
|
const result = await useCase.execute({
|
||||||
|
userId: UniqueID.create(userId).object,
|
||||||
|
profileDTO: { ...profileDTO, lang_code: "en" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.isSuccess).toBe(true);
|
||||||
|
expect(user.language.code).toBe("en");
|
||||||
|
expect(userRepo.update).toHaveBeenCalledWith(user);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not change the language when lang_code is omitted", async () => {
|
||||||
|
user.language = Language.createFromCode("en").object;
|
||||||
|
const useCase = new UpdateProfileUseCase({ adapter: adapter as any, repositoryManager: repositoryManager as any });
|
||||||
|
|
||||||
|
const result = await useCase.execute({
|
||||||
|
userId: UniqueID.create(userId).object,
|
||||||
|
profileDTO,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.isSuccess).toBe(true);
|
||||||
|
expect(user.language.code).toBe("en");
|
||||||
|
expect(userRepo.getById).not.toHaveBeenCalled();
|
||||||
|
expect(userRepo.update).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -9,12 +9,14 @@ import { IInfrastructureError } from "@/contexts/common/infrastructure";
|
|||||||
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
|
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
|
||||||
import {
|
import {
|
||||||
IUpdateProfile_Request_DTO,
|
IUpdateProfile_Request_DTO,
|
||||||
|
Language,
|
||||||
Percentage,
|
Percentage,
|
||||||
Result,
|
Result,
|
||||||
TextValueObject,
|
TextValueObject,
|
||||||
UniqueID,
|
UniqueID,
|
||||||
} from "@shared/contexts";
|
} from "@shared/contexts";
|
||||||
import { IProfileRepository, Profile } from "../domain";
|
import { IProfileRepository, Profile } from "../domain";
|
||||||
|
import { IUserRepository, User } from "@/contexts/users/domain";
|
||||||
|
|
||||||
export interface IUpdateProfileUseCaseRequest extends IUseCaseRequest {
|
export interface IUpdateProfileUseCaseRequest extends IUseCaseRequest {
|
||||||
userId: UniqueID;
|
userId: UniqueID;
|
||||||
@ -52,6 +54,22 @@ export class UpdateProfileUseCase
|
|||||||
|
|
||||||
const profile = exitsOrError.object;
|
const profile = exitsOrError.object;
|
||||||
|
|
||||||
|
let user: User | null = null;
|
||||||
|
if (profileDTO.lang_code !== undefined) {
|
||||||
|
const languageOrError = Language.createFromCode(profileDTO.lang_code);
|
||||||
|
if (languageOrError.isFailure) {
|
||||||
|
return Result.fail(languageOrError.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userOrError = await this._getUser(userId);
|
||||||
|
if (userOrError.isFailure) {
|
||||||
|
return Result.fail(userOrError.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
user = userOrError.object;
|
||||||
|
user.language = languageOrError.object;
|
||||||
|
}
|
||||||
|
|
||||||
// Actualizar el perfil con datos actualizados
|
// Actualizar el perfil con datos actualizados
|
||||||
profile.contactInformation = TextValueObject.create(profileDTO.contact_information).object;
|
profile.contactInformation = TextValueObject.create(profileDTO.contact_information).object;
|
||||||
profile.defaultPaymentMethod = TextValueObject.create(profileDTO.default_payment_method).object;
|
profile.defaultPaymentMethod = TextValueObject.create(profileDTO.default_payment_method).object;
|
||||||
@ -78,18 +96,24 @@ export class UpdateProfileUseCase
|
|||||||
profile.defaultTax = taxOrError.object;
|
profile.defaultTax = taxOrError.object;
|
||||||
|
|
||||||
// Guardar los cambios
|
// Guardar los cambios
|
||||||
return this._saveProfile(profile);
|
return this._saveProfile(profile, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _saveProfile(updatedProfile: Profile) {
|
private async _saveProfile(updatedProfile: Profile, updatedUser: User | null) {
|
||||||
// Guardar el contacto
|
// Guardar el contacto
|
||||||
const transaction = this._adapter.startTransaction();
|
const transaction = this._adapter.startTransaction();
|
||||||
const profileRepository = this._getProfileRepository();
|
const profileRepository = this._getProfileRepository();
|
||||||
|
const userRepository = this._getUserRepository();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await transaction.complete(async (t) => {
|
await transaction.complete(async (t) => {
|
||||||
const profileRepo = profileRepository({ transaction: t });
|
const profileRepo = profileRepository({ transaction: t });
|
||||||
await profileRepo.update(updatedProfile);
|
await profileRepo.update(updatedProfile);
|
||||||
|
|
||||||
|
if (updatedUser) {
|
||||||
|
const userRepo = userRepository({ transaction: t });
|
||||||
|
await userRepo.update(updatedUser);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return Result.ok<Profile>(updatedProfile);
|
return Result.ok<Profile>(updatedProfile);
|
||||||
@ -127,4 +151,26 @@ export class UpdateProfileUseCase
|
|||||||
private _getProfileRepository() {
|
private _getProfileRepository() {
|
||||||
return this._repositoryManager.getRepository<IProfileRepository>("Profile");
|
return this._repositoryManager.getRepository<IProfileRepository>("Profile");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _getUser(userId: UniqueID) {
|
||||||
|
const userRepository = this._getUserRepository();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await userRepository().getById(userId);
|
||||||
|
if (!user) {
|
||||||
|
return Result.fail(UseCaseError.create(UseCaseError.NOT_FOUND_ERROR, "User not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok<User>(user);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const infrastructureError = error as IInfrastructureError;
|
||||||
|
return Result.fail(
|
||||||
|
UseCaseError.create(UseCaseError.REPOSITORY_ERROR, infrastructureError.message)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getUserRepository() {
|
||||||
|
return this._repositoryManager.getRepository<IUserRepository>("User");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import { UpdateProfileUseCase } from "@/contexts/profile/application";
|
import { UpdateProfileUseCase } from "@/contexts/profile/application";
|
||||||
import { IProfileContext } from "../../../Profile.context";
|
import { IProfileContext } from "../../../Profile.context";
|
||||||
import { registerProfileRepository } from "../../../Profile.repository";
|
import { registerProfileRepository } from "../../../Profile.repository";
|
||||||
|
import { registerUserRepository } from "@/contexts/users/infrastructure/User.repository";
|
||||||
import { UpdateProfileController } from "./UpdateProfile.controller";
|
import { UpdateProfileController } from "./UpdateProfile.controller";
|
||||||
import { UpdateProfilePresenter } from "./presenter";
|
import { UpdateProfilePresenter } from "./presenter";
|
||||||
|
|
||||||
export const createUpdateProfileController = (context: IProfileContext) => {
|
export const createUpdateProfileController = (context: IProfileContext) => {
|
||||||
registerProfileRepository(context);
|
registerProfileRepository(context);
|
||||||
|
registerUserRepository(context);
|
||||||
|
|
||||||
return new UpdateProfileController(
|
return new UpdateProfileController(
|
||||||
{
|
{
|
||||||
|
|||||||
@ -76,6 +76,10 @@ export class User extends AggregateRoot<IUserProps> implements IUser {
|
|||||||
return this.props.language;
|
return this.props.language;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set language(language: Language) {
|
||||||
|
this.props.language = language;
|
||||||
|
}
|
||||||
|
|
||||||
get isUser(): boolean {
|
get isUser(): boolean {
|
||||||
return this.hasRole(UserRole.ROLE_USER);
|
return this.hasRole(UserRole.ROLE_USER);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,7 +21,7 @@ class UserMapper
|
|||||||
email: this.mapsValue(source, "email", Email.create),
|
email: this.mapsValue(source, "email", Email.create),
|
||||||
password: this.mapsValue(source, "password", Password.createFromHashedTextPassword),
|
password: this.mapsValue(source, "password", Password.createFromHashedTextPassword),
|
||||||
roles: source.roles.map((rol) => UserRole.create(rol).object),
|
roles: source.roles.map((rol) => UserRole.create(rol).object),
|
||||||
language: this.mapsValue(source, "language", Language.createFromCode),
|
language: this.mapsValue(source, "lang_code", Language.createFromCode),
|
||||||
};
|
};
|
||||||
|
|
||||||
const id = this.mapsValue(source, "id", UniqueID.create);
|
const id = this.mapsValue(source, "id", UniqueID.create);
|
||||||
|
|||||||
@ -25,4 +25,16 @@ describe("ensureUpdateProfile_Request_DTOIsValid default_tax_label", () => {
|
|||||||
ensureUpdateProfile_Request_DTOIsValid({ ...validProfile, default_tax_label } as any)
|
ensureUpdateProfile_Request_DTOIsValid({ ...validProfile, default_tax_label } as any)
|
||||||
).toHaveProperty("isFailure", true);
|
).toHaveProperty("isFailure", true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([undefined, "es", "en"])('accepts interface language %p', (lang_code) => {
|
||||||
|
expect(
|
||||||
|
ensureUpdateProfile_Request_DTOIsValid({ ...validProfile, lang_code })
|
||||||
|
).toHaveProperty("isSuccess", true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["ca", "fr", "zz"])('rejects unsupported interface language %p', (lang_code) => {
|
||||||
|
expect(
|
||||||
|
ensureUpdateProfile_Request_DTOIsValid({ ...validProfile, lang_code })
|
||||||
|
).toHaveProperty("isFailure", true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
import Joi from "joi";
|
import Joi from "joi";
|
||||||
import { IPercentage_DTO, Result, RuleValidator } from "../../../../common";
|
import { IPercentage_DTO, Result, RuleValidator } from "../../../../common";
|
||||||
|
|
||||||
|
export const SUPPORTED_INTERFACE_LANGUAGE_CODES = ["es", "en"] as const;
|
||||||
|
|
||||||
export interface IUpdateProfile_Request_DTO {
|
export interface IUpdateProfile_Request_DTO {
|
||||||
|
lang_code?: string;
|
||||||
contact_information: string;
|
contact_information: string;
|
||||||
default_payment_method: string;
|
default_payment_method: string;
|
||||||
default_notes: string;
|
default_notes: string;
|
||||||
@ -15,6 +18,7 @@ export interface IUpdateProfile_Request_DTO {
|
|||||||
|
|
||||||
export function ensureUpdateProfile_Request_DTOIsValid(userDTO: IUpdateProfile_Request_DTO) {
|
export function ensureUpdateProfile_Request_DTOIsValid(userDTO: IUpdateProfile_Request_DTO) {
|
||||||
const schema = Joi.object({
|
const schema = Joi.object({
|
||||||
|
lang_code: Joi.string().trim().lowercase().valid(...SUPPORTED_INTERFACE_LANGUAGE_CODES).optional(),
|
||||||
contact_information: Joi.string().optional().allow(null).allow("").default(""),
|
contact_information: Joi.string().optional().allow(null).allow("").default(""),
|
||||||
default_payment_method: Joi.string().optional().allow(null).allow("").default(""),
|
default_payment_method: Joi.string().optional().allow(null).allow("").default(""),
|
||||||
default_notes: Joi.string().optional().allow(null).allow("").default(""),
|
default_notes: Joi.string().optional().allow(null).allow("").default(""),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user