diff --git a/apps/web/src/app.tsx b/apps/web/src/app.tsx
index 51929c33..2417d0a5 100644
--- a/apps/web/src/app.tsx
+++ b/apps/web/src/app.tsx
@@ -59,7 +59,7 @@ export const App = () => {
-
+
{import.meta.env.DEV && }
diff --git a/modules/core/src/common/dto/tin.dto.ts b/modules/core/src/common/dto/tin.dto.ts
index d3fae216..9593ac5f 100644
--- a/modules/core/src/common/dto/tin.dto.ts
+++ b/modules/core/src/common/dto/tin.dto.ts
@@ -16,8 +16,8 @@ import { z } from "zod/v4";
export const TinSchema = z
.string()
.trim()
- .min(1, "TIN cannot be empty.")
- .max(32, "TIN is too long.")
+ .min(2, "TIN cannot be empty.")
+ .max(10, "TIN is too long.")
.regex(/^(?=.*[A-Za-z0-9])[A-Za-z0-9.\-/ ]+$/, "TIN has an invalid format.");
export type TinDTO = z.infer;
diff --git a/modules/core/src/web/lib/data-source/axios/create-axios-data-source.ts b/modules/core/src/web/lib/data-source/axios/create-axios-data-source.ts
index 2b8c4326..be18279a 100644
--- a/modules/core/src/web/lib/data-source/axios/create-axios-data-source.ts
+++ b/modules/core/src/web/lib/data-source/axios/create-axios-data-source.ts
@@ -70,8 +70,15 @@ export const createAxiosDataSource = (client: AxiosInstance): IDataSource => {
data: TData,
params?: Record
): Promise => {
+ const url = `${resource}/${id}`;
+
+ console.log("Axios updateOne => ", {
+ url,
+ data,
+ });
+
const res = await client.put, TData>(
- `${resource}/${id}`,
+ url,
data,
params as AxiosRequestConfig
);
diff --git a/modules/core/src/web/lib/helpers/build-top-level-form-dirty-patch.ts b/modules/core/src/web/lib/helpers/build-top-level-form-dirty-patch.ts
new file mode 100644
index 00000000..83c62ab0
--- /dev/null
+++ b/modules/core/src/web/lib/helpers/build-top-level-form-dirty-patch.ts
@@ -0,0 +1,212 @@
+import type { FieldNamesMarkedBoolean } from "react-hook-form";
+
+/**
+ * Devuelve el valor sin aplicar ninguna normalización.
+ *
+ * Propósito:
+ * - Permitir pasar valores ya alineados con el contrato de salida.
+ * - Evitar transformaciones innecesarias en casos triviales.
+ *
+ * Uso típico:
+ * - Campos que ya tienen exactamente el shape del patch.
+ * - Reemplazos completos de colecciones (`items`).
+ *
+ * Reglas:
+ * - Solo usar cuando `Input === Output` (mismo tipo semántico).
+ * - No usar si hay diferencias de contrato (ej: `""` vs `null`).
+ * - No usar para ocultar transformaciones necesarias.
+ *
+ * Ejemplos:
+ * ```ts
+ * asIs(10) // 10
+ * asIs(true) // true
+ *
+ * // Caso válido: array ya en formato patch
+ * asIs([{ id: "1", description: null }])
+ * ```
+ *
+ * Ejemplo incorrecto:
+ * ```ts
+ * // ❌ description debería ser nullableString
+ * asIs({ description: "" })
+ * ```
+ *
+ * @typeParam T - Tipo de entrada y salida.
+ * @param value - Valor sin normalizar.
+ * @returns Mismo valor sin modificaciones.
+ */
+export const asIs = (value: T): T => value;
+
+/**
+ * Convierte un string en un valor requerido normalizado.
+ *
+ * Reglas:
+ * - Elimina espacios laterales (`trim`)
+ * - Nunca devuelve `null` ni `undefined`
+ * - No valida contenido (eso pertenece a capa superior)
+ *
+ * Ejemplos:
+ * ```ts
+ * toRequiredString(" ABC ") // "ABC"
+ * toRequiredString("") // ""
+ * ```
+ *
+ * @param value - Valor de entrada desde UI/form.
+ * @returns String normalizado.
+ */
+export const toRequiredString = (value: string): string => {
+ return value.trim();
+};
+
+/**
+ * Convierte un string en un valor nullable.
+ *
+ * Reglas:
+ * - Elimina espacios laterales (`trim`)
+ * - `""` o whitespace → `null`
+ * - valor con contenido → string normalizado
+ *
+ * Ejemplos:
+ * ```ts
+ * toNullableString(" ABC ") // "ABC"
+ * toNullableString("") // null
+ * toNullableString(" ") // null
+ * ```
+ *
+ * @param value - Valor de entrada desde UI/form.
+ * @returns String normalizado o `null`.
+ */
+export const toNullableString = (value: string): string | null => {
+ const trimmed = value.trim();
+
+ return trimmed === "" ? null : trimmed;
+};
+
+/**
+ * Normaliza un número requerido.
+ *
+ * Reglas:
+ * - Devuelve el mismo valor
+ * - No valida rango ni precisión
+ * - No convierte `null` ni `undefined`
+ *
+ * Uso típico:
+ * - Campos numéricos obligatorios ya validados por el formulario
+ *
+ * Ejemplos:
+ * ```ts
+ * toNumber(10) // 10
+ * toNumber(0) // 0
+ * ```
+ *
+ * @param value - Número de entrada.
+ * @returns Número sin transformación.
+ */
+export const toNumber = (value: number): number => {
+ return value;
+};
+
+/**
+ * Normaliza un número nullable.
+ *
+ * Reglas:
+ * - `null` → `null`
+ * - número válido → mismo valor
+ * - No convierte `0` a null
+ *
+ * Uso típico:
+ * - Campos opcionales numéricos
+ *
+ * Ejemplos:
+ * ```ts
+ * toNullableNumber(10) // 10
+ * toNullableNumber(null) // null
+ * ```
+ *
+ * @param value - Número o null.
+ * @returns Número o `null`.
+ */
+export const toNullableNumber = (value: number | null): number | null => {
+ return value;
+};
+
+/**
+ * Normaliza un booleano.
+ *
+ * Reglas:
+ * - Devuelve el mismo valor
+ * - No realiza coerción (`"true"` → ❌)
+ *
+ * Uso típico:
+ * - Flags de UI ya tipados correctamente
+ *
+ * Ejemplos:
+ * ```ts
+ * toBoolean(true) // true
+ * toBoolean(false) // false
+ * ```
+ *
+ * @param value - Booleano de entrada.
+ * @returns Booleano sin transformación.
+ */
+export const toBoolean = (value: boolean): boolean => {
+ return value;
+};
+
+// -----
+
+type FieldNormalizer = (value: TInput) => TOutput;
+
+export type DirtyPatchSpec = {
+ [K in keyof TForm & keyof TPatch]?: FieldNormalizer;
+};
+
+/**
+ * Comprueba si un campo top-level fue modificado.
+ *
+ * Se encapsula el cast porque `FieldNamesMarkedBoolean` puede representar
+ * estructuras anidadas, pero este builder solo soporta claves top-level.
+ */
+const isTopLevelDirty = (
+ dirtyFields: FieldNamesMarkedBoolean,
+ key: keyof TForm
+): boolean => {
+ return Boolean((dirtyFields as Partial>)[key]);
+};
+
+/**
+ * Construye un patch top-level a partir de `formData`, `dirtyFields` y una spec de normalizadores.
+ *
+ * Solo procesa claves declaradas en `spec`.
+ * Solo incluye campos marcados como dirty en primer nivel.
+ * No soporta paths anidados ni merge granular de arrays.
+ *
+ * @example
+ * ```ts
+ * const patchSpec = {
+ * name: requiredString,
+ * notes: nullableString,
+ * items: raw,
+ * } satisfies DirtyPatchSpec