Created a new test file for validating the UpdateProfile DTO, ensuring that the default_tax_label property accepts valid inputs and rejects invalid ones.

This commit is contained in:
David Arranz 2026-10-05 10:59:59 +02:00
parent 3212c6ec6d
commit 75de474c16
19 changed files with 149 additions and 34 deletions

View File

@ -1,4 +1,10 @@
import { ErrorOverlay, FormPercentageField, FormTextAreaField, LoadingOverlay } from "@/components";
import {
ErrorOverlay,
FormPercentageField,
FormTextAreaField,
FormTextField,
LoadingOverlay,
} from "@/components";
import {
Alert,
AlertDescription,
@ -49,6 +55,7 @@ export const SettingsEditor = () => {
amount: undefined,
scale: 2,
},
default_tax_label: "% IVA",
}),
[]
);
@ -287,12 +294,21 @@ export const SettingsEditor = () => {
</CardDescription>
</CardHeader>
<CardContent>
<FormTextField
maxLength={32}
placeholder={t("settings.form_fields.default_tax_label.placeholder")}
name='default_tax_label'
required
description={t("settings.form_fields.default_tax_label.desc")}
/>
<br />
<FormPercentageField
scale={2}
disabled={formState.disabled}
placeholder={t("settings.form_fields.default_tax.desc")}
name='default_tax'
required
description={t("settings.form_fields.default_tax.desc")}
/>
</CardContent>
<CardFooter className='px-6 py-4 border-t'>

View File

@ -162,7 +162,6 @@
"sent_to": "Send to Uecko"
}
},
"resume": {
"title": "Quote",
"download_quote": "Download quote",
@ -170,7 +169,6 @@
"resume": "Resume",
"preview": "Preview"
},
"quote_information": "Quote Information",
"customer_information": "Customer Information",
"payment_information": "Payment Information",
@ -455,6 +453,11 @@
"placeholder": "",
"desc": "Default tax rate for your quotes"
},
"default_tax_label": {
"label": "Tax label",
"placeholder": "% VAT",
"desc": "Name or label shown alongside the tax"
},
"default_show_line_discount_column": {
"label": "Show discount column by default",
"desc": "New quotes will initially show the discount column in their lines. You can change this option later for each quote."
@ -482,4 +485,4 @@
}
}
}
}
}

View File

@ -162,7 +162,6 @@
"sent_to": "Enviar a Uecko"
}
},
"resume": {
"title": "Cotización",
"download_quote": "Descargar",
@ -170,7 +169,6 @@
"resume": "Resumen",
"preview": "Previsualización"
},
"quote_information": "Información",
"customer_information": "Cliente",
"payment_information": "Pago",
@ -243,7 +241,6 @@
"description": "Para ayudar a rellenar la cotización, puede añadir bloques de texto predefinidos de esta lista.",
"toast_article_added": "Bloque de texto añadido:"
},
"quote_sent_to_editor": {
"trigger_button": "Enviar a Uecko",
"title": "Enviar la cotización a Uecko",
@ -251,7 +248,6 @@
"submit_button": "Enviar",
"toast_status_changed": "Cotización enviada a Uecko"
},
"quote_status_editor": {
"trigger_button": "Cambiar el estado",
"title": "Cambiar el estado de la cotización",
@ -439,8 +435,8 @@
"subtitle": "",
"tabs": {
"profile": "Ajustes de perfil",
"quotes": "Ajustes legales",
"legal": "Ajustes para cotizaciones"
"quotes": "Ajustes para cotizaciones",
"legal": "Ajustes legales"
}
},
"form_fields": {
@ -459,9 +455,14 @@
"desc": "Esta información de contacto aparecerá en las cotizaciones que entregue a sus clientes."
},
"default_tax": {
"label": "IVA por defecto (%)",
"label": "Impuesto por defecto (%)",
"placeholder": "",
"desc": "Porcentaje de IVA por defecto en las cotizaciones"
"desc": "Porcentaje de impuesto por defecto en las cotizaciones"
},
"default_tax_label": {
"label": "Etiqueta del impuesto",
"placeholder": "% IVA",
"desc": "Nombre o etiqueta que se mostrará junto al impuesto"
},
"default_show_line_discount_column": {
"label": "Mostrar columna descuento por defecto",
@ -497,4 +498,4 @@
"form_fields": {}
}
}
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -8,7 +8,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="https://fonts.upset.dev/css2?family=Poppins&display=swap" rel="stylesheet" />
<title>Presupuestador Uecko</title>
<script type="module" crossorigin src="/assets/index-D_mpiX-a.js"></script>
<script type="module" crossorigin src="/assets/index-DzeF1POa.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CBtV2-Es.css">
</head>

