Sprint 2 Etapas 1-2: modelo recursos utilizationMinutes+units, Dexie v3, ActivityPanel wow
This commit is contained in:
@@ -79,7 +79,7 @@ describe('runSimulation', () => {
|
||||
name: string,
|
||||
directCost: number,
|
||||
minutes: number,
|
||||
resources: Array<{ resourceId: string; utilizationPercent: number }> = []
|
||||
resources: Array<{ resourceId: string; utilizationMinutes: number; units?: number }> = []
|
||||
): Activity => ({
|
||||
id: `act-${bpmnElementId}`,
|
||||
processId: 'proc1',
|
||||
@@ -88,7 +88,7 @@ describe('runSimulation', () => {
|
||||
type: 'task',
|
||||
directCostFixed: directCost,
|
||||
executionTimeMinutes: minutes,
|
||||
assignedResources: resources,
|
||||
assignedResources: resources.map((r) => ({ ...r, units: r.units ?? 1 })),
|
||||
automatable: false,
|
||||
automatedCostFixed: 0,
|
||||
automatedTimeMinutes: 0,
|
||||
@@ -136,7 +136,7 @@ describe('runSimulation', () => {
|
||||
it('incluye costo de recursos asignados', () => {
|
||||
const resource = makeResource('res1', 'Analista', 60) // $60/hora
|
||||
const activityA = makeActivity('taskA', 'Tarea A', 0, 60, [
|
||||
{ resourceId: 'res1', utilizationPercent: 1.0 }, // 1 hora completa
|
||||
{ resourceId: 'res1', utilizationMinutes: 60, units: 1 }, // 1 hora completa
|
||||
])
|
||||
|
||||
const input: SimulationInput = {
|
||||
@@ -390,3 +390,108 @@ describe('runSimulation — scenario automated', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// ─── Sprint 2: modelo utilizationMinutes + units ──────────────────────────────
|
||||
|
||||
describe('runSimulation — recursos con modelo utilizationMinutes + units', () => {
|
||||
const makeRes = (id: string, costPerHour: number): Resource => ({
|
||||
id, processId: 'proc1', name: `Recurso ${id}`, type: 'role', costPerHour,
|
||||
})
|
||||
|
||||
const makeAct = (
|
||||
bpmnElementId: string,
|
||||
directCost: number,
|
||||
minutes: number,
|
||||
resources: import('@/domain/types').ActivityResourceAssignment[] = []
|
||||
): Activity => ({
|
||||
id: `act-${bpmnElementId}`, processId: 'proc1', bpmnElementId,
|
||||
name: bpmnElementId, type: 'task',
|
||||
directCostFixed: directCost, executionTimeMinutes: minutes,
|
||||
assignedResources: resources,
|
||||
automatable: false, automatedCostFixed: 0, automatedTimeMinutes: 0,
|
||||
})
|
||||
|
||||
it('actividad sin recursos: costo = directCostFixed', () => {
|
||||
const act = makeAct('t1', 200, 30)
|
||||
const result = runSimulation({
|
||||
processXml: LINEAR_BPMN,
|
||||
activities: new Map([['taskA', { ...act, bpmnElementId: 'taskA' }]]),
|
||||
gateways: new Map(), resources: new Map(),
|
||||
globalSettings: { currency: 'USD', overheadPercentage: 0 },
|
||||
})
|
||||
expect(result.totalDirectCost).toBe(200)
|
||||
})
|
||||
|
||||
it('actividad con 1 recurso: costo = fijo + costPerHour × utilizationMinutes/60 × units × execProb', () => {
|
||||
// Analista: $120/h, 30 min, 1 unidad → $60. Fijo: $50. Total: $110
|
||||
const act = makeAct('taskA', 50, 60, [{ resourceId: 'r1', utilizationMinutes: 30, units: 1 }])
|
||||
const result = runSimulation({
|
||||
processXml: LINEAR_BPMN,
|
||||
activities: new Map([['taskA', act]]),
|
||||
gateways: new Map(),
|
||||
resources: new Map([['r1', makeRes('r1', 120)]]),
|
||||
globalSettings: { currency: 'USD', overheadPercentage: 0 },
|
||||
})
|
||||
const actResult = result.perActivity.find((a) => a.bpmnElementId === 'taskA')
|
||||
expect(actResult?.expectedDirectCost).toBeCloseTo(110)
|
||||
})
|
||||
|
||||
it('actividad con 2 recursos: costo = fijo + suma de costos de recursos', () => {
|
||||
// r1: $60/h × 30min/60 × 1 = $30. r2: $40/h × 60min/60 × 1 = $40. Fijo: $20. Total: $90
|
||||
const act = makeAct('taskA', 20, 60, [
|
||||
{ resourceId: 'r1', utilizationMinutes: 30, units: 1 },
|
||||
{ resourceId: 'r2', utilizationMinutes: 60, units: 1 },
|
||||
])
|
||||
const result = runSimulation({
|
||||
processXml: LINEAR_BPMN,
|
||||
activities: new Map([['taskA', act]]),
|
||||
gateways: new Map(),
|
||||
resources: new Map([['r1', makeRes('r1', 60)], ['r2', makeRes('r2', 40)]]),
|
||||
globalSettings: { currency: 'USD', overheadPercentage: 0 },
|
||||
})
|
||||
const actResult = result.perActivity.find((a) => a.bpmnElementId === 'taskA')
|
||||
expect(actResult?.expectedDirectCost).toBeCloseTo(90)
|
||||
})
|
||||
|
||||
it('units = 2: costo de recurso se duplica', () => {
|
||||
// $60/h × 30min/60 × 2 unidades = $60. Fijo: $0. Total: $60
|
||||
const act = makeAct('taskA', 0, 30, [{ resourceId: 'r1', utilizationMinutes: 30, units: 2 }])
|
||||
const result = runSimulation({
|
||||
processXml: LINEAR_BPMN,
|
||||
activities: new Map([['taskA', act]]),
|
||||
gateways: new Map(),
|
||||
resources: new Map([['r1', makeRes('r1', 60)]]),
|
||||
globalSettings: { currency: 'USD', overheadPercentage: 0 },
|
||||
})
|
||||
expect(result.totalDirectCost).toBeCloseTo(60)
|
||||
})
|
||||
|
||||
it('utilizationMinutes = 0: el recurso no agrega costo', () => {
|
||||
const act = makeAct('taskA', 100, 60, [{ resourceId: 'r1', utilizationMinutes: 0, units: 1 }])
|
||||
const result = runSimulation({
|
||||
processXml: LINEAR_BPMN,
|
||||
activities: new Map([['taskA', act]]),
|
||||
gateways: new Map(),
|
||||
resources: new Map([['r1', makeRes('r1', 200)]]),
|
||||
globalSettings: { currency: 'USD', overheadPercentage: 0 },
|
||||
})
|
||||
expect(result.totalDirectCost).toBe(100)
|
||||
})
|
||||
|
||||
it('resource no encontrado en el mapa: se ignora sin lanzar error', () => {
|
||||
const act = makeAct('taskA', 100, 60, [{ resourceId: 'no-existe', utilizationMinutes: 30, units: 1 }])
|
||||
expect(() => runSimulation({
|
||||
processXml: LINEAR_BPMN,
|
||||
activities: new Map([['taskA', act]]),
|
||||
gateways: new Map(), resources: new Map(),
|
||||
globalSettings: { currency: 'USD', overheadPercentage: 0 },
|
||||
})).not.toThrow()
|
||||
const result = runSimulation({
|
||||
processXml: LINEAR_BPMN,
|
||||
activities: new Map([['taskA', act]]),
|
||||
gateways: new Map(), resources: new Map(),
|
||||
globalSettings: { currency: 'USD', overheadPercentage: 0 },
|
||||
})
|
||||
expect(result.totalDirectCost).toBe(100)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user