#49 feat: graficos de provenance en columnas por nivel y aristas deduplicadas

This commit is contained in:
2026-08-16 02:21:41 -05:00
parent 2967ea4f4d
commit 5c91e66429
6 changed files with 166 additions and 25 deletions

View File

@@ -177,7 +177,7 @@ No hay un estilo mayoritario. El código histórico está partido:
- Payload: `[{ product: {id, name, catalogue_images[]}, suppliers: [{ supplier: {...}, organization|null, municipality|null, department|null, country|null }] }]`. - Payload: `[{ product: {id, name, catalogue_images[]}, suppliers: [{ supplier: {...}, organization|null, municipality|null, department|null, country|null }] }]`.
- **Genérico reutilizable:** - **Genérico reutilizable:**
- `src/components/graph/VisChart.vue`: wrapper de vis-network (props `nodes`, `edges`, `height`, `options`; import `import { DataSet, Network } from 'vis-network/standalone'`; emite `select` con el nodo). **OJO**: nodo con `image: null` → TypeError de vis; omitir la clave `image` si no hay foto - `src/components/graph/VisChart.vue`: wrapper de vis-network (props `nodes`, `edges`, `height`, `options`; import `import { DataSet, Network } from 'vis-network/standalone'`; emite `select` con el nodo). **OJO**: nodo con `image: null` → TypeError de vis; omitir la clave `image` si no hay foto
- **Específico público** (`src/components/provenance/`): builders puros en `provenance-graph.js` (`buildSupplierOrganizationGraph`, `buildTerritoryGraph`, `hasAnySupplier`, `hasAnyTerritory`) y el adaptador vis `provenance-vis.js` (`toVisNodes`, `toVisEdges`, `chartOptions`). **Semántica de certeza**: arista con `certain: true` es continua (inequívoca) y `certain: false` es discontinua (dudosa); con varios proveedores por producto se inserta un nodo `junction:<productId>` (disyunción) con arista sólida hasta él y discontinua hacia cada proveedor; la duda se corta donde los proveedores coinciden (misma organización/municipio/departamento). Los charts (que muestran "próximamente estará disponible" cuando no hay relaciones), `ProvenanceDetailModal.vue` y `ProvenanceSection.vue` - **Específico público** (`src/components/provenance/`): builders puros en `provenance-graph.js` (`buildSupplierOrganizationGraph`, `buildTerritoryGraph`, `hasAnySupplier`, `hasAnyTerritory`) y el adaptador vis `provenance-vis.js` (`toVisNodes`, `toVisEdges`, `chartOptions`). **Semántica de certeza**: arista con `certain: true` es continua (inequívoca) y `certain: false` es discontinua (dudosa); con varios proveedores por producto se inserta un nodo `junction:<productId>` (disyunción) con arista sólida hasta él y discontinua hacia cada proveedor; la duda se corta donde los proveedores coinciden (misma organización/municipio/departamento). El gráfico de territorio va solo hasta departamento (sin país, por ahora Colombia). Las aristas se deduplican por par `(from, to)` y si un mismo par repite con distinta certeza gana la duda. Los charts (que muestran "próximamente estará disponible" cuando no hay relaciones), `ProvenanceDetailModal.vue` y `ProvenanceSection.vue`
- **Admin CRUD** (`src/components/provenance/admin/`): `OrganizationsManagement.vue`, `SuppliersManagement.vue`, `GeographyManagement.vue` (tabs países/departamentos/municipios), `SupplierLinkDialog.vue` (vincula productos↔proveedores, abierto desde `ProductsManagement.vue`). Páginas en `src/pages/admin/{organizations,suppliers,geography}.vue`; rutas en `ADMIN_ROUTES` (`router/index.js`); ítems en `NavBar.vue` - **Admin CRUD** (`src/components/provenance/admin/`): `OrganizationsManagement.vue`, `SuppliersManagement.vue`, `GeographyManagement.vue` (tabs países/departamentos/municipios), `SupplierLinkDialog.vue` (vincula productos↔proveedores, abierto desde `ProductsManagement.vue`). Páginas en `src/pages/admin/{organizations,suppliers,geography}.vue`; rutas en `ADMIN_ROUTES` (`router/index.js`); ítems en `NavBar.vue`
- **Endpoints provenance**: `/don_confiao/api/organizations/`, `/suppliers/`, `/countries/`, `/departments/`, `/municipalities/` (CRUD); vincular productos con `PATCH /don_confiao/api/products/<id>/` body `{"suppliers": [ids]}`; detalle de producto (con `suppliers`) via `GET /don_confiao/api/products/<id>/` - **Endpoints provenance**: `/don_confiao/api/organizations/`, `/suppliers/`, `/countries/`, `/departments/`, `/municipalities/` (CRUD); vincular productos con `PATCH /don_confiao/api/products/<id>/` body `{"suppliers": [ids]}`; detalle de producto (con `suppliers`) via `GET /don_confiao/api/products/<id>/`
- Los tests mockean `vis-network/standalone` (`vi.mock('vis-network/standalone', ...)`) o el propio `VisChart.vue`, y la API con `global.provide: { api }` - Los tests mockean `vis-network/standalone` (`vi.mock('vis-network/standalone', ...)`) o el propio `VisChart.vue`, y la API con `global.provide: { api }`