View File

@ -61,6 +61,15 @@ export class UpdateProfileUseCase
profile.defaultShowLineDiscountColumn =
profileDTO.default_show_line_discount_column ?? profile.defaultShowLineDiscountColumn;
if (profileDTO.default_tax_label !== undefined) {
const taxLabelOrError = TextValueObject.create(profileDTO.default_tax_label);
if (taxLabelOrError.isFailure) {
return Result.fail(taxLabelOrError.error);
}
profile.defaultTaxLabel = taxLabelOrError.object;
}
const taxOrError = Percentage.create(profileDTO.default_tax);
if (taxOrError.isFailure) {
return Result.fail(taxOrError.error);

View File

@ -24,6 +24,7 @@ export interface IProfileProps {
defaultLegalTerms: Note;
defaultQuoteValidity: Note;
defaultTax: Percentage;
defaultTaxLabel: Note;
defaultShowLineDiscountColumn: boolean;
logo: ProfileLogotype;
@ -43,6 +44,7 @@ export interface IProfile {
defaultQuoteValidity: Note;
defaultTax: Percentage;
defaultTaxLabel: Note;
defaultShowLineDiscountColumn: boolean;
logo: ProfileLogotype;
@ -118,6 +120,14 @@ export class Profile extends AggregateRoot<IProfileProps> implements IProfile {
this.props.defaultTax = newDefaultTax;
}
get defaultTaxLabel(): Note {
return this.props.defaultTaxLabel;
}
set defaultTaxLabel(newDefaultTaxLabel: Note) {
this.props.defaultTaxLabel = newDefaultTaxLabel;
}
get defaultShowLineDiscountColumn(): boolean {
return this.props.defaultShowLineDiscountColumn;
}

View File

@ -36,6 +36,7 @@ export const GetProfilePresenter: IGetProfilePresenter = {
default_legal_terms: profile.defaultLegalTerms.toString(),
default_quote_validity: profile.defaultQuoteValidity.toString(),
default_tax: profile.defaultTax.convertScale(2).toObject(),
default_tax_label: profile.defaultTaxLabel.toString(),
default_show_line_discount_column: profile.defaultShowLineDiscountColumn,
logo: await profile.logo.toBase64(),

View File

@ -16,6 +16,7 @@ export const UpdateProfilePresenter: IUpdateProfilePresenter = {
default_legal_terms: profile.defaultLegalTerms.toString(),
default_quote_validity: profile.defaultQuoteValidity.toString(),
default_tax: profile.defaultTax.convertScale(2).toObject(),
default_tax_label: profile.defaultTaxLabel.toString(),
default_show_line_discount_column: profile.defaultShowLineDiscountColumn,
};
},

View File

@ -1,7 +1,7 @@
import { IProfileContext } from "../Profile.context";
import { createProfileMapper } from "./profile.mapper";
const source = (defaultShowLineDiscountColumn: boolean | null) =>
const source = (defaultShowLineDiscountColumn: boolean | null, defaultTaxLabel?: string | null) =>
({
default_show_line_discount_column: defaultShowLineDiscountColumn,
dataValues: {
@ -17,6 +17,7 @@ const source = (defaultShowLineDiscountColumn: boolean | null) =>
default_legal_terms: "",
default_quote_validity: "",
default_tax: 2100,
default_tax_label: defaultTaxLabel,
},
}) as any;
@ -33,4 +34,15 @@ describe("ProfileMapper default_show_line_discount_column", () => {
it("uses true for historical null values", () => {
expect(mapper.mapToDomain(source(null)).defaultShowLineDiscountColumn).toBe(true);
});
it("maps a tax label in both persistence directions", () => {
const profile = mapper.mapToDomain(source(true, "% IGIC"));
expect(profile.defaultTaxLabel.toString()).toBe("% IGIC");
expect(mapper.mapToPersistence(profile).default_tax_label).toBe("% IGIC");
});
it("uses the default tax label for historical null values", () => {
expect(mapper.mapToDomain(source(true, null)).defaultTaxLabel.toString()).toBe("% IVA");
});
});

View File

@ -4,7 +4,11 @@ import {
SequelizeMapper,
} from "@/contexts/common/infrastructure";
import { DealerStatus } from "@/contexts/sales/domain";
import { Dealer_Model, DealerCreationAttributes } from "@/contexts/sales/infrastructure/sequelize";
import {
DEFAULT_TAX_LABEL,
Dealer_Model,
DealerCreationAttributes,
} from "@/contexts/sales/infrastructure/sequelize";
import {
CurrencyData,
Language,
@ -52,6 +56,9 @@ class ProfileMapper
"default_quote_validity",
TextValueObject.create
);
const defaultTaxLabel = this.mapsValue(source, "default_tax_label", (label) =>
TextValueObject.create(label ?? DEFAULT_TAX_LABEL)
);
const defaultTax = this.mapsValue(source, "default_tax", (tax) =>
Percentage.create({
@ -72,6 +79,7 @@ class ProfileMapper
defaultLegalTerms,
defaultQuoteValidity,
defaultTax,
defaultTaxLabel,
defaultShowLineDiscountColumn: source.default_show_line_discount_column ?? true,
logo,
@ -97,6 +105,7 @@ class ProfileMapper
default_legal_terms: source.defaultLegalTerms.toPrimitive(),
default_quote_validity: source.defaultQuoteValidity.toPrimitive(),
default_tax: source.defaultTax.convertScale(2).toPrimitive(),
default_tax_label: source.defaultTaxLabel.toPrimitive(),
default_show_line_discount_column: source.defaultShowLineDiscountColumn,
logo: source.logo.toPrimitive(),
};

View File

@ -6,7 +6,7 @@ import {
import { CurrencyData, KeyValueMap, Language, Name, UniqueID } from "@shared/contexts";
import { Dealer, DealerLogotype, DealerStatus, IDealerProps } from "../../domain/entities";
import { ISalesContext } from "../Sales.context";
import { DealerCreationAttributes, Dealer_Model } from "../sequelize";
import { DEFAULT_TAX_LABEL, DealerCreationAttributes, Dealer_Model } from "../sequelize";
export interface IDealerMapper
extends ISequelizeMapper<Dealer_Model, DealerCreationAttributes, Dealer> {}
@ -35,6 +35,7 @@ class DealerMapper
["default_quote_validity", source.default_quote_validity],
["default_show_line_discount_column", source.default_show_line_discount_column ?? true],
["default_tax", source.default_tax],
["default_tax_label", source.default_tax_label ?? DEFAULT_TAX_LABEL],
]);
const props: IDealerProps = {
@ -76,6 +77,8 @@ class DealerMapper
default_show_line_discount_column:
source.additionalInfo.get("default_show_line_discount_column")?.toString() !== "false",
default_tax: Number(source.additionalInfo.get("default_tax") ?? 0),
default_tax_label:
source.additionalInfo.get("default_tax_label")?.toString() ?? DEFAULT_TAX_LABEL,
logo: source.logo.toPrimitive(),
};
}

View File

@ -19,6 +19,8 @@ export type DealerCreationAttributes = InferCreationAttributes<
//quotes?: QuoteCreationAttributes[];
};
export const DEFAULT_TAX_LABEL = "% IVA";
export class Dealer_Model extends Model<
InferAttributes<Dealer_Model, { omit: "user" | "quotes" }>,
InferCreationAttributes<Dealer_Model, { omit: "user" | "quotes" }>
@ -55,6 +57,7 @@ export class Dealer_Model extends Model<
declare default_quote_validity: CreationOptional<string>;
declare default_show_line_discount_column: CreationOptional<boolean>;
declare default_tax: CreationOptional<number | null>;
declare default_tax_label: CreationOptional<string | null>;
declare status: CreationOptional<string>;
declare lang_code: CreationOptional<string>;
declare currency_code: CreationOptional<string>;
@ -99,6 +102,12 @@ export default (sequelize: Sequelize) => {
defaultValue: 2100,
},
default_tax_label: {
type: DataTypes.STRING(32),
allowNull: false,
defaultValue: DEFAULT_TAX_LABEL,
},
logo: {
type: DataTypes.STRING,
allowNull: true,

View File

@ -5,6 +5,10 @@ import { assign } from "lodash";
import { DateTime, Settings } from "luxon";
import { createSequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
import {
DEFAULT_TAX_LABEL,
Dealer_Model,
} from "@/contexts/sales/infrastructure/sequelize/dealer.model";
import { trace } from "console";
import { config } from "../../config";
import { app } from "../express/app";
@ -106,7 +110,12 @@ const server: http.Server = http
try {
//firebirdConn.sync().then(() => {
sequelizeConn.sync({ force: false, alter: true }).then(() => {
sequelizeConn.sync({ force: false, alter: true }).then(async () => {
await Dealer_Model.update(
{ default_tax_label: DEFAULT_TAX_LABEL },
{ where: { default_tax_label: null } }
);
initStructure(sequelizeConn.connection);
insertUsers();

View File

@ -15,6 +15,7 @@ export interface IGetProfileResponse_DTO {
default_legal_terms: string;
default_quote_validity: string;
default_tax: IPercentage_DTO;
default_tax_label: string;
default_show_line_discount_column: boolean;
status: string;
lang_code: string;

View File

@ -0,0 +1,28 @@
import { ensureUpdateProfile_Request_DTOIsValid } from "./IUpdateProfile_Request.dto";
const validProfile = {
contact_information: "",
default_payment_method: "",
default_notes: "",
default_legal_terms: "",
default_quote_validity: "",
default_tax: { amount: 2100, scale: 2 },
logo: "",
};
describe("ensureUpdateProfile_Request_DTOIsValid default_tax_label", () => {
it.each([undefined, "% IVA", "% IGIC", " % VAT "])(
"accepts %p",
(default_tax_label) => {
expect(
ensureUpdateProfile_Request_DTOIsValid({ ...validProfile, default_tax_label })
).toHaveProperty("isSuccess", true);
}
);
it.each(["", " ", "a".repeat(33), 21])("rejects %p", (default_tax_label) => {
expect(
ensureUpdateProfile_Request_DTOIsValid({ ...validProfile, default_tax_label } as any)
).toHaveProperty("isFailure", true);
});
});

View File

@ -8,6 +8,7 @@ export interface IUpdateProfile_Request_DTO {
default_legal_terms: string;
default_quote_validity: string;
default_tax: IPercentage_DTO;
default_tax_label?: string;
default_show_line_discount_column?: boolean;
logo: string;
}
@ -23,6 +24,7 @@ export function ensureUpdateProfile_Request_DTOIsValid(userDTO: IUpdateProfile_R
amount: Joi.number().allow(null),
scale: Joi.number(),
}).optional(),
default_tax_label: Joi.string().trim().min(1).max(32).optional(),
default_show_line_discount_column: Joi.boolean(),
logo: Joi.string().optional().allow(null).allow("").default(""),
}).unknown(true);

View File

@ -8,5 +8,6 @@ export interface IUpdateProfileResponse_DTO {
default_legal_terms: string;
default_quote_validity: string;
default_tax: IPercentage_DTO;
default_tax_label: string;
default_show_line_discount_column: boolean;
}