Licencias de software
This commit is contained in:
parent
1989bbff89
commit
5144073481
@ -43,14 +43,104 @@ export async function verifyResources(
|
|||||||
return ResourceResponseSchema.parse(await response.json()).resource;
|
return ResourceResponseSchema.parse(await response.json()).resource;
|
||||||
}
|
}
|
||||||
const initial = await read(global);
|
const initial = await read(global);
|
||||||
assert.equal(initial.items.length, 18);
|
assert.equal(initial.items.length, 26);
|
||||||
assert.equal(initial.items.filter((item) => item.values.kind === "template").length, 5);
|
assert.equal(initial.items.filter((item) => item.values.kind === "template").length, 6);
|
||||||
assert.ok(initial.items.every((item) => item.builtin && item.id[14] === "7"));
|
assert.ok(initial.items.every((item) => item.builtin && item.id[14] === "7"));
|
||||||
assert.ok(
|
assert.ok(
|
||||||
initial.items.every((item) =>
|
initial.items.every((item) =>
|
||||||
item.bindings.every((binding) => !binding.required && binding.id[14] === "7")
|
item.bindings.every((binding) => !binding.required && binding.id[14] === "7")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
const licenseTemplate = initial.items.find(
|
||||||
|
(item) => item.builtinKey === "template:software_license"
|
||||||
|
);
|
||||||
|
assert.ok(licenseTemplate && licenseTemplate.values.kind === "template");
|
||||||
|
const licenseFieldId = (key: string) => {
|
||||||
|
const field = initial.items.find((item) => item.builtinKey === `field:${key}`);
|
||||||
|
assert.ok(field && field.values.kind === "field");
|
||||||
|
return field.id;
|
||||||
|
};
|
||||||
|
const licenseInput = {
|
||||||
|
name: "Microsoft 365 Business Standard",
|
||||||
|
templateId: licenseTemplate.id,
|
||||||
|
active: true,
|
||||||
|
notes: "",
|
||||||
|
characteristics: {
|
||||||
|
[licenseFieldId("software_product")]: "Microsoft 365",
|
||||||
|
[licenseFieldId("software_edition")]: "Business Standard",
|
||||||
|
[licenseFieldId("license_model")]: "Suscripción",
|
||||||
|
[licenseFieldId("license_metric")]: "Por usuario",
|
||||||
|
[licenseFieldId("license_quantity")]: 25,
|
||||||
|
[licenseFieldId("expires")]: "2027-10-07",
|
||||||
|
[licenseFieldId("renews")]: "2027-09-30",
|
||||||
|
[licenseFieldId("auto_renew")]: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const license = await resource(inventory, licenseInput);
|
||||||
|
assert.equal(license.companyId, companyId);
|
||||||
|
assert.equal(license.specification.templateName, "Licencia de software");
|
||||||
|
assert.equal(license.values.characteristics[licenseFieldId("auto_renew")], false);
|
||||||
|
assert.equal(license.id[14], "7");
|
||||||
|
const licenseUpdated = await resource(`${inventory}/${license.id}/update`, {
|
||||||
|
...licenseInput,
|
||||||
|
characteristics: { ...licenseInput.characteristics, [licenseFieldId("license_quantity")]: 30 },
|
||||||
|
});
|
||||||
|
assert.equal(licenseUpdated.history.length, 2);
|
||||||
|
assert.equal(
|
||||||
|
licenseUpdated.history[0]?.values.characteristics[licenseFieldId("license_quantity")],
|
||||||
|
25
|
||||||
|
);
|
||||||
|
await error(
|
||||||
|
inventory,
|
||||||
|
{
|
||||||
|
values: {
|
||||||
|
...licenseInput,
|
||||||
|
name: "Fecha inválida",
|
||||||
|
characteristics: { [licenseFieldId("renews")]: "2027-02-30" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"INVALID_CHARACTERISTICS"
|
||||||
|
);
|
||||||
|
await error(
|
||||||
|
inventory,
|
||||||
|
{
|
||||||
|
values: {
|
||||||
|
...licenseInput,
|
||||||
|
name: "Modalidad inválida",
|
||||||
|
characteristics: { [licenseFieldId("license_model")]: "Desconocida" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"INVALID_CHARACTERISTICS"
|
||||||
|
);
|
||||||
|
const minimal = await resource(inventory, {
|
||||||
|
...licenseInput,
|
||||||
|
name: "ESET empresa",
|
||||||
|
characteristics: {},
|
||||||
|
});
|
||||||
|
assert.deepEqual(minimal.values.characteristics, {});
|
||||||
|
await error(
|
||||||
|
`/resources/${otherId}/${license.id}/update`,
|
||||||
|
{ values: licenseInput },
|
||||||
|
"RESOURCE_NOT_FOUND",
|
||||||
|
404
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
(await request(`${company}/${licenseTemplate.id}/status`, { active: false })).status,
|
||||||
|
200
|
||||||
|
);
|
||||||
|
await error(
|
||||||
|
inventory,
|
||||||
|
{ values: { ...licenseInput, name: "No disponible" } },
|
||||||
|
"TEMPLATE_UNAVAILABLE"
|
||||||
|
);
|
||||||
|
await resource(`${inventory}/${license.id}/update`, {
|
||||||
|
...licenseUpdated.values,
|
||||||
|
notes: "Conservada",
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
(await request(`${company}/${licenseTemplate.id}/status`, { active: null })).status,
|
||||||
|
200
|
||||||
|
);
|
||||||
const assetsResponse = await request("/asset-catalog/global");
|
const assetsResponse = await request("/asset-catalog/global");
|
||||||
const assets = AssetCatalogResponseSchema.parse(await assetsResponse.json());
|
const assets = AssetCatalogResponseSchema.parse(await assetsResponse.json());
|
||||||
assert.ok(initial.items.every((item) => !assets.items.some((asset) => asset.id === item.id)));
|
assert.ok(initial.items.every((item) => !assets.items.some((asset) => asset.id === item.id)));
|
||||||
|
|||||||
@ -49,7 +49,7 @@ Buzones independientes corporativos o externos, cuenta informática opcional, pe
|
|||||||
|
|
||||||
## Recursos con campos y plantillas: base implementada
|
## Recursos con campos y plantillas: base implementada
|
||||||
|
|
||||||
Catálogo independiente de activos, 13 campos y cinco plantillas de serie, adiciones globales y por compañía, disponibilidad local y recursos con especificación capturada e historial. Los buzones y recursos básicos anteriores están migrados, conservando UUID, historial y relaciones. La gestión anterior se ha retirado; sus tablas se mantienen como proyecciones para permisos y configuraciones existentes. Consultar [Recursos, campos y plantillas](recursos-campos-y-plantillas.md).
|
Catálogo independiente de activos, 20 campos y seis plantillas de serie, adiciones globales y por compañía, disponibilidad local y recursos con especificación capturada e historial. Los buzones y recursos básicos anteriores están migrados, conservando UUID, historial y relaciones. La gestión anterior se ha retirado; sus tablas se mantienen como proyecciones para permisos y configuraciones existentes. Consultar [Recursos, campos y plantillas](recursos-campos-y-plantillas.md).
|
||||||
|
|
||||||
## Impresoras por perfil: implementado
|
## Impresoras por perfil: implementado
|
||||||
|
|
||||||
@ -87,7 +87,7 @@ Varios métodos por activo, catálogo global ampliable con herramientas de serie
|
|||||||
|
|
||||||
1. Vinculación explícita entre un acceso remoto y su VPN previa, si resulta necesaria.
|
1. Vinculación explícita entre un acceso remoto y su VPN previa, si resulta necesaria.
|
||||||
|
|
||||||
Este orden se concreta antes de cada entrega. Las licencias y su relación con cuentas se explorarán posteriormente.
|
Este orden se concreta antes de cada entrega. El registro de licencias como recursos está implementado; sus asignaciones y relaciones con cuentas, personas, equipos y aplicaciones quedan pendientes.
|
||||||
|
|
||||||
## Inventario: implementado
|
## Inventario: implementado
|
||||||
|
|
||||||
@ -103,9 +103,13 @@ Tickets por compañía y consulta global con búsqueda/filtros, referencia corta
|
|||||||
|
|
||||||
Consulta global y por compañía, técnico, tiempos, diagnóstico, trabajo realizado, resultado y elementos afectados. Varias intervenciones por ticket o actuaciones internas sin ticket. Pasos opcionales, procedimientos versionados, comprobaciones obligatorias al finalizar, historial, idempotencia y control de concurrencia. Consultar [Intervenciones y plantillas](intervenciones-y-plantillas.md).
|
Consulta global y por compañía, técnico, tiempos, diagnóstico, trabajo realizado, resultado y elementos afectados. Varias intervenciones por ticket o actuaciones internas sin ticket. Pasos opcionales, procedimientos versionados, comprobaciones obligatorias al finalizar, historial, idempotencia y control de concurrencia. Consultar [Intervenciones y plantillas](intervenciones-y-plantillas.md).
|
||||||
|
|
||||||
|
## Licencias de software: registro implementado
|
||||||
|
|
||||||
|
Plantilla global protegida, siete campos nuevos y cuatro reutilizados del catálogo de recursos. Registro por compañía, características opcionales, modalidades, criterio de licenciamiento, cantidad contratada, caducidad, renovación e historial. Modales con ayuda específica. Pendientes las asignaciones, capacidad disponible, vínculos con aplicaciones y avisos. Consultar [Recursos, campos y plantillas](recursos-campos-y-plantillas.md).
|
||||||
|
|
||||||
## Servicios y documentación: pendiente
|
## Servicios y documentación: pendiente
|
||||||
|
|
||||||
Software, licencias, Microsoft 365, ERP, antivirus, cloud, dominios, DNS, certificados, contratos y documentación.
|
Gestión de asignaciones de licencias, Microsoft 365, ERP, antivirus, cloud, dominios, DNS, certificados, contratos y documentación.
|
||||||
Las credenciales se resolverán con una solución específica; no almacenarlas como texto libre.
|
Las credenciales se resolverán con una solución específica; no almacenarlas como texto libre.
|
||||||
|
|
||||||
## Decisiones pendientes
|
## Decisiones pendientes
|
||||||
|
|||||||
@ -20,7 +20,7 @@ Cada compañía puede activar o desactivar una definición global para su ámbit
|
|||||||
|
|
||||||
## Datos de serie
|
## Datos de serie
|
||||||
|
|
||||||
Se incluyen 13 campos y cinco plantillas:
|
Se incluyen 20 campos y seis plantillas:
|
||||||
|
|
||||||
| Plantilla | Campos |
|
| Plantilla | Campos |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@ -28,6 +28,7 @@ Se incluyen 13 campos y cinco plantillas:
|
|||||||
| Carpeta compartida | Ruta UNC, servidor como referencia documental y descripción |
|
| Carpeta compartida | Ruta UNC, servidor como referencia documental y descripción |
|
||||||
| Aplicación | Proveedor, URL y descripción |
|
| Aplicación | Proveedor, URL y descripción |
|
||||||
| Certificado digital | Titular, emisor, caducidad, huella digital y finalidad |
|
| Certificado digital | Titular, emisor, caducidad, huella digital y finalidad |
|
||||||
|
| Licencia de software | Producto, edición o plan, proveedor, modalidad, criterio de licenciamiento, cantidad contratada, referencia, caducidad, fecha de renovación, renovación automática y descripción |
|
||||||
| Recurso de acceso | Referencia; contexto estructurado de tipo de acceso, sistema y entorno |
|
| Recurso de acceso | Referencia; contexto estructurado de tipo de acceso, sistema y entorno |
|
||||||
|
|
||||||
Las características de serie son opcionales salvo la dirección válida exigida al registrar un buzón. Los datos se alimentan con claves de serie estables: reiniciar no duplica definiciones, cambia UUID ni reactiva elementos desactivados. No se crean recursos, personas o compañías de ejemplo.
|
Las características de serie son opcionales salvo la dirección válida exigida al registrar un buzón. Los datos se alimentan con claves de serie estables: reiniciar no duplica definiciones, cambia UUID ni reactiva elementos desactivados. No se crean recursos, personas o compañías de ejemplo.
|
||||||
@ -77,3 +78,13 @@ POP/IMAP pertenece a la configuración de correo por perfil; letras de unidad pe
|
|||||||
Las pruebas con MariaDB de test comprueban UUID y revisiones originales, repetición idempotente, edición canónica y actualización de proyecciones, aislamiento por compañía y rechazo de escrituras antiguas. Se mantienen las pruebas de múltiples correos, permisos, necesidades, cobertura y perfiles. Las pruebas de dominio cubren correo inválido o ausente, normalización, cuenta opcional, cambio auditado y tipo fijo.
|
Las pruebas con MariaDB de test comprueban UUID y revisiones originales, repetición idempotente, edición canónica y actualización de proyecciones, aislamiento por compañía y rechazo de escrituras antiguas. Se mantienen las pruebas de múltiples correos, permisos, necesidades, cobertura y perfiles. Las pruebas de dominio cubren correo inválido o ausente, normalización, cuenta opcional, cambio auditado y tipo fijo.
|
||||||
|
|
||||||
La comprobación en navegador usa los datos existentes de desarrollo, sin crear registros ficticios: ficha, historial, personas vinculadas, edición modal y selectores.
|
La comprobación en navegador usa los datos existentes de desarrollo, sin crear registros ficticios: ficha, historial, personas vinculadas, edición modal y selectores.
|
||||||
|
|
||||||
|
## Licencias de software: registro implementado
|
||||||
|
|
||||||
|
Una licencia es un Resource de la compañía creado con la plantilla de serie Licencia de software. Se registra en Recursos → Nuevo recurso, seleccionando esa plantilla; no constituye un activo ni una instalación de aplicación. Ejemplos: Microsoft 365 Business Standard, ESET Endpoint contratado para la empresa o licencia perpetua de ERP.
|
||||||
|
|
||||||
|
Nombre, plantilla y estado siguen el contrato común. Todas las características de esta plantilla son opcionales. Modalidad ofrece Suscripción, Perpetua, OEM y Otra; criterio ofrece Por usuario, Por dispositivo, Por servidor, Concurrente, Corporativa y Otro. Cantidad contratada es una característica numérica documental; no representa plazas libres ni impone aún reglas de asignación. Caducidad y fecha de renovación se registran independientemente. Renovación automática distingue sí, no y sin datos. No almacenar claves de activación.
|
||||||
|
|
||||||
|
Se reutilizan proveedor, referencia, caducidad y descripción del catálogo de recursos. Los siete campos nuevos y la plantilla se incorporan de forma idempotente al arrancar, sin cambiar identidades, fichas existentes ni disponibilidad configurada. La plantilla de serie está protegida y se puede copiar para personalizar; se puede desactivar globalmente o para una compañía. La ayuda específica aparece al final del modal, antes de Guardar y Cancelar.
|
||||||
|
|
||||||
|
La ficha captura su especificación y conserva revisiones como cualquier recurso. Registrar una licencia no crea asignaciones, vínculos con aplicaciones/cuentas/equipos, avisos de vencimiento ni cálculos de capacidad. Estas relaciones y avisos quedan pendientes de una entrega posterior.
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import type { ResourceCatalog } from "./resource-catalog.aggregate";
|
|||||||
import type { ResourceDefinitionSpec, ResourceFieldType } from "./resource-definition";
|
import type { ResourceDefinitionSpec, ResourceFieldType } from "./resource-definition";
|
||||||
import { ResourceDefinitionValues } from "./value-objects/resource-definition-values";
|
import { ResourceDefinitionValues } from "./value-objects/resource-definition-values";
|
||||||
|
|
||||||
const FIELDS: readonly [string, string, ResourceFieldType][] = [
|
const FIELDS: readonly [string, string, ResourceFieldType, (readonly string[])?][] = [
|
||||||
["email", "Dirección de correo", "text"],
|
["email", "Dirección de correo", "text"],
|
||||||
["provider", "Proveedor", "text"],
|
["provider", "Proveedor", "text"],
|
||||||
["context", "Contexto", "text"],
|
["context", "Contexto", "text"],
|
||||||
@ -16,6 +16,18 @@ const FIELDS: readonly [string, string, ResourceFieldType][] = [
|
|||||||
["fingerprint", "Huella digital", "text"],
|
["fingerprint", "Huella digital", "text"],
|
||||||
["purpose", "Finalidad", "text"],
|
["purpose", "Finalidad", "text"],
|
||||||
["reference", "Referencia", "text"],
|
["reference", "Referencia", "text"],
|
||||||
|
["software_product", "Producto de software", "text"],
|
||||||
|
["software_edition", "Edición o plan", "text"],
|
||||||
|
["license_model", "Modalidad de licencia", "choice", ["Suscripción", "Perpetua", "OEM", "Otra"]],
|
||||||
|
[
|
||||||
|
"license_metric",
|
||||||
|
"Criterio de licenciamiento",
|
||||||
|
"choice",
|
||||||
|
["Por usuario", "Por dispositivo", "Por servidor", "Concurrente", "Corporativa", "Otro"],
|
||||||
|
],
|
||||||
|
["license_quantity", "Cantidad contratada", "number"],
|
||||||
|
["renews", "Fecha de renovación", "date"],
|
||||||
|
["auto_renew", "Renovación automática", "boolean"],
|
||||||
];
|
];
|
||||||
const TEMPLATES: readonly [string, string, readonly string[]][] = [
|
const TEMPLATES: readonly [string, string, readonly string[]][] = [
|
||||||
["mailbox", "Buzón de correo", ["email", "provider", "context"]],
|
["mailbox", "Buzón de correo", ["email", "provider", "context"]],
|
||||||
@ -23,6 +35,23 @@ const TEMPLATES: readonly [string, string, readonly string[]][] = [
|
|||||||
["shared_folder", "Carpeta compartida", ["unc", "server", "description"]],
|
["shared_folder", "Carpeta compartida", ["unc", "server", "description"]],
|
||||||
["application", "Aplicación", ["provider", "url", "description"]],
|
["application", "Aplicación", ["provider", "url", "description"]],
|
||||||
["certificate", "Certificado digital", ["holder", "issuer", "expires", "fingerprint", "purpose"]],
|
["certificate", "Certificado digital", ["holder", "issuer", "expires", "fingerprint", "purpose"]],
|
||||||
|
[
|
||||||
|
"software_license",
|
||||||
|
"Licencia de software",
|
||||||
|
[
|
||||||
|
"software_product",
|
||||||
|
"software_edition",
|
||||||
|
"provider",
|
||||||
|
"license_model",
|
||||||
|
"license_metric",
|
||||||
|
"license_quantity",
|
||||||
|
"reference",
|
||||||
|
"expires",
|
||||||
|
"renews",
|
||||||
|
"auto_renew",
|
||||||
|
"description",
|
||||||
|
],
|
||||||
|
],
|
||||||
];
|
];
|
||||||
/** Solo alimenta definiciones; no crea recursos ni migra registros anteriores. */
|
/** Solo alimenta definiciones; no crea recursos ni migra registros anteriores. */
|
||||||
export function seedResourceCatalog(catalog: ResourceCatalog) {
|
export function seedResourceCatalog(catalog: ResourceCatalog) {
|
||||||
@ -38,8 +67,15 @@ export function seedResourceCatalog(catalog: ResourceCatalog) {
|
|||||||
}
|
}
|
||||||
current = result.data;
|
current = result.data;
|
||||||
};
|
};
|
||||||
for (const [key, name, type] of FIELDS) {
|
for (const [key, name, type, choices] of FIELDS) {
|
||||||
add(`field:${key}`, { kind: "field", name, description: "", type, unit: "", choices: [] });
|
add(`field:${key}`, {
|
||||||
|
kind: "field",
|
||||||
|
name,
|
||||||
|
description: "",
|
||||||
|
type,
|
||||||
|
unit: "",
|
||||||
|
choices: [...(choices ?? [])],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
for (const [key, name, fields] of TEMPLATES) {
|
for (const [key, name, fields] of TEMPLATES) {
|
||||||
const references = fields.map((field) => {
|
const references = fields.map((field) => {
|
||||||
|
|||||||
@ -24,7 +24,7 @@ const field = {
|
|||||||
} as const;
|
} as const;
|
||||||
test("catálogo de recursos: serie idempotente, UUID v7, características opcionales y protección", () => {
|
test("catálogo de recursos: serie idempotente, UUID v7, características opcionales y protección", () => {
|
||||||
const catalog = seedResourceCatalog(ResourceCatalog.create());
|
const catalog = seedResourceCatalog(ResourceCatalog.create());
|
||||||
assert.equal(catalog.definitions.length, 18);
|
assert.equal(catalog.definitions.length, 26);
|
||||||
assert.equal(seedResourceCatalog(catalog), catalog);
|
assert.equal(seedResourceCatalog(catalog), catalog);
|
||||||
assert.ok(catalog.definitions.every((item) => item.id.toPrimitive()[14] === "7"));
|
assert.ok(catalog.definitions.every((item) => item.id.toPrimitive()[14] === "7"));
|
||||||
const template = catalog.definitions.find((item) => item.values.data.kind === "template");
|
const template = catalog.definitions.find((item) => item.values.data.kind === "template");
|
||||||
@ -110,3 +110,62 @@ test("ámbitos, tipos, listas, campos externos y disponibilidad por compañía",
|
|||||||
assert.ok(reset.isSuccess);
|
assert.ok(reset.isSuccess);
|
||||||
assert.equal(reset.data.active(stock, Maybe.some(company)), true);
|
assert.equal(reset.data.active(stock, Maybe.some(company)), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("licencias: ampliar un catálogo previo conserva identidades y disponibilidad", () => {
|
||||||
|
const seeded = seedResourceCatalog(ResourceCatalog.create());
|
||||||
|
const originalKeys = new Set([
|
||||||
|
"field:email",
|
||||||
|
"field:provider",
|
||||||
|
"field:context",
|
||||||
|
"field:unc",
|
||||||
|
"field:server",
|
||||||
|
"field:description",
|
||||||
|
"field:url",
|
||||||
|
"field:holder",
|
||||||
|
"field:issuer",
|
||||||
|
"field:expires",
|
||||||
|
"field:fingerprint",
|
||||||
|
"field:purpose",
|
||||||
|
"field:reference",
|
||||||
|
"template:mailbox",
|
||||||
|
"template:access",
|
||||||
|
"template:shared_folder",
|
||||||
|
"template:application",
|
||||||
|
"template:certificate",
|
||||||
|
]);
|
||||||
|
const previous = ResourceCatalog.rehydrate(
|
||||||
|
{
|
||||||
|
definitions: seeded.definitions.filter((item) =>
|
||||||
|
originalKeys.has(item.stockKey.getOrUndefined() ?? "")
|
||||||
|
),
|
||||||
|
availability: [],
|
||||||
|
},
|
||||||
|
seeded.id
|
||||||
|
);
|
||||||
|
const original = previous.definitions[0];
|
||||||
|
assert.ok(original);
|
||||||
|
const disabled = previous.setActive(Maybe.none(), original.id, false);
|
||||||
|
assert.ok(disabled.isSuccess);
|
||||||
|
const upgraded = seedResourceCatalog(disabled.data);
|
||||||
|
assert.equal(upgraded.definitions.length, 26);
|
||||||
|
for (const existing of disabled.data.definitions) {
|
||||||
|
assert.equal(
|
||||||
|
upgraded.definitions.find((item) => item.id.equals(existing.id)),
|
||||||
|
existing
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const license = upgraded.definitions.find(
|
||||||
|
(item) => item.stockKey.getOrUndefined() === "template:software_license"
|
||||||
|
);
|
||||||
|
assert.ok(license && license.values.data.kind === "template");
|
||||||
|
assert.ok(license.values.data.fields.every((binding) => !binding.required));
|
||||||
|
const protectedTemplate = license.update(values({ ...license.values.data, name: "Otra" }));
|
||||||
|
assert.ok(!protectedTemplate.isSuccess);
|
||||||
|
assert.equal(protectedTemplate.error.code, "BUILTIN_PROTECTED");
|
||||||
|
const company = UniqueID.generateNewID();
|
||||||
|
const locallyDisabled = upgraded.setActive(Maybe.some(company), license.id, false);
|
||||||
|
assert.ok(locallyDisabled.isSuccess);
|
||||||
|
assert.equal(locallyDisabled.data.active(license, Maybe.some(company)), false);
|
||||||
|
assert.equal(locallyDisabled.data.active(license, Maybe.none()), true);
|
||||||
|
assert.equal(seedResourceCatalog(locallyDisabled.data), locallyDisabled.data);
|
||||||
|
});
|
||||||
|
|||||||
@ -23,13 +23,21 @@ export const inventorySpanish = {
|
|||||||
snapshotNotice:
|
snapshotNotice:
|
||||||
"La ficha conserva los campos de la plantilla utilizados al dar de alta el recurso.",
|
"La ficha conserva los campos de la plantilla utilizados al dar de alta el recurso.",
|
||||||
resourceDescription:
|
resourceDescription:
|
||||||
"Registra un buzón, carpeta compartida, aplicación o certificado disponible en el contexto de esta compañía.",
|
"Registra un buzón, carpeta compartida, aplicación, certificado o licencia de software disponible en el contexto de esta compañía.",
|
||||||
resourceExamples:
|
resourceExamples:
|
||||||
"Ejemplos: administracion@empresa.es, carpeta Administración, Zoom corporativo o certificado de firma.",
|
"Ejemplos: administracion@empresa.es, carpeta Administración, Zoom corporativo, certificado de firma o suscripción Microsoft 365.",
|
||||||
resourceGuidance:
|
resourceGuidance:
|
||||||
"Las características son opcionales salvo que la plantilla indique lo contrario. No registres contraseñas ni claves privadas.",
|
"Las características son opcionales salvo que la plantilla indique lo contrario. No registres contraseñas ni claves privadas.",
|
||||||
resourceScope:
|
resourceScope:
|
||||||
"Este registro identifica el recurso de la compañía. Su disponibilidad y su configuración en los equipos se gestionan por separado.",
|
"Este registro identifica el recurso de la compañía. Su disponibilidad y su configuración en los equipos se gestionan por separado.",
|
||||||
|
licenseDescription:
|
||||||
|
"Registra una licencia o suscripción de software contratada por esta compañía.",
|
||||||
|
licenseExamples:
|
||||||
|
"Ejemplos: Microsoft 365 Business Standard por usuario, ESET Endpoint por dispositivo o una licencia perpetua de ERP.",
|
||||||
|
licenseGuidance:
|
||||||
|
"Todos los campos son opcionales. Cantidad contratada indica las plazas o unidades del contrato. Caducidad y renovación pueden ser distintas; una licencia perpetua puede no tener caducidad. No registres claves de activación.",
|
||||||
|
licenseScope:
|
||||||
|
"La aplicación utilizada y la licencia contratada son recursos distintos. Este registro no asigna plazas ni calcula cuántas quedan disponibles.",
|
||||||
relatedNotice: "La plantilla se guardará aunque después canceles el recurso.",
|
relatedNotice: "La plantilla se guardará aunque después canceles el recurso.",
|
||||||
inventoryErrors: {
|
inventoryErrors: {
|
||||||
INVALID_REQUEST: "Revisa los datos del formulario.",
|
INVALID_REQUEST: "Revisa los datos del formulario.",
|
||||||
@ -73,13 +81,20 @@ export const inventoryEnglish = {
|
|||||||
recordedBy: "Recorded by {{name}}",
|
recordedBy: "Recorded by {{name}}",
|
||||||
snapshotNotice: "This record retains the template fields captured when the resource was created.",
|
snapshotNotice: "This record retains the template fields captured when the resource was created.",
|
||||||
resourceDescription:
|
resourceDescription:
|
||||||
"Register a mailbox, shared folder, application or certificate used in this company's context.",
|
"Register a mailbox, shared folder, application, certificate or software license used in this company's context.",
|
||||||
resourceExamples:
|
resourceExamples:
|
||||||
"Examples: admin@company.com, Administration folder, corporate Zoom or signing certificate.",
|
"Examples: admin@company.com, Administration folder, corporate Zoom, signing certificate or Microsoft 365 subscription.",
|
||||||
resourceGuidance:
|
resourceGuidance:
|
||||||
"Characteristics are optional unless the template requires them. Never store passwords or private keys.",
|
"Characteristics are optional unless the template requires them. Never store passwords or private keys.",
|
||||||
resourceScope:
|
resourceScope:
|
||||||
"This record identifies the company resource. Availability and device configuration are managed separately.",
|
"This record identifies the company resource. Availability and device configuration are managed separately.",
|
||||||
|
licenseDescription: "Register a software license or subscription contracted by this company.",
|
||||||
|
licenseExamples:
|
||||||
|
"Examples: Microsoft 365 Business Standard per user, ESET Endpoint per device or a perpetual ERP license.",
|
||||||
|
licenseGuidance:
|
||||||
|
"All fields are optional. Contracted quantity records seats or units covered by the contract. Expiration and renewal dates may differ; perpetual licenses may have no expiration. Do not enter activation keys.",
|
||||||
|
licenseScope:
|
||||||
|
"The application used and its contracted license are separate resources. This record does not allocate seats or calculate remaining capacity.",
|
||||||
relatedNotice: "The template remains saved if you later cancel the resource.",
|
relatedNotice: "The template remains saved if you later cancel the resource.",
|
||||||
inventoryErrors: {
|
inventoryErrors: {
|
||||||
INVALID_REQUEST: "Check the form data.",
|
INVALID_REQUEST: "Check the form data.",
|
||||||
|
|||||||
@ -84,6 +84,10 @@ export function ResourceForm({
|
|||||||
);
|
);
|
||||||
const template = templates.find((item) => item.id === values.templateId);
|
const template = templates.find((item) => item.id === values.templateId);
|
||||||
const fields = resourceFields(resource, template, items);
|
const fields = resourceFields(resource, template, items);
|
||||||
|
const isSoftwareLicense = items.some(
|
||||||
|
(item) => item.id === values.templateId && item.builtinKey === "template:software_license"
|
||||||
|
);
|
||||||
|
const helpPrefix = isSoftwareLicense ? "license" : "resource";
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: (input: ResourceInput) =>
|
mutationFn: (input: ResourceInput) =>
|
||||||
inventoryApi.save(companyId, input, csrfToken, resource?.id),
|
inventoryApi.save(companyId, input, csrfToken, resource?.id),
|
||||||
@ -234,11 +238,11 @@ export function ResourceForm({
|
|||||||
)}
|
)}
|
||||||
<InventoryFeedback error={mutation.error ?? referenceQuery.error} />
|
<InventoryFeedback error={mutation.error ?? referenceQuery.error} />
|
||||||
<ContextHelp
|
<ContextHelp
|
||||||
description={t("resourceDescription")}
|
description={t(`${helpPrefix}Description`)}
|
||||||
examples={t("resourceExamples")}
|
examples={t(`${helpPrefix}Examples`)}
|
||||||
examplesLabel={t("examplesLabel")}
|
examplesLabel={t("examplesLabel")}
|
||||||
guidance={t("resourceGuidance")}
|
guidance={t(`${helpPrefix}Guidance`)}
|
||||||
scopeNote={t("resourceScope")}
|
scopeNote={t(`${helpPrefix}Scope`)}
|
||||||
title={t("helpTitle")}
|
title={t("helpTitle")}
|
||||||
/>
|
/>
|
||||||
<FormActions>
|
<FormActions>
|
||||||
|
|||||||
@ -75,3 +75,57 @@ test("recurso: ayuda inferior, plantilla con alta rápida y sin asignaciones de
|
|||||||
);
|
);
|
||||||
assert.equal(child.includes('aria-label="Nueva plantilla"'), false);
|
assert.equal(child.includes('aria-label="Nueva plantilla"'), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("licencia: formulario reutilizable con campos opcionales y ayuda antes de guardar", async () => {
|
||||||
|
const catalog = buildCatalogResponse(seedResourceCatalog(ResourceCatalog.create()), Maybe.none());
|
||||||
|
const template = catalog.items.find((item) => item.builtinKey === "template:software_license");
|
||||||
|
assert.ok(template && template.values.kind === "template");
|
||||||
|
const fields = template.values.fields.map((binding) => {
|
||||||
|
const field = catalog.items.find((item) => item.id === binding.fieldId);
|
||||||
|
assert.ok(field && field.values.kind === "field");
|
||||||
|
return {
|
||||||
|
fieldId: field.id,
|
||||||
|
name: field.values.name,
|
||||||
|
type: field.values.type,
|
||||||
|
unit: field.values.unit,
|
||||||
|
choices: field.values.choices,
|
||||||
|
required: binding.required,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const resource = {
|
||||||
|
id: UniqueID.generateNewID().toPrimitive(),
|
||||||
|
companyId,
|
||||||
|
specification: { templateId: template.id, templateName: template.values.name, fields },
|
||||||
|
values: {
|
||||||
|
name: "Microsoft 365",
|
||||||
|
templateId: template.id,
|
||||||
|
active: true,
|
||||||
|
notes: "",
|
||||||
|
characteristics: {},
|
||||||
|
},
|
||||||
|
history: [],
|
||||||
|
};
|
||||||
|
for (const [language, help] of [
|
||||||
|
["es", "Registra una licencia o suscripción"],
|
||||||
|
["en", "Register a software license or subscription"],
|
||||||
|
]) {
|
||||||
|
assert.ok(language && help);
|
||||||
|
const html = await render(
|
||||||
|
language,
|
||||||
|
<ResourceForm
|
||||||
|
companyId={companyId}
|
||||||
|
csrfToken="test"
|
||||||
|
items={catalog.items}
|
||||||
|
onSaved={() => undefined}
|
||||||
|
resource={resource}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
assert.ok(html.includes("Cantidad contratada"));
|
||||||
|
assert.ok(html.includes("Fecha de renovación"));
|
||||||
|
assert.ok(html.includes("Renovación automática"));
|
||||||
|
assert.ok(html.includes(help));
|
||||||
|
assert.ok(html.indexOf(help) < html.indexOf('type="submit"'));
|
||||||
|
assert.equal(html.includes("Cantidad contratada *"), false);
|
||||||
|
assert.equal(html.includes("licenseDescription"), false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user