diff --git a/src/domain/bpmn-parser.ts b/src/domain/bpmn-parser.ts index d8d3c3b..35e044f 100644 --- a/src/domain/bpmn-parser.ts +++ b/src/domain/bpmn-parser.ts @@ -64,49 +64,58 @@ export function parseBpmnXml(xml: string): ProcessGraph { const flows = new Map() const startEventIds: BpmnElementId[] = [] - // Buscar el proceso principal (puede estar anidado bajo definitions) - const processEl = doc.querySelector('[*|localName="process"], process') - if (!processEl) { + // Buscar TODOS los procesos del documento. Las colaboraciones tienen múltiples + // como hijos directos de , uno por pool. + const definitions = doc.documentElement + const processEls = Array.from(definitions.children).filter( + (el) => normalizeLocalName(el.tagName) === 'process' + ) + + if (processEls.length === 0) { throw new BpmnParseError('No se encontró un elemento en el BPMN') } - // Iterar todos los hijos directos del proceso - for (const child of Array.from(processEl.children)) { - const local = normalizeLocalName(child.tagName) - const type: BpmnNodeType = TAG_TO_TYPE[local] ?? 'unknown' + // Parsear cada proceso y acumular nodos y flujos en mapas compartidos + for (const processEl of processEls) { + for (const child of Array.from(processEl.children)) { + const local = normalizeLocalName(child.tagName) + const type: BpmnNodeType = TAG_TO_TYPE[local] ?? 'unknown' - if (local === 'sequenceflow') { - const flow: ProcessGraphFlow = { - id: getAttr(child, 'id'), - sourceRef: getAttr(child, 'sourceRef'), - targetRef: getAttr(child, 'targetRef'), - name: getAttr(child, 'name') || undefined, + if (local === 'sequenceflow') { + const flow: ProcessGraphFlow = { + id: getAttr(child, 'id'), + sourceRef: getAttr(child, 'sourceRef'), + targetRef: getAttr(child, 'targetRef'), + name: getAttr(child, 'name') || undefined, + } + if (flow.id) flows.set(flow.id, flow) + continue } - if (flow.id) flows.set(flow.id, flow) - continue + + if (type === 'unknown') continue + + const id = getAttr(child, 'id') + if (!id) continue + + const node: ProcessGraphNode = { + id, + name: getAttr(child, 'name'), + type, + outgoing: parseFlowRefs(child, 'outgoing'), + incoming: parseFlowRefs(child, 'incoming'), + } + + nodes.set(id, node) + if (type === 'startEvent') startEventIds.push(id) } - - if (type === 'unknown') continue - - const id = getAttr(child, 'id') - if (!id) continue - - const node: ProcessGraphNode = { - id, - name: getAttr(child, 'name'), - type, - outgoing: parseFlowRefs(child, 'outgoing'), - incoming: parseFlowRefs(child, 'incoming'), - } - - nodes.set(id, node) - if (type === 'startEvent') startEventIds.push(id) } if (nodes.size === 0) { throw new BpmnParseError('El proceso no contiene elementos reconocibles') } + // Pools de soporte (SAP, bandeja manual, etc.) pueden no tener StartEvent propio — + // exigimos al menos uno entre todos los pools del diagrama. if (startEventIds.length === 0) { throw new BpmnParseError('El proceso no tiene evento de inicio (StartEvent)') } diff --git a/tests/integration/bpmn-collaboration.test.ts b/tests/integration/bpmn-collaboration.test.ts new file mode 100644 index 0000000..06d1f8b --- /dev/null +++ b/tests/integration/bpmn-collaboration.test.ts @@ -0,0 +1,42 @@ +import { describe, it, expect } from 'vitest' +import { readFileSync } from 'fs' +import { resolve } from 'path' +import { parseBpmnXml, extractActivityElements } from '@/domain/bpmn-parser' + +describe('Colaboración multi-pool', () => { + const xml = readFileSync(resolve(__dirname, '../proceso_carga_facturas.bpmn'), 'utf-8') + + it('parsea sin error', () => { + expect(() => parseBpmnXml(xml)).not.toThrow() + }) + + it('extrae actividades de TODOS los pools, no solo el primero', () => { + const graph = parseBpmnXml(xml) + const acts = extractActivityElements(graph) + const ids = acts.map((a) => a.bpmnElementId) + + // Pool 1 — IKData (OCR) + expect(ids).toContain('Task_OCR') + expect(ids).toContain('Task_GenerarPayload') + + // Pool 2 — Robot RPA (proceso principal) + expect(ids).toContain('Task_LeerPayload') + expect(ids).toContain('Task_AbrirSAP') + expect(ids).toContain('Task_GuardarFactura') + expect(ids).toContain('Task_DerivarBandeja') + + // Pool 3 — SAP (pool de soporte sin StartEvent propio) + expect(ids).toContain('Task_RecibirFactura') + + // Pool 4 — Bandeja manual (pool de soporte sin StartEvent propio) + expect(ids).toContain('Task_RevisionManual') + + expect(acts.length).toBeGreaterThanOrEqual(14) + }) + + it('tiene StartEvents de los pools principales aunque otros pools no tengan', () => { + const graph = parseBpmnXml(xml) + expect(graph.startEventIds).toContain('StartEvent_IKData') + expect(graph.startEventIds).toContain('StartEvent_Robot') + }) +})