View File

@@ -17,6 +17,7 @@ function makeCollector () {
const nodes = [] const nodes = []
const edges = [] const edges = []
const seen = new Map() const seen = new Map()
const edgeSeen = new Map()
return { return {
nodes, nodes,
edges, edges,
@@ -36,7 +37,14 @@ function makeCollector () {
return seen.get(id) return seen.get(id)
}, },
addEdge (from, to, certain) { addEdge (from, to, certain) {
edges.push({ from, to, certain: Boolean(certain) }) const key = `${from}\u0000${to}`
if (edgeSeen.has(key)) {
edgeSeen.get(key).certain = edgeSeen.get(key).certain && Boolean(certain)
return
}
const edge = { from, to, certain: Boolean(certain) }
edgeSeen.set(key, edge)
edges.push(edge)
}, },
} }
} }
@@ -97,7 +105,6 @@ export function buildTerritoryGraph (provenance) {
const municipalityCertain = isCertain(distinctValues(relations.map(rel => rel.municipality))) const municipalityCertain = isCertain(distinctValues(relations.map(rel => rel.municipality)))
const departmentCertain = isCertain(distinctValues(relations.map(rel => rel.department))) const departmentCertain = isCertain(distinctValues(relations.map(rel => rel.department)))
const countryCertain = isCertain(distinctValues(relations.map(rel => rel.country)))
for (const rel of relations) { for (const rel of relations) {
if (!rel.municipality) continue if (!rel.municipality) continue
@@ -110,12 +117,6 @@ export function buildTerritoryGraph (provenance) {
const department = collector.addNode('department', rel.department) const department = collector.addNode('department', rel.department)
collector.addEdge(municipality.id, department.id, departmentCertain) collector.addEdge(municipality.id, department.id, departmentCertain)
} }
for (const rel of relations) {
if (!rel.department || !rel.country) continue
const department = collector.addNode('department', rel.department)
const country = collector.addNode('country', rel.country)
collector.addEdge(department.id, country.id, countryCertain)
}
} }
return { nodes: collector.nodes, edges: collector.edges } return { nodes: collector.nodes, edges: collector.edges }
} }
@@ -127,7 +128,7 @@ export function hasAnySupplier (provenance) {
export function hasAnyTerritory (provenance) { export function hasAnyTerritory (provenance) {
return (provenance || []).some(entry => return (provenance || []).some(entry =>
(entry.suppliers || []).some(rel => (entry.suppliers || []).some(rel =>
rel.municipality || rel.department || rel.country rel.municipality || rel.department
) )
) )
} }

View File

@@ -14,17 +14,49 @@ const COLORS = {
junction: '#78909c', junction: '#78909c',
} }
const PRODUCT_SPACING = 110
const COLUMN_X = {
product: -240,
junction: -120,
supplier: 0,
organization: 240,
municipality: 240,
department: 480,
}
export function toVisNodes (nodes) { export function toVisNodes (nodes) {
const products = nodes.filter(node => node.kind === 'product')
const productY = new Map()
products.forEach((node, index) => {
productY.set(node.entity.id, (index - (products.length - 1) / 2) * PRODUCT_SPACING)
})
return nodes.map(node => { return nodes.map(node => {
const color = COLORS[node.kind] || '#cfd8dc' const color = COLORS[node.kind] || '#cfd8dc'
const { image, ...rest } = node const { image, ...rest } = node
if (node.kind === 'junction') { if (node.kind === 'junction') {
return { ...rest, shape: 'dot', color, size: 10, label: '' } return {
...rest,
shape: 'dot',
color,
size: 10,
label: '',
x: COLUMN_X.junction,
y: productY.get(node.entity.id),
fixed: { x: true, y: true },
}
} }
if (image) { const base = image
return { ...rest, image, shape: 'circularImage', borderWidth: 2 } ? { ...rest, image, shape: 'circularImage', borderWidth: 2 }
: { ...rest, shape: 'dot', color, borderWidth: 2 }
if (node.kind === 'product') {
return {
...base,
x: COLUMN_X.product,
y: productY.get(node.entity.id),
fixed: { x: true, y: true },
}
} }
return { ...rest, shape: 'dot', color, borderWidth: 2 } return { ...base, x: COLUMN_X[node.kind], fixed: { x: true, y: false } }
}) })
} }

View File

@@ -139,4 +139,44 @@ describe('ProductSupplierOrganizationChart', () => {
expect(wrapper.findComponent(ProvenanceDetailModal).props('visible')).toBe(false) expect(wrapper.findComponent(ProvenanceDetailModal).props('visible')).toBe(false)
}) })
it('alinea los productos en una columna vertical fija y centrada', () => {
const provenance = [
{ product: { id: 1, name: 'Panela' }, suppliers: [{ supplier: { id: 5, name: 'A' } }] },
{ product: { id: 2, name: 'Arroz' }, suppliers: [{ supplier: { id: 6, name: 'B' } }] },
{ product: { id: 3, name: 'Queso' }, suppliers: [{ supplier: { id: 7, name: 'C' } }] },
]
const wrapper = mountChart({ provenance })
const products = visChart(wrapper).props('nodes').filter(node => node.kind === 'product')
expect(products).toHaveLength(3)
expect(new Set(products.map(node => node.x)).size).toBe(1)
expect(products.map(node => node.y)).toEqual([-110, 0, 110])
products.forEach(node => {
expect(node.fixed).toEqual({ x: true, y: true })
})
})
it('alinea proveedores y organizaciones en columnas por nivel', () => {
const wrapper = mountChart()
const supplier = visChart(wrapper).props('nodes').find(node => node.id === 'supplier:5')
expect(supplier.x).toBe(0)
expect(supplier.fixed).toEqual({ x: true, y: false })
expect(supplier.y).toBeUndefined()
const organization = visChart(wrapper).props('nodes').find(node => node.id === 'organization:3')
expect(organization.x).toBe(240)
expect(organization.fixed).toEqual({ x: true, y: false })
})
it('el punto de disyunción queda a la misma altura que su producto', () => {
const wrapper = mountChart({ provenance: provenanceWithTwoSuppliers })
const junction = visChart(wrapper).props('nodes').find(node => node.id === 'junction:1')
const product = visChart(wrapper).props('nodes').find(node => node.id === 'product:1')
expect(junction.x).toBe(-120)
expect(junction.y).toBe(product.y)
expect(junction.fixed).toEqual({ x: true, y: true })
})
}) })

View File

@@ -63,7 +63,7 @@ describe('ProductTerritoryChart', () => {
vi.clearAllMocks() vi.clearAllMocks()
}) })
it('renderiza el gráfico con municipio, departamento y país', () => { it('renderiza el gráfico con municipio y departamento, sin país', () => {
const wrapper = mountChart() const wrapper = mountChart()
const chart = visChart(wrapper) const chart = visChart(wrapper)
@@ -73,7 +73,7 @@ describe('ProductTerritoryChart', () => {
expect(nodeIds).toContain('supplier:5') expect(nodeIds).toContain('supplier:5')
expect(nodeIds).toContain('municipality:7') expect(nodeIds).toContain('municipality:7')
expect(nodeIds).toContain('department:2') expect(nodeIds).toContain('department:2')
expect(nodeIds).toContain('country:1') expect(nodeIds).not.toContain('country:1')
}) })
it('incluye la etiqueta del nodo y el color según el tipo', () => { it('incluye la etiqueta del nodo y el color según el tipo', () => {
@@ -96,7 +96,20 @@ describe('ProductTerritoryChart', () => {
expect(edges.find(edge => edge.from === 'junction:1' && edge.to === 'supplier:5').dashes).toBe(true) expect(edges.find(edge => edge.from === 'junction:1' && edge.to === 'supplier:5').dashes).toBe(true)
expect(edges.find(edge => edge.from === 'supplier:5' && edge.to === 'municipality:7').dashes).toBe(true) expect(edges.find(edge => edge.from === 'supplier:5' && edge.to === 'municipality:7').dashes).toBe(true)
expect(edges.find(edge => edge.from === 'municipality:7' && edge.to === 'department:2').dashes).toBe(true) expect(edges.find(edge => edge.from === 'municipality:7' && edge.to === 'department:2').dashes).toBe(true)
expect(edges.find(edge => edge.from === 'department:2' && edge.to === 'country:1').dashes).toBe(false) })
it('con varios productos del mismo proveedor solo traza una línea al municipio', () => {
const provenanceWithRepeatedSupplier = [
{ 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 wrapper = mountChart({ provenance: provenanceWithRepeatedSupplier })
const chart = visChart(wrapper)
const edges = chart.props('edges')
expect(edges.filter(edge => edge.from === 'supplier:5' && edge.to === 'municipality:7')).toHaveLength(1)
expect(chart.props('nodes').filter(node => node.id === 'municipality:7')).toHaveLength(1)
}) })
it('muestra mensaje de próximamente cuando ningún proveedor tiene territorio', () => { it('muestra mensaje de próximamente cuando ningún proveedor tiene territorio', () => {
@@ -121,4 +134,13 @@ describe('ProductTerritoryChart', () => {
expect(wrapper.findComponent(ProvenanceDetailModal).props('visible')).toBe(true) expect(wrapper.findComponent(ProvenanceDetailModal).props('visible')).toBe(true)
expect(document.body.textContent).toContain('La Mesa') expect(document.body.textContent).toContain('La Mesa')
}) })
it('alinea municipios y departamentos en columnas por nivel', () => {
const wrapper = mountChart()
const nodes = visChart(wrapper).props('nodes')
expect(nodes.find(node => node.id === 'supplier:5').x).toBe(0)
expect(nodes.find(node => node.id === 'municipality:7').x).toBe(240)
expect(nodes.find(node => node.id === 'department:2').x).toBe(480)
})
}) })

View File

@@ -87,6 +87,26 @@ describe('hasAnyTerritory', () => {
const noSuppliers = [{ product: { id: 1, name: 'Panela' }, suppliers: [] }] const noSuppliers = [{ product: { id: 1, name: 'Panela' }, suppliers: [] }]
expect(hasAnyTerritory(noSuppliers)).toBe(false) 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', () => { describe('buildSupplierOrganizationGraph', () => {
@@ -146,7 +166,7 @@ describe('buildSupplierOrganizationGraph', () => {
expect(graph.edges).toEqual([{ from: 'product:1', to: 'supplier:5', certain: true }]) expect(graph.edges).toEqual([{ from: 'product:1', to: 'supplier:5', certain: true }])
}) })
it('no duplica nodos que se repiten entre productos', () => { it('no duplica nodos ni aristas cuando se repiten entre productos', () => {
const data = [ const data = [
{ product: { id: 1, name: 'Panela' }, suppliers: [{ supplier: { id: 5, name: 'La Mesa' }, organization: { id: 3, name: 'Red' } }] }, { 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' } }] }, { product: { id: 2, name: 'Arroz' }, suppliers: [{ supplier: { id: 5, name: 'La Mesa' }, organization: { id: 3, name: 'Red' } }] },
@@ -155,7 +175,19 @@ describe('buildSupplierOrganizationGraph', () => {
expect(graph.nodes.filter(n => n.id === 'supplier:5')).toHaveLength(1) expect(graph.nodes.filter(n => n.id === 'supplier:5')).toHaveLength(1)
expect(graph.nodes.filter(n => n.id === 'organization:3')).toHaveLength(1) expect(graph.nodes.filter(n => n.id === 'organization:3')).toHaveLength(1)
expect(graph.edges).toHaveLength(4) expect(graph.edges.filter(e => e.from === 'supplier:5' && e.to === 'organization:3')).toHaveLength(1)
expect(graph.edges).toHaveLength(3)
})
it('si el mismo par de nodos repite con distinta certeza, gana la duda', () => {
const data = [
{ 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)
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', () => { it('no incluye posiciones de layout', () => {
@@ -181,7 +213,25 @@ describe('buildTerritoryGraph', () => {
expect(edgeOf(graph, 'product:1', 'supplier:5')).toEqual({ from: 'product:1', to: 'supplier:5', certain: true }) 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, '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 }) expect(edgeOf(graph, 'municipality:7', 'department:2')).toEqual({ from: 'municipality:7', to: 'department:2', certain: true })
expect(edgeOf(graph, 'department:2', 'country:1')).toEqual({ from: 'department:2', to: 'country:1', 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í', () => { it('cuando los proveedores comparten municipio, el territorio es cierto desde allí', () => {
@@ -194,7 +244,6 @@ describe('buildTerritoryGraph', () => {
expect(edgeOf(graph, 'supplier:5', 'municipality:7')).toEqual({ from: 'supplier:5', to: 'municipality:7', certain: true }) 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, 'supplier:6', 'municipality:7')).toEqual({ from: 'supplier:6', to: 'municipality:7', certain: true })
expect(edgeOf(graph, 'municipality:7', 'department:2').certain).toBe(true) expect(edgeOf(graph, 'municipality:7', 'department:2').certain).toBe(true)
expect(edgeOf(graph, 'department:2', 'country:1').certain).toBe(true)
}) })
it('municipios distintos pero mismo departamento: dudoso hasta el municipio y cierto desde el departamento', () => { it('municipios distintos pero mismo departamento: dudoso hasta el municipio y cierto desde el departamento', () => {
@@ -206,18 +255,15 @@ describe('buildTerritoryGraph', () => {
expect(edgeOf(graph, 'supplier:6', 'municipality:8').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: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 }) expect(edgeOf(graph, 'municipality:8', 'department:2')).toEqual({ from: 'municipality:8', to: 'department:2', certain: true })
expect(edgeOf(graph, 'department:2', 'country:1').certain).toBe(true)
}) })
it('municipios y departamentos distintos mantienen la duda, y el país común es cierto', () => { it('municipios y departamentos distintos mantienen la duda', () => {
const graph = buildTerritoryGraph(twoSuppliers()) const graph = buildTerritoryGraph(twoSuppliers())
expect(edgeOf(graph, 'supplier:5', 'municipality:7').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, 'municipality:7', 'department:2').certain).toBe(false)
expect(edgeOf(graph, 'supplier:6', 'municipality:8').certain).toBe(false) expect(edgeOf(graph, 'supplier:6', 'municipality:8').certain).toBe(false)
expect(edgeOf(graph, 'municipality:8', 'department:3').certain).toBe(false) expect(edgeOf(graph, 'municipality:8', 'department:3').certain).toBe(false)
expect(edgeOf(graph, 'department:2', 'country:1')).toEqual({ from: 'department:2', to: 'country:1', certain: true })
expect(edgeOf(graph, 'department:3', 'country:1')).toEqual({ from: 'department:3', to: 'country:1', certain: true })
}) })
it('no crea nodos de territorio cuando el proveedor no tiene municipio', () => { it('no crea nodos de territorio cuando el proveedor no tiene municipio', () => {