#49 feat: grafico unificado de provenance con filtro por niveles y re-estabilizacion
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
buildSupplierOrganizationGraph,
|
||||
buildTerritoryGraph,
|
||||
buildProvenanceGraph,
|
||||
hasAnySupplier,
|
||||
hasAnyTerritory,
|
||||
} from '@/components/provenance/provenance-graph'
|
||||
|
||||
const singleSupplier = [
|
||||
const ALL_KINDS = ['product', 'supplier', 'organization', 'municipality', 'department', 'country']
|
||||
|
||||
const singleRelation = [
|
||||
{
|
||||
product: {
|
||||
id: 1,
|
||||
@@ -15,39 +15,41 @@ const singleSupplier = [
|
||||
},
|
||||
suppliers: [
|
||||
{
|
||||
supplier: { id: 5, name: 'Asociación Agropecuaria La Mesa', description: 'Cooperativa' },
|
||||
organization: { id: 3, name: 'Red de Economía Solidaria', description: 'Red' },
|
||||
supplier: { id: 5, name: 'Asociación Agropecuaria La Mesa' },
|
||||
organization: { id: 3, name: 'Red de Economía Solidaria' },
|
||||
municipality: { id: 7, name: 'La Mesa' },
|
||||
department: { id: 2, name: 'Cundinamarca' },
|
||||
country: { id: 1, name: 'Colombia', code: 'CO' },
|
||||
country: { id: 1, name: 'Colombia' },
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const twoSuppliers = (overrides = {}) => [
|
||||
{
|
||||
product: { id: 1, name: 'Panela' },
|
||||
suppliers: [
|
||||
{
|
||||
supplier: { id: 5, name: 'Asociación A' },
|
||||
organization: { id: 3, name: 'Red' },
|
||||
municipality: { id: 7, name: 'La Mesa' },
|
||||
department: { id: 2, name: 'Cundinamarca' },
|
||||
country: { id: 1, name: 'Colombia' },
|
||||
...(overrides.first || {}),
|
||||
},
|
||||
{
|
||||
supplier: { id: 6, name: 'Asociación B' },
|
||||
organization: { id: 3, name: 'Red' },
|
||||
municipality: { id: 8, name: 'San Antonio' },
|
||||
department: { id: 3, name: 'Antioquia' },
|
||||
country: { id: 1, name: 'Colombia' },
|
||||
...(overrides.second || {}),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
function twoSuppliers (overrides = {}) {
|
||||
return [
|
||||
{
|
||||
product: { id: 1, name: 'Panela' },
|
||||
suppliers: [
|
||||
{
|
||||
supplier: { id: 5, name: 'Asociación A' },
|
||||
organization: { id: 3, name: 'Red' },
|
||||
municipality: { id: 7, name: 'La Mesa' },
|
||||
department: { id: 2, name: 'Cundinamarca' },
|
||||
country: { id: 1, name: 'Colombia' },
|
||||
...(overrides.first || {}),
|
||||
},
|
||||
{
|
||||
supplier: { id: 6, name: 'Asociación B' },
|
||||
organization: { id: 3, name: 'Red' },
|
||||
municipality: { id: 8, name: 'San Antonio' },
|
||||
department: { id: 3, name: 'Antioquia' },
|
||||
country: { id: 1, name: 'Colombia' },
|
||||
...(overrides.second || {}),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function edgeOf (graph, from, to) {
|
||||
return graph.edges.find(edge => edge.from === from && edge.to === to)
|
||||
@@ -55,7 +57,7 @@ function edgeOf (graph, from, to) {
|
||||
|
||||
describe('hasAnySupplier', () => {
|
||||
it('es true cuando algún producto tiene proveedores', () => {
|
||||
expect(hasAnySupplier(singleSupplier)).toBe(true)
|
||||
expect(hasAnySupplier(singleRelation)).toBe(true)
|
||||
})
|
||||
|
||||
it('es false cuando todos los productos están sin proveedores', () => {
|
||||
@@ -68,115 +70,82 @@ describe('hasAnySupplier', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('hasAnyTerritory', () => {
|
||||
it('es true cuando algún proveedor tiene territorio', () => {
|
||||
expect(hasAnyTerritory(singleSupplier)).toBe(true)
|
||||
})
|
||||
describe('buildProvenanceGraph', () => {
|
||||
it('por defecto solo grafica productos y proveedores', () => {
|
||||
const graph = buildProvenanceGraph(singleRelation)
|
||||
|
||||
it('es false cuando todos los territorios vienen en null', () => {
|
||||
const noTerritory = [
|
||||
{
|
||||
product: { id: 1, name: 'Panela' },
|
||||
suppliers: [{ supplier: { id: 5, name: 'La Mesa' }, organization: null, municipality: null, department: null, country: null }],
|
||||
},
|
||||
]
|
||||
expect(hasAnyTerritory(noTerritory)).toBe(false)
|
||||
})
|
||||
|
||||
it('es false cuando no hay suppliers', () => {
|
||||
const noSuppliers = [{ product: { id: 1, name: 'Panela' }, suppliers: [] }]
|
||||
expect(hasAnyTerritory(noSuppliers)).toBe(false)
|
||||
})
|
||||
|
||||
it('es true cuando hay municipio aunque el país venga null', () => {
|
||||
const data = [
|
||||
{
|
||||
product: { id: 1, name: 'Panela' },
|
||||
suppliers: [{ supplier: { id: 5, name: 'La Mesa' }, municipality: { id: 7, name: 'Santa Bárbara' }, department: null, country: null }],
|
||||
},
|
||||
]
|
||||
expect(hasAnyTerritory(data)).toBe(true)
|
||||
})
|
||||
|
||||
it('es false cuando solo hay país', () => {
|
||||
const data = [
|
||||
{
|
||||
product: { id: 1, name: 'Panela' },
|
||||
suppliers: [{ supplier: { id: 5, name: 'La Mesa' }, municipality: null, department: null, country: { id: 1, name: 'Colombia' } }],
|
||||
},
|
||||
]
|
||||
expect(hasAnyTerritory(data)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildSupplierOrganizationGraph', () => {
|
||||
it('con un único proveedor conecta con arista cierta y sin junction', () => {
|
||||
const graph = buildSupplierOrganizationGraph(singleSupplier)
|
||||
|
||||
expect(graph.nodes.map(n => n.id)).not.toContain('junction:1')
|
||||
expect(edgeOf(graph, 'product:1', 'supplier:5')).toEqual({ from: 'product:1', to: 'supplier:5', certain: true })
|
||||
})
|
||||
|
||||
it('con varios proveedores crea un punto de disyunción: sólida hasta él y dudosa hacia cada proveedor', () => {
|
||||
const graph = buildSupplierOrganizationGraph(twoSuppliers())
|
||||
|
||||
expect(graph.nodes.map(n => n.id)).toContain('junction:1')
|
||||
expect(edgeOf(graph, 'product:1', 'junction:1')).toEqual({ from: 'product:1', to: 'junction:1', certain: true })
|
||||
expect(edgeOf(graph, 'junction:1', 'supplier:5')).toEqual({ from: 'junction:1', to: 'supplier:5', certain: false })
|
||||
expect(edgeOf(graph, 'junction:1', 'supplier:6')).toEqual({ from: 'junction:1', to: 'supplier:6', certain: false })
|
||||
})
|
||||
|
||||
it('una organización compartida por todos los proveedores es cierta', () => {
|
||||
const graph = buildSupplierOrganizationGraph(twoSuppliers())
|
||||
|
||||
expect(edgeOf(graph, 'supplier:5', 'organization:3')).toEqual({ from: 'supplier:5', to: 'organization:3', certain: true })
|
||||
expect(edgeOf(graph, 'supplier:6', 'organization:3')).toEqual({ from: 'supplier:6', to: 'organization:3', certain: true })
|
||||
})
|
||||
|
||||
it('organizaciones distintas entre proveedores son dudosas', () => {
|
||||
const graph = buildSupplierOrganizationGraph(twoSuppliers({
|
||||
first: { organization: { id: 3, name: 'Red' } },
|
||||
second: { organization: { id: 4, name: 'Cooperativa' } },
|
||||
}))
|
||||
|
||||
expect(edgeOf(graph, 'supplier:5', 'organization:3').certain).toBe(false)
|
||||
expect(edgeOf(graph, 'supplier:6', 'organization:4').certain).toBe(false)
|
||||
})
|
||||
|
||||
it('incluye la imagen del producto y el detalle de la entidad', () => {
|
||||
const graph = buildSupplierOrganizationGraph(singleSupplier)
|
||||
const product = graph.nodes.find(n => n.id === 'product:1')
|
||||
|
||||
expect(product.image).toBe('http://localhost/media/panela.jpg')
|
||||
expect(product.label).toBe('Panela regional por Kg')
|
||||
expect(product.entity.kind).toBe('product')
|
||||
expect(product.entity.name).toBe('Panela regional por Kg')
|
||||
})
|
||||
|
||||
it('omite el nodo de organización cuando la relación es null', () => {
|
||||
const data = [
|
||||
{
|
||||
product: { id: 1, name: 'Panela' },
|
||||
suppliers: [{ supplier: { id: 5, name: 'La Mesa' }, organization: null }],
|
||||
},
|
||||
]
|
||||
const graph = buildSupplierOrganizationGraph(data)
|
||||
|
||||
expect(graph.nodes.map(n => n.id)).not.toContain('organization:')
|
||||
expect(graph.nodes.map(n => n.kind)).toEqual(['product', 'supplier'])
|
||||
expect(graph.edges).toEqual([{ from: 'product:1', to: 'supplier:5', certain: true }])
|
||||
})
|
||||
|
||||
it('no duplica nodos ni aristas cuando se repiten entre productos', () => {
|
||||
const data = [
|
||||
{ product: { id: 1, name: 'Panela' }, suppliers: [{ supplier: { id: 5, name: 'La Mesa' }, organization: { id: 3, name: 'Red' } }] },
|
||||
{ product: { id: 2, name: 'Arroz' }, suppliers: [{ supplier: { id: 5, name: 'La Mesa' }, organization: { id: 3, name: 'Red' } }] },
|
||||
]
|
||||
const graph = buildSupplierOrganizationGraph(data)
|
||||
it('con todos los niveles grafica la cadena completa y la organización', () => {
|
||||
const graph = buildProvenanceGraph(singleRelation, ALL_KINDS)
|
||||
|
||||
expect(graph.nodes.filter(n => n.id === 'supplier:5')).toHaveLength(1)
|
||||
expect(graph.nodes.filter(n => n.id === 'organization:3')).toHaveLength(1)
|
||||
expect(graph.edges.filter(e => e.from === 'supplier:5' && e.to === 'organization:3')).toHaveLength(1)
|
||||
expect(graph.edges).toHaveLength(3)
|
||||
expect(edgeOf(graph, 'product:1', 'supplier:5')).toEqual({ from: 'product:1', to: 'supplier:5', certain: true })
|
||||
expect(edgeOf(graph, 'supplier:5', 'organization:3').certain).toBe(true)
|
||||
expect(edgeOf(graph, 'supplier:5', 'municipality:7')).toEqual({ from: 'supplier:5', to: 'municipality:7', certain: true })
|
||||
expect(edgeOf(graph, 'municipality:7', 'department:2').certain).toBe(true)
|
||||
expect(edgeOf(graph, 'department:2', 'country:1').certain).toBe(true)
|
||||
})
|
||||
|
||||
it('con varios proveedores crea el punto de disyunción y la duda se propaga', () => {
|
||||
const graph = buildProvenanceGraph(twoSuppliers(), ALL_KINDS)
|
||||
|
||||
expect(graph.nodes.map(n => n.id)).toContain('junction:1')
|
||||
expect(edgeOf(graph, 'product:1', 'junction:1')).toEqual({ from: 'product:1', to: 'junction:1', certain: true })
|
||||
expect(edgeOf(graph, 'junction:1', 'supplier:5').certain).toBe(false)
|
||||
expect(edgeOf(graph, 'junction:1', 'supplier:6').certain).toBe(false)
|
||||
expect(edgeOf(graph, 'supplier:5', 'municipality:7').certain).toBe(false)
|
||||
expect(edgeOf(graph, 'municipality:7', 'department:2').certain).toBe(false)
|
||||
expect(edgeOf(graph, 'department:2', 'country:1').certain).toBe(true)
|
||||
expect(edgeOf(graph, 'supplier:5', 'organization:3').certain).toBe(true)
|
||||
})
|
||||
|
||||
it('omite los niveles no solicitados conectando el nivel previo con el siguiente', () => {
|
||||
const graph = buildProvenanceGraph(singleRelation, ['product', 'supplier', 'department'])
|
||||
|
||||
expect(graph.nodes.map(n => n.kind)).toEqual(['product', 'supplier', 'department'])
|
||||
expect(edgeOf(graph, 'supplier:5', 'department:2')).toEqual({ from: 'supplier:5', to: 'department:2', certain: true })
|
||||
expect(edgeOf(graph, 'supplier:5', 'municipality:7')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('sin proveedores activos conecta el producto con el siguiente nivel', () => {
|
||||
const graph = buildProvenanceGraph(singleRelation, ['product', 'municipality', 'department'])
|
||||
|
||||
expect(edgeOf(graph, 'product:1', 'municipality:7')).toEqual({ from: 'product:1', to: 'municipality:7', certain: true })
|
||||
expect(edgeOf(graph, 'municipality:7', 'department:2').certain).toBe(true)
|
||||
})
|
||||
|
||||
it('sin productos activos los proveedores quedan como raíces sin junction', () => {
|
||||
const graph = buildProvenanceGraph(twoSuppliers(), ['supplier', 'municipality'])
|
||||
|
||||
expect(graph.nodes.some(n => n.kind === 'product')).toBe(false)
|
||||
expect(graph.nodes.some(n => n.kind === 'junction')).toBe(false)
|
||||
expect(edgeOf(graph, 'supplier:5', 'municipality:7').certain).toBe(false)
|
||||
})
|
||||
|
||||
it('con país activo crea el nodo país aunque se omitan niveles intermedios', () => {
|
||||
const graph = buildProvenanceGraph(singleRelation, ['product', 'supplier', 'country'])
|
||||
|
||||
expect(graph.nodes.map(n => n.id)).toContain('country:1')
|
||||
expect(edgeOf(graph, 'supplier:5', 'country:1').certain).toBe(true)
|
||||
})
|
||||
|
||||
it('no crea nodos de país si no se solicita', () => {
|
||||
const graph = buildProvenanceGraph(singleRelation, ['product', 'supplier', 'municipality'])
|
||||
|
||||
expect(graph.nodes.some(n => n.kind === 'country')).toBe(false)
|
||||
})
|
||||
|
||||
it('no duplica aristas cuando varios productos comparten proveedor y municipio', () => {
|
||||
const data = [
|
||||
{ product: { id: 1, name: 'Panela' }, suppliers: [{ supplier: { id: 5, name: 'A' }, municipality: { id: 7, name: 'La Mesa' }, department: { id: 2, name: 'Cundinamarca' } }] },
|
||||
{ product: { id: 2, name: 'Arroz' }, suppliers: [{ supplier: { id: 5, name: 'A' }, municipality: { id: 7, name: 'La Mesa' }, department: { id: 2, name: 'Cundinamarca' } }] },
|
||||
]
|
||||
const graph = buildProvenanceGraph(data, ['product', 'supplier', 'municipality'])
|
||||
|
||||
expect(graph.nodes.filter(n => n.id === 'municipality:7')).toHaveLength(1)
|
||||
expect(graph.edges.filter(e => e.from === 'supplier:5' && e.to === 'municipality:7')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('si el mismo par de nodos repite con distinta certeza, gana la duda', () => {
|
||||
@@ -184,98 +153,24 @@ describe('buildSupplierOrganizationGraph', () => {
|
||||
{ product: { id: 1, name: 'Panela' }, suppliers: [{ supplier: { id: 5, name: 'A' }, organization: { id: 3, name: 'Red' } }] },
|
||||
{ product: { id: 2, name: 'Arroz' }, suppliers: [{ supplier: { id: 5, name: 'A' }, organization: { id: 3, name: 'Red' } }, { supplier: { id: 6, name: 'B' }, organization: { id: 4, name: 'Coop' } }] },
|
||||
]
|
||||
const graph = buildSupplierOrganizationGraph(data)
|
||||
const graph = buildProvenanceGraph(data, ['product', 'supplier', 'organization'])
|
||||
|
||||
expect(graph.edges.filter(e => e.from === 'supplier:5' && e.to === 'organization:3')).toHaveLength(1)
|
||||
expect(edgeOf(graph, 'supplier:5', 'organization:3').certain).toBe(false)
|
||||
})
|
||||
|
||||
it('no incluye posiciones de layout', () => {
|
||||
const graph = buildSupplierOrganizationGraph(singleSupplier)
|
||||
it('incluye la imagen y el detalle de la entidad del producto', () => {
|
||||
const graph = buildProvenanceGraph(singleRelation)
|
||||
const product = graph.nodes.find(n => n.id === 'product:1')
|
||||
|
||||
expect(product.x).toBeUndefined()
|
||||
expect(product.y).toBeUndefined()
|
||||
expect(product.image).toBe('http://localhost/media/panela.jpg')
|
||||
expect(product.entity.kind).toBe('product')
|
||||
})
|
||||
|
||||
it('devuelve un grafo vacío cuando no hay provenance', () => {
|
||||
const graph = buildSupplierOrganizationGraph([])
|
||||
const graph = buildProvenanceGraph([])
|
||||
|
||||
expect(graph.nodes).toEqual([])
|
||||
expect(graph.edges).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildTerritoryGraph', () => {
|
||||
it('con un único proveedor toda la cadena es cierta', () => {
|
||||
const graph = buildTerritoryGraph(singleSupplier)
|
||||
|
||||
expect(edgeOf(graph, 'product:1', 'supplier:5')).toEqual({ from: 'product:1', to: 'supplier:5', certain: true })
|
||||
expect(edgeOf(graph, 'supplier:5', 'municipality:7')).toEqual({ from: 'supplier:5', to: 'municipality:7', certain: true })
|
||||
expect(edgeOf(graph, 'municipality:7', 'department:2')).toEqual({ from: 'municipality:7', to: 'department:2', certain: true })
|
||||
})
|
||||
|
||||
it('no incluye nodos ni aristas de país', () => {
|
||||
const graph = buildTerritoryGraph(singleSupplier)
|
||||
|
||||
expect(graph.nodes.map(n => n.id)).not.toContain('country:1')
|
||||
expect(graph.edges.some(edge => edge.from.startsWith('country:') || edge.to.startsWith('country:'))).toBe(false)
|
||||
})
|
||||
|
||||
it('con varios productos del mismo proveedor solo traza una línea hacia el municipio', () => {
|
||||
const data = [
|
||||
{ product: { id: 1, name: 'Panela' }, suppliers: [{ supplier: { id: 5, name: 'La Mesa' }, municipality: { id: 7, name: 'Santa Bárbara' }, department: { id: 2, name: 'Antioquia' } }] },
|
||||
{ product: { id: 2, name: 'Arroz' }, suppliers: [{ supplier: { id: 5, name: 'La Mesa' }, municipality: { id: 7, name: 'Santa Bárbara' }, department: { id: 2, name: 'Antioquia' } }] },
|
||||
{ product: { id: 3, name: 'Queso' }, suppliers: [{ supplier: { id: 5, name: 'La Mesa' }, municipality: { id: 7, name: 'Santa Bárbara' }, department: { id: 2, name: 'Antioquia' } }] },
|
||||
]
|
||||
const graph = buildTerritoryGraph(data)
|
||||
|
||||
expect(graph.edges.filter(e => e.from === 'supplier:5' && e.to === 'municipality:7')).toHaveLength(1)
|
||||
expect(graph.nodes.filter(n => n.id === 'municipality:7')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('cuando los proveedores comparten municipio, el territorio es cierto desde allí', () => {
|
||||
const graph = buildTerritoryGraph(twoSuppliers({
|
||||
second: { municipality: { id: 7, name: 'La Mesa' }, department: { id: 2, name: 'Cundinamarca' } },
|
||||
}))
|
||||
|
||||
expect(edgeOf(graph, 'product:1', 'junction:1').certain).toBe(true)
|
||||
expect(edgeOf(graph, 'junction:1', 'supplier:5').certain).toBe(false)
|
||||
expect(edgeOf(graph, 'supplier:5', 'municipality:7')).toEqual({ from: 'supplier:5', to: 'municipality:7', certain: true })
|
||||
expect(edgeOf(graph, 'supplier:6', 'municipality:7')).toEqual({ from: 'supplier:6', to: 'municipality:7', certain: true })
|
||||
expect(edgeOf(graph, 'municipality:7', 'department:2').certain).toBe(true)
|
||||
})
|
||||
|
||||
it('municipios distintos pero mismo departamento: dudoso hasta el municipio y cierto desde el departamento', () => {
|
||||
const graph = buildTerritoryGraph(twoSuppliers({
|
||||
second: { department: { id: 2, name: 'Cundinamarca' } },
|
||||
}))
|
||||
|
||||
expect(edgeOf(graph, 'supplier:5', 'municipality:7').certain).toBe(false)
|
||||
expect(edgeOf(graph, 'supplier:6', 'municipality:8').certain).toBe(false)
|
||||
expect(edgeOf(graph, 'municipality:7', 'department:2')).toEqual({ from: 'municipality:7', to: 'department:2', certain: true })
|
||||
expect(edgeOf(graph, 'municipality:8', 'department:2')).toEqual({ from: 'municipality:8', to: 'department:2', certain: true })
|
||||
})
|
||||
|
||||
it('municipios y departamentos distintos mantienen la duda', () => {
|
||||
const graph = buildTerritoryGraph(twoSuppliers())
|
||||
|
||||
expect(edgeOf(graph, 'supplier:5', 'municipality:7').certain).toBe(false)
|
||||
expect(edgeOf(graph, 'municipality:7', 'department:2').certain).toBe(false)
|
||||
expect(edgeOf(graph, 'supplier:6', 'municipality:8').certain).toBe(false)
|
||||
expect(edgeOf(graph, 'municipality:8', 'department:3').certain).toBe(false)
|
||||
})
|
||||
|
||||
it('no crea nodos de territorio cuando el proveedor no tiene municipio', () => {
|
||||
const data = [
|
||||
{
|
||||
product: { id: 1, name: 'Panela' },
|
||||
suppliers: [{ supplier: { id: 5, name: 'La Mesa' }, municipality: null, department: null, country: null }],
|
||||
},
|
||||
]
|
||||
const graph = buildTerritoryGraph(data)
|
||||
|
||||
expect(graph.nodes.map(n => n.id)).toEqual(['product:1', 'supplier:5'])
|
||||
expect(graph.edges).toEqual([{ from: 'product:1', to: 'supplier:5', certain: true }])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user