feat(process-repo): extender fromRow/toRow/getById con areaId y areaName

- fromRow: extrae areaId y areaName (vía JOIN en getById)
- toRow y updateEditable: incluyen area_id en escrituras a Supabase
- getById: JOIN con areas!area_id(name) para obtener nombre en una query
- Fixtures de tests actualizados con areaId: null (campo requerido)
- 4 tests nuevos en tests/persistence/area-repo.test.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 18:18:14 -03:00
parent f9e886e12d
commit d6e7226842
16 changed files with 118 additions and 17 deletions

View File

@@ -92,6 +92,7 @@ export function ImportPage() {
ownerId: user!.id, ownerId: user!.id,
updatedBy: user!.id, updatedBy: user!.id,
orgId: null, orgId: null,
areaId: null,
} }
const activityElements = extractActivityElements(graph) const activityElements = extractActivityElements(graph)

View File

@@ -13,6 +13,7 @@ function toRow(process: Process, userId: string) {
analysis_horizon_years: process.analysisHorizonYears, analysis_horizon_years: process.analysisHorizonYears,
automation_investment: process.automationInvestment, automation_investment: process.automationInvestment,
group_id: process.groupId, group_id: process.groupId,
area_id: process.areaId,
tags: process.tags, tags: process.tags,
// owner_id se preserva del valor existente en el proceso; solo se inicializa con userId en creates // owner_id se preserva del valor existente en el proceso; solo se inicializa con userId en creates
owner_id: process.ownerId || userId, owner_id: process.ownerId || userId,
@@ -25,6 +26,8 @@ function toRow(process: Process, userId: string) {
function fromRow(row: Record<string, unknown>): Process { function fromRow(row: Record<string, unknown>): Process {
// org puede estar presente si el query hizo JOIN con organizations (getById lo incluye) // org puede estar presente si el query hizo JOIN con organizations (getById lo incluye)
const org = row.org as { name: string } | null const org = row.org as { name: string } | null
// area puede estar presente si el query hizo JOIN con areas (getById lo incluye)
const area = row.area as { name: string } | null
return { return {
id: row.id as string, id: row.id as string,
name: row.name as string, name: row.name as string,
@@ -43,6 +46,8 @@ function fromRow(row: Record<string, unknown>): Process {
updatedBy: (row.updated_by as string) ?? '', updatedBy: (row.updated_by as string) ?? '',
orgId: (row.org_id as string | null) ?? null, orgId: (row.org_id as string | null) ?? null,
orgName: org?.name ?? null, orgName: org?.name ?? null,
areaId: (row.area_id as string | null) ?? null,
areaName: area?.name ?? null,
} }
} }
@@ -91,6 +96,7 @@ export const supabaseProcessRepo = {
automation_investment: process.automationInvestment, automation_investment: process.automationInvestment,
tags: process.tags, tags: process.tags,
group_id: process.groupId, group_id: process.groupId,
area_id: process.areaId,
updated_by: userId, updated_by: userId,
updated_at: new Date().toISOString(), updated_at: new Date().toISOString(),
}) })
@@ -102,7 +108,7 @@ export const supabaseProcessRepo = {
// JOIN con organizations para obtener orgName en una sola query (evita fetch separado y race conditions) // JOIN con organizations para obtener orgName en una sola query (evita fetch separado y race conditions)
const { data, error } = await supabase const { data, error } = await supabase
.from('processes') .from('processes')
.select('*, org:organizations!org_id(name)') .select('*, org:organizations!org_id(name), area:areas!area_id(name)')
.eq('id', id) .eq('id', id)
.single() .single()
if (error) return undefined if (error) return undefined

View File

@@ -229,7 +229,7 @@ describe('MethodologyFooter', () => {
id: 'p1', name: 'Proceso Test', clientName: '', id: 'p1', name: 'Proceso Test', clientName: '',
bpmnXml: '<def/>', currency: 'USD', bpmnXml: '<def/>', currency: 'USD',
overheadPercentage: 0.2, annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0, overheadPercentage: 0.2, annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0,
createdAt: 0, updatedAt: 0, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, createdAt: 0, updatedAt: 0, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, areaId: null,
} }
expect(() => render(<MethodologyFooter process={proc} />)).not.toThrow() expect(() => render(<MethodologyFooter process={proc} />)).not.toThrow()
}) })
@@ -239,7 +239,7 @@ describe('MethodologyFooter', () => {
id: 'p1', name: 'Proc', clientName: '', id: 'p1', name: 'Proc', clientName: '',
bpmnXml: '', currency: 'USD', bpmnXml: '', currency: 'USD',
overheadPercentage: 0.25, annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0, overheadPercentage: 0.25, annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0,
createdAt: 0, updatedAt: 0, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, createdAt: 0, updatedAt: 0, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, areaId: null,
} }
render(<MethodologyFooter process={proc} />) render(<MethodologyFooter process={proc} />)
const text = document.body.textContent ?? '' const text = document.body.textContent ?? ''
@@ -251,7 +251,7 @@ describe('MethodologyFooter', () => {
id: 'p1', name: 'Proc', clientName: '', id: 'p1', name: 'Proc', clientName: '',
bpmnXml: '', currency: 'USD', bpmnXml: '', currency: 'USD',
overheadPercentage: 0.2, annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0, overheadPercentage: 0.2, annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0,
createdAt: 0, updatedAt: 0, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, createdAt: 0, updatedAt: 0, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, areaId: null,
} }
render(<MethodologyFooter process={proc} />) render(<MethodologyFooter process={proc} />)
const text = document.body.textContent ?? '' const text = document.body.textContent ?? ''
@@ -342,7 +342,7 @@ describe('ReportPage — con datos completos', () => {
updatedAt: Date.now() - 1000 * 60 * 30, updatedAt: Date.now() - 1000 * 60 * 30,
groupId: null, groupId: null,
tags: [], tags: [],
ownerId: 'test-user', updatedBy: 'test-user', orgId: null, ownerId: 'test-user', updatedBy: 'test-user', orgId: null, areaId: null,
} }
const mockSimulation = { const mockSimulation = {

View File

@@ -600,7 +600,7 @@ const mockProcessBase = {
currency: 'USD', overheadPercentage: 0.2, currency: 'USD', overheadPercentage: 0.2,
annualFrequency: 1200, analysisHorizonYears: 3, automationInvestment: 50_000, annualFrequency: 1200, analysisHorizonYears: 3, automationInvestment: 50_000,
createdAt: Date.now() - 3600_000, updatedAt: Date.now() - 3600_000, createdAt: Date.now() - 3600_000, updatedAt: Date.now() - 3600_000,
groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, areaId: null,
} }
const mockSimulationBase = { const mockSimulationBase = {

View File

@@ -14,7 +14,7 @@ function makeProcess(extras: Partial<Process> = {}): Process {
id: 'p1', name: 'Proceso de Crédito', clientName: 'Banco XYZ', id: 'p1', name: 'Proceso de Crédito', clientName: 'Banco XYZ',
bpmnXml: '', currency: 'USD', overheadPercentage: 0.2, bpmnXml: '', currency: 'USD', overheadPercentage: 0.2,
annualFrequency: 1200, analysisHorizonYears: 3, automationInvestment: 50_000, annualFrequency: 1200, analysisHorizonYears: 3, automationInvestment: 50_000,
createdAt: 0, updatedAt: 0, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, ...extras, createdAt: 0, updatedAt: 0, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, areaId: null, ...extras,
} }
} }

