feat(api): módulo del API Gateway (reportes Crystal): sesión y cola propias, matrícula de reportes y ejecución con PDF
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
74743503f0
commit
2a29c63e7d
13 files changed
+774
-2
No files matched your search
@@ -8,7 +8,10 @@ export type AuditAction =
|
||||
| 'sl_action'
|
||||
| 'sl_denied'
|
||||
| 'permissions_set'
|
||||
| 'permissions_remove';
|
||||
| 'permissions_remove'
|
||||
| 'report_export'
|
||||
| 'report_register'
|
||||
| 'report_unregister';
|
||||
|
||||
export type AuditOutcome = 'ok' | 'error' | 'denied';
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { type GatewayParam, exportBody, paramsFromLoadCr } from './gateway.js';
|
||||
|
||||
describe('paramsFromLoadCr', () => {
|
||||
it('convierte lo que describe LoadCR en definiciones de matrícula', () => {
|
||||
const params = paramsFromLoadCr([
|
||||
{ name: 'PWZRunDate', type: 'xsd:date', description: 'Fecha de ejecución de pago:', isOptionalPrompt: 'true', valueRangeKind: 'Range', allowMultiValue: 'false', defaultValues: [] },
|
||||
{ name: 'Dockey@', type: 'xsd:decimal', description: 'Especificar Dockey@:', isOptionalPrompt: 'false', valueRangeKind: 'Discrete', allowMultiValue: 'false' },
|
||||
{ name: 'BP@From OCRD', type: 'xsd:string', description: 'Socios de negocios', isOptionalPrompt: 'true', allowMultiValue: 'true', defaultValues: ['C1'] },
|
||||
{ type: 'xsd:string' },
|
||||
]);
|
||||
expect(params).toEqual([
|
||||
{ name: 'PWZRunDate', type: 'xsd:date', label: 'Fecha de ejecución de pago', kind: 'date', required: false, range: true },
|
||||
{ name: 'Dockey@', type: 'xsd:decimal', label: 'Dockey@', kind: 'number', required: true },
|
||||
{ name: 'BP@From OCRD', type: 'xsd:string', label: 'Socios de negocios', kind: 'text', required: false, multiple: true, defaultValue: 'C1' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exportBody', () => {
|
||||
const p = (over: Partial<GatewayParam>): GatewayParam => ({ name: 'P', type: 'xsd:string', label: 'Parámetro', kind: 'text', required: false, ...over });
|
||||
|
||||
it('un valor va como lista de listas, con el nombre y el tipo del parámetro', () => {
|
||||
expect(exportBody([p({ name: 'Dockey@', type: 'xsd:decimal', required: true })], { 'Dockey@': ' 137138 ' }).body).toEqual([{ name: 'Dockey@', type: 'xsd:decimal', value: [['137138']] }]);
|
||||
});
|
||||
|
||||
it('varios valores van uno por lista y un rango, desde y hasta en una sola', () => {
|
||||
expect(exportBody([p({ multiple: true })], { P: ['A', ' ', 'B'] }).body[0].value).toEqual([['A'], ['B']]);
|
||||
expect(exportBody([p({ range: true })], { P: ['2026-01-01', '2026-01-31'] }).body[0].value).toEqual([['2026-01-01', '2026-01-31']]);
|
||||
expect(exportBody([p({ range: true })], { P: ['2026-01-01', ''] }).body[0].value).toEqual([['2026-01-01', '2026-01-01']]); // sin «hasta», el mismo día
|
||||
});
|
||||
|
||||
it('un opcional vacío no se envía y un obligatorio vacío se reporta', () => {
|
||||
const r = exportBody([p({ name: 'A', label: 'Cliente', required: true }), p({ name: 'B' })], { A: '', B: '' });
|
||||
expect(r.body).toEqual([]);
|
||||
expect(r.missing).toEqual(['Cliente']);
|
||||
});
|
||||
|
||||
it('ignora valores de parámetros que no están matriculados', () => {
|
||||
expect(exportBody([p({ name: 'A' })], { A: '1', Otro: '2' }).body.map((b) => b.name)).toEqual(['A']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Reportes del API Gateway de SAP B1 (Crystal Reports): cómo se matriculan, qué parámetros piden y cómo se arma lo
|
||||
* que se envía a `ExportPDFData`. Lo comparten la API y la web.
|
||||
*/
|
||||
|
||||
export type GatewayParamKind = 'text' | 'number' | 'date' | 'select' | 'lookup';
|
||||
|
||||
/** Un parámetro de un reporte matriculado. */
|
||||
export interface GatewayParam {
|
||||
/** Nombre del parámetro en Crystal, con su sufijo (`Item@Select * from OITM`, `Dockey@`). */
|
||||
name: string;
|
||||
/** Tipo de Crystal (`xsd:string`, `xsd:decimal`, `xsd:date`…); se envía tal cual. */
|
||||
type: string;
|
||||
label: string;
|
||||
/** Cómo se pide al usuario: texto, número, fecha, lista fija o ventana de selección. */
|
||||
kind: GatewayParamKind;
|
||||
required: boolean;
|
||||
/** Se piden dos valores, desde y hasta. */
|
||||
range?: boolean;
|
||||
/** Se admiten varios valores. */
|
||||
multiple?: boolean;
|
||||
defaultValue?: string;
|
||||
placeholder?: string;
|
||||
/** Opciones de una lista fija (`kind = select`). */
|
||||
options?: { value: string; label: string }[];
|
||||
/** Ventana de selección (`kind = lookup`): identificador de su catálogo en la web (`socios`, `articulos`, `doc:Invoices`…). */
|
||||
lookup?: string;
|
||||
}
|
||||
|
||||
/** Un reporte matriculado: el que los usuarios ven en la lista para ejecutar. */
|
||||
export interface GatewayReport {
|
||||
/** Código del reporte en el API Gateway (`RCRI0001`, `DLN20015`). */
|
||||
code: string;
|
||||
title: string;
|
||||
category: string;
|
||||
description?: string;
|
||||
enabled: boolean;
|
||||
params: GatewayParam[];
|
||||
}
|
||||
|
||||
/** Un reporte que el API Gateway autoriza (`LoadAuthorizedCRList`). */
|
||||
export interface AuthorizedReport {
|
||||
code: string;
|
||||
name: string;
|
||||
/** Carpeta de Crystal (`#1000004#4`). */
|
||||
root_name: string;
|
||||
root_guid: string;
|
||||
}
|
||||
|
||||
/** Lo que el usuario escribió: un texto, o una lista si el parámetro es de rango o admite varios valores. */
|
||||
export type GatewayValues = Readonly<Record<string, string | readonly string[] | undefined>>;
|
||||
|
||||
/** Un parámetro tal como lo describe `LoadCR`. */
|
||||
interface CrystalParameter {
|
||||
name?: string;
|
||||
type?: string;
|
||||
description?: string;
|
||||
isOptionalPrompt?: string;
|
||||
valueRangeKind?: string;
|
||||
allowMultiValue?: string;
|
||||
defaultValues?: unknown[];
|
||||
}
|
||||
|
||||
const NUMERIC = /(decimal|int|double|float|number|currency)/i;
|
||||
|
||||
/** Los parámetros de un reporte (respuesta de `LoadCR`) como definiciones de matrícula, para completarlas y ajustarlas. */
|
||||
export function paramsFromLoadCr(resultSet: readonly CrystalParameter[]): GatewayParam[] {
|
||||
return resultSet
|
||||
.filter((p): p is CrystalParameter & { name: string } => typeof p.name === 'string' && p.name !== '')
|
||||
.map((p) => {
|
||||
const type = p.type ?? 'xsd:string';
|
||||
const first = p.defaultValues?.[0];
|
||||
return {
|
||||
name: p.name,
|
||||
type,
|
||||
label: (p.description ?? p.name).replace(/[:\s]+$/, '').replace(/^Especificar\s+/i, '') || p.name,
|
||||
kind: /date/i.test(type) ? 'date' : NUMERIC.test(type) ? 'number' : 'text',
|
||||
required: p.isOptionalPrompt !== 'true',
|
||||
...(p.valueRangeKind === 'Range' ? { range: true } : {}),
|
||||
...(p.allowMultiValue === 'true' ? { multiple: true } : {}),
|
||||
...(first !== undefined && first !== null && first !== '' ? { defaultValue: String(first) } : {}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const isBlank = (v: string | readonly string[] | undefined): boolean => v === undefined || (typeof v === 'string' ? v.trim() === '' : v.every((x) => x.trim() === ''));
|
||||
|
||||
/**
|
||||
* Cuerpo de `ExportPDFData`: por cada parámetro matriculado `{ name, type, value }`, con el valor como lista de listas
|
||||
* (`[["137138"]]` un valor, `[["a"],["b"]]` varios, `[["desde","hasta"]]` un rango). Un parámetro opcional sin valor no se
|
||||
* envía. Devuelve los parámetros obligatorios que faltan por separado.
|
||||
*/
|
||||
export function exportBody(params: readonly GatewayParam[], values: GatewayValues): { body: { name: string; type: string; value: string[][] }[]; missing: string[] } {
|
||||
const body: { name: string; type: string; value: string[][] }[] = [];
|
||||
const missing: string[] = [];
|
||||
for (const p of params) {
|
||||
const raw = values[p.name];
|
||||
if (isBlank(raw)) {
|
||||
if (p.required) missing.push(p.label);
|
||||
continue;
|
||||
}
|
||||
const list = (typeof raw === 'string' ? [raw] : [...(raw as readonly string[])]).map((x) => x.trim());
|
||||
let value: string[][];
|
||||
if (p.range) value = [[list[0] ?? '', list[1] || list[0] || '']];
|
||||
else if (p.multiple) value = list.filter((x) => x !== '').map((x) => [x]);
|
||||
else value = [[list[0]]];
|
||||
body.push({ name: p.name, type: p.type, value });
|
||||
}
|
||||
return { body, missing };
|
||||
}
|
||||
@@ -4,3 +4,4 @@ export * from './company.js';
|
||||
export * from './permissions.js';
|
||||
export * from './sl.js';
|
||||
export * from './menu.js';
|
||||
export * from './gateway.js';
|
||||
Reference in new issue
Block a user