View File

@@ -9,7 +9,7 @@ function makeProcess(currency: string, extras: Partial<Process> = {}): Process {
id: 'p1', name: 'Proceso de Ventas', clientName: 'Empresa ABC', id: 'p1', name: 'Proceso de Ventas', clientName: 'Empresa ABC',
bpmnXml: '', currency, overheadPercentage: 0.2, bpmnXml: '', currency, overheadPercentage: 0.2,
annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0, annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0,
createdAt: 0, updatedAt: 0, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, ...extras, createdAt: 0, updatedAt: 0, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, areaId: null, ...extras,
} }
} }

View File

@@ -66,7 +66,7 @@ describe('Medición de tamaños de CSV — 3 sample BPMNs', () => {
bpmnXml: xml, currency: 'USD', overheadPercentage: 0.2, bpmnXml: xml, currency: 'USD', overheadPercentage: 0.2,
annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0, annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0,
createdAt: 0, updatedAt: 0, createdAt: 0, updatedAt: 0,
groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, areaId: null,
} }
const simulation: Simulation = { const simulation: Simulation = {

View File

@@ -51,7 +51,7 @@ const mockProcess: Process = {
bpmnXml: '', currency: 'USD', overheadPercentage: 0.2, bpmnXml: '', currency: 'USD', overheadPercentage: 0.2,
annualFrequency: 1200, analysisHorizonYears: 3, automationInvestment: 50_000, annualFrequency: 1200, analysisHorizonYears: 3, automationInvestment: 50_000,
createdAt: 0, updatedAt: 0, createdAt: 0, updatedAt: 0,
groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, areaId: null,
} }
const perActivityActual = [ const perActivityActual = [

View File

@@ -43,7 +43,7 @@ const mockProcess: Process = {
bpmnXml: '', currency: 'USD', overheadPercentage: 0.2, bpmnXml: '', currency: 'USD', overheadPercentage: 0.2,
annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0, annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0,
createdAt: 0, updatedAt: 0, createdAt: 0, updatedAt: 0,
groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, areaId: null,
} }
// Simulación SIN recursos en ninguna actividad // Simulación SIN recursos en ninguna actividad

View File

@@ -54,7 +54,7 @@ const mockProcess: Process = {
overheadPercentage: 0.2, overheadPercentage: 0.2,
annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0, annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0,
createdAt: 0, updatedAt: 0, createdAt: 0, updatedAt: 0,
groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, areaId: null,
} }
const mockSimulation: Simulation = { const mockSimulation: Simulation = {

View File

@@ -59,7 +59,7 @@ const mockProcess: Process = {
overheadPercentage: 0.2, overheadPercentage: 0.2,
annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 50000, annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 50000,
createdAt: 0, updatedAt: 0, createdAt: 0, updatedAt: 0,
groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, areaId: null,
} }
const mockSimulation: Simulation = { const mockSimulation: Simulation = {

View File

@@ -56,7 +56,7 @@ const mockProcess: Process = {
overheadPercentage: 0.2, overheadPercentage: 0.2,
annualFrequency: 5000, analysisHorizonYears: 3, automationInvestment: 80000, annualFrequency: 5000, analysisHorizonYears: 3, automationInvestment: 80000,
createdAt: 0, updatedAt: 0, createdAt: 0, updatedAt: 0,
groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, areaId: null,
} }
const mockSimulation: Simulation = { const mockSimulation: Simulation = {

View File

@@ -52,7 +52,7 @@ const mockProcess: Process = {
bpmnXml: '', currency: 'USD', overheadPercentage: 0.2, bpmnXml: '', currency: 'USD', overheadPercentage: 0.2,
annualFrequency: 5000, analysisHorizonYears: 3, automationInvestment: 80000, annualFrequency: 5000, analysisHorizonYears: 3, automationInvestment: 80000,
createdAt: 0, updatedAt: 0, createdAt: 0, updatedAt: 0,
groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, areaId: null,
} }
const mockRoi: RoiResult = { const mockRoi: RoiResult = {

View File

@@ -52,7 +52,7 @@ const mockProcess: Process = {
bpmnXml: '', currency: 'USD', overheadPercentage: 0.2, bpmnXml: '', currency: 'USD', overheadPercentage: 0.2,
annualFrequency: 5000, analysisHorizonYears: 3, automationInvestment: 80000, annualFrequency: 5000, analysisHorizonYears: 3, automationInvestment: 80000,
createdAt: 0, updatedAt: 0, createdAt: 0, updatedAt: 0,
groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, areaId: null,
} }
const mockSimulation: Simulation = { const mockSimulation: Simulation = {

View File

@@ -0,0 +1,94 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { supabaseAreaRepo } from '@/persistence/supabase/area-repo'
const { mockSingle, mockData } = vi.hoisted(() => ({
mockSingle: vi.fn(),
mockData: vi.fn(),
}))
vi.mock('@/lib/supabase', () => {
const buildChain = () => {
const chain: Record<string, unknown> = {}
chain.select = vi.fn().mockReturnValue(chain)
chain.insert = vi.fn().mockReturnValue(chain)
chain.update = vi.fn().mockReturnValue(chain)
chain.delete = vi.fn().mockReturnValue(chain)
chain.eq = vi.fn().mockReturnValue(chain)
chain.order = vi.fn().mockImplementation(() => mockData())
chain.single = mockSingle
return chain
}
return {
supabase: {
from: vi.fn().mockImplementation(() => buildChain()),
},
}
})
const AREA_ROW = {
id: 'area-uuid-1',
org_id: 'org-uuid-1',
name: 'Finanzas',
description: 'Área de finanzas',
created_at: '2026-07-07T00:00:00.000Z',
updated_at: '2026-07-07T00:00:00.000Z',
}
describe('supabaseAreaRepo', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('getByOrg', () => {
it('devuelve lista de áreas mapeadas correctamente', async () => {
mockData.mockResolvedValue({ data: [AREA_ROW], error: null })
const result = await supabaseAreaRepo.getByOrg('org-uuid-1')
expect(result).toHaveLength(1)
expect(result[0]).toMatchObject({
id: 'area-uuid-1',
orgId: 'org-uuid-1',
name: 'Finanzas',
description: 'Área de finanzas',
createdAt: new Date('2026-07-07T00:00:00.000Z').getTime(),
})
})
it('devuelve array vacío si la org no tiene áreas', async () => {
mockData.mockResolvedValue({ data: [], error: null })
const result = await supabaseAreaRepo.getByOrg('org-sin-areas')
expect(result).toEqual([])
})
})
describe('create', () => {
it('inserta el área y retorna el registro mapeado', async () => {
mockSingle.mockResolvedValue({ data: AREA_ROW, error: null })
const result = await supabaseAreaRepo.create({
orgId: 'org-uuid-1',
name: 'Finanzas',
description: 'Área de finanzas',
})
expect(result).toMatchObject({
id: 'area-uuid-1',
orgId: 'org-uuid-1',
name: 'Finanzas',
description: 'Área de finanzas',
})
})
it('propaga el error de Supabase si el insert falla', async () => {
mockSingle.mockResolvedValue({ data: null, error: new Error('RLS violation') })
await expect(
supabaseAreaRepo.create({ orgId: 'org-1', name: 'Test' })
).rejects.toThrow('RLS violation')
})
})
})

View File

@@ -94,7 +94,7 @@ describe('simulation store — invalidación por cambio en process store', () =>
currency: 'USD', overheadPercentage: 0.2, currency: 'USD', overheadPercentage: 0.2,
annualFrequency: 500, analysisHorizonYears: 2, automationInvestment: 10_000, annualFrequency: 500, analysisHorizonYears: 2, automationInvestment: 10_000,
createdAt: Date.now(), updatedAt: Date.now(), createdAt: Date.now(), updatedAt: Date.now(),
groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, areaId: null,
} }
useProcessStore.setState({ currentProcess: proc }) useProcessStore.setState({ currentProcess: proc })