POST /work-logs/recompute/:orderId (ADMIN-only, idempotent) repairs ghost actualLabor after SQL-direct deletion of VERIFIED WorkLogs. Convention documented in CLAUDE.md. 3 test-gardes (T-TRUD3-19/20/18a). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
497 lines
20 KiB
TypeScript
497 lines
20 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest';
|
||
import { WorkLogService } from '../../src/modules/work-log/work-log.service.js';
|
||
import { WorkerService } from '../../src/modules/work-log/worker.service.js';
|
||
|
||
function makeOperation(overrides: Record<string, any> = {}) {
|
||
return {
|
||
id: 1,
|
||
orderId: 1,
|
||
payType: 'PIECE',
|
||
baremeUnitPrice: 17.85,
|
||
baremeTotalPrice: 553.35,
|
||
quantity: 31,
|
||
unit: 'шт',
|
||
description: 'Распил прямой рез',
|
||
parentOperationId: null,
|
||
order: { id: 1, quantity: 10, status: 'IN_PRODUCTION' },
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
function makeWorker(overrides: Record<string, any> = {}) {
|
||
return { id: 1, name: 'Ivan Petrov', nameRu: 'Петров Иван', category: 'SBORSHIK', active: true, phone: null, ...overrides };
|
||
}
|
||
|
||
function mockPrisma(overrides: Record<string, any> = {}) {
|
||
return {
|
||
orderLaborOperation: {
|
||
findUnique: vi.fn().mockResolvedValue(makeOperation()),
|
||
findMany: vi.fn().mockResolvedValue([]),
|
||
},
|
||
worker: {
|
||
findUnique: vi.fn().mockResolvedValue(makeWorker()),
|
||
findMany: vi.fn().mockResolvedValue([makeWorker()]),
|
||
create: vi.fn().mockImplementation(({ data }) => ({ id: 1, ...data, createdAt: new Date(), updatedAt: new Date() })),
|
||
update: vi.fn().mockImplementation(({ data }) => ({ ...makeWorker(), ...data })),
|
||
},
|
||
workLog: {
|
||
create: vi.fn().mockImplementation(({ data }) => ({
|
||
id: 1, ...data, worker: makeWorker(), operation: { id: 1, description: 'Распил', payType: 'PIECE', unit: 'шт' },
|
||
createdAt: new Date(), updatedAt: new Date(),
|
||
})),
|
||
findUnique: vi.fn().mockResolvedValue(null),
|
||
findMany: vi.fn().mockResolvedValue([]),
|
||
update: vi.fn().mockImplementation(({ data }) => ({
|
||
id: 1, orderId: 1, operationId: 1, workerId: 1, quantityDone: 5, costAmount: 89.25,
|
||
status: data.status, verifiedById: data.verifiedById, verifiedAt: data.verifiedAt,
|
||
rejectionReason: data.rejectionReason ?? null,
|
||
worker: makeWorker(), operation: { id: 1, description: 'Распил', payType: 'PIECE', unit: 'шт' },
|
||
createdAt: new Date(), updatedAt: new Date(),
|
||
})),
|
||
aggregate: vi.fn().mockResolvedValue({ _sum: { quantityDone: null, costAmount: null } }),
|
||
},
|
||
financialTracking: {
|
||
updateMany: vi.fn().mockResolvedValue({ count: 1 }),
|
||
},
|
||
order: {
|
||
findUnique: vi.fn().mockResolvedValue({ id: 1, quantity: 10 }),
|
||
},
|
||
...overrides,
|
||
} as any;
|
||
}
|
||
|
||
describe('WorkLogService', () => {
|
||
describe('create', () => {
|
||
it('T-TRUD3-01: creates a PIECE work log with correct cost', async () => {
|
||
const prisma = mockPrisma();
|
||
const service = new WorkLogService(prisma);
|
||
const result = await service.create({
|
||
operationId: 1, workerId: 1, workDate: '2026-06-26', quantityDone: 5,
|
||
}, 'user-id');
|
||
|
||
expect(result.workLog.costAmount).toBe(89.25); // 5 × 17.85
|
||
expect(result.workLog.quantityDone).toBe(5);
|
||
expect(result.ceilingCheck).not.toBeNull();
|
||
expect(result.ceilingCheck!.ceiling).toBe(310); // 31 × 10
|
||
expect(result.ceilingCheck!.exceeded).toBe(false);
|
||
expect(result.hourlyBudgetCheck).toBeNull();
|
||
});
|
||
|
||
it('T-TRUD3-02: creates an HOURLY work log with budget check', async () => {
|
||
const prisma = mockPrisma({
|
||
orderLaborOperation: {
|
||
findUnique: vi.fn().mockResolvedValue(makeOperation({
|
||
payType: 'HOURLY', baremeUnitPrice: 375, baremeTotalPrice: 750, quantity: 120, unit: 'мин',
|
||
})),
|
||
},
|
||
});
|
||
const service = new WorkLogService(prisma);
|
||
const result = await service.create({
|
||
operationId: 1, workerId: 1, workDate: '2026-06-26', quantityDone: 2,
|
||
}, 'user-id');
|
||
|
||
expect(result.workLog.costAmount).toBe(750); // 2 hours × 375 ₽/h
|
||
expect(result.ceilingCheck).toBeNull(); // no ceiling for HOURLY
|
||
expect(result.hourlyBudgetCheck).not.toBeNull();
|
||
expect(result.hourlyBudgetCheck!.budget).toBe(7500); // 750 × 10
|
||
expect(result.hourlyBudgetCheck!.spent).toBe(750);
|
||
expect(result.hourlyBudgetCheck!.exceeded).toBe(false);
|
||
});
|
||
|
||
it('T-TRUD3-03: creates an AREA work log (peinture)', async () => {
|
||
const prisma = mockPrisma({
|
||
orderLaborOperation: {
|
||
findUnique: vi.fn().mockResolvedValue(makeOperation({
|
||
payType: 'AREA', baremeUnitPrice: 65, baremeTotalPrice: 1300, quantity: 20, unit: 'м²',
|
||
})),
|
||
},
|
||
});
|
||
const service = new WorkLogService(prisma);
|
||
const result = await service.create({
|
||
operationId: 1, workerId: 1, workDate: '2026-06-26', quantityDone: 8,
|
||
}, 'user-id');
|
||
|
||
expect(result.workLog.costAmount).toBe(520); // 8 × 65
|
||
expect(result.ceilingCheck!.ceiling).toBe(200); // 20 × 10
|
||
expect(result.ceilingCheck!.exceeded).toBe(false);
|
||
});
|
||
|
||
it('T-TRUD3-04: ceiling exceeded without justification → rejects (BLOCKING)', async () => {
|
||
const prisma = mockPrisma({
|
||
workLog: {
|
||
...mockPrisma().workLog,
|
||
aggregate: vi.fn().mockResolvedValue({ _sum: { quantityDone: 300, costAmount: 5355 } }),
|
||
},
|
||
});
|
||
const service = new WorkLogService(prisma);
|
||
const err: any = await service.create({
|
||
operationId: 1, workerId: 1, workDate: '2026-06-26', quantityDone: 15,
|
||
}, 'user-id').catch(e => e);
|
||
|
||
expect(err.statusCode).toBe(400);
|
||
expect(err.code).toBe('CEILING_EXCEEDED');
|
||
expect(err.ceilingCheck.exceeded).toBe(true);
|
||
expect(err.ceilingCheck.ceiling).toBe(310);
|
||
expect(err.ceilingCheck.cumulAfter).toBe(315);
|
||
expect(err.ceilingCheck.overBy).toBe(5);
|
||
expect(prisma.workLog.create).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('T-TRUD3-04b: ceiling exceeded WITH justification → creates', async () => {
|
||
const prisma = mockPrisma({
|
||
workLog: {
|
||
...mockPrisma().workLog,
|
||
aggregate: vi.fn().mockResolvedValue({ _sum: { quantityDone: 300, costAmount: 5355 } }),
|
||
},
|
||
});
|
||
const service = new WorkLogService(prisma);
|
||
const result = await service.create({
|
||
operationId: 1, workerId: 1, workDate: '2026-06-26', quantityDone: 15,
|
||
excessJustification: 'Доработка по рекламации клиента',
|
||
}, 'user-id');
|
||
|
||
expect(result.ceilingCheck!.exceeded).toBe(true);
|
||
expect(result.ceilingCheck!.overBy).toBe(5);
|
||
expect(prisma.workLog.create).toHaveBeenCalled();
|
||
const createCall = prisma.workLog.create.mock.calls[0][0];
|
||
expect(createCall.data.excessJustification).toBe('Доработка по рекламации клиента');
|
||
});
|
||
|
||
it('T-TRUD3-05: hourly budget exceeded without justification → rejects (BLOCKING)', async () => {
|
||
const prisma = mockPrisma({
|
||
orderLaborOperation: {
|
||
findUnique: vi.fn().mockResolvedValue(makeOperation({
|
||
payType: 'HOURLY', baremeUnitPrice: 375, baremeTotalPrice: 750, quantity: 120, unit: 'мин',
|
||
})),
|
||
},
|
||
workLog: {
|
||
...mockPrisma().workLog,
|
||
aggregate: vi.fn().mockResolvedValue({ _sum: { quantityDone: 18, costAmount: 6750 } }),
|
||
},
|
||
});
|
||
const service = new WorkLogService(prisma);
|
||
const err: any = await service.create({
|
||
operationId: 1, workerId: 1, workDate: '2026-06-26', quantityDone: 4,
|
||
}, 'user-id').catch(e => e);
|
||
|
||
expect(err.statusCode).toBe(400);
|
||
expect(err.code).toBe('BUDGET_EXCEEDED');
|
||
expect(err.hourlyBudgetCheck.exceeded).toBe(true);
|
||
expect(err.hourlyBudgetCheck.budget).toBe(7500);
|
||
expect(err.hourlyBudgetCheck.spent).toBe(8250);
|
||
expect(prisma.workLog.create).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('T-TRUD3-05b: hourly budget exceeded WITH justification → creates', async () => {
|
||
const prisma = mockPrisma({
|
||
orderLaborOperation: {
|
||
findUnique: vi.fn().mockResolvedValue(makeOperation({
|
||
payType: 'HOURLY', baremeUnitPrice: 375, baremeTotalPrice: 750, quantity: 120, unit: 'мин',
|
||
})),
|
||
},
|
||
workLog: {
|
||
...mockPrisma().workLog,
|
||
aggregate: vi.fn().mockResolvedValue({ _sum: { quantityDone: 18, costAmount: 6750 } }),
|
||
},
|
||
});
|
||
const service = new WorkLogService(prisma);
|
||
const result = await service.create({
|
||
operationId: 1, workerId: 1, workDate: '2026-06-26', quantityDone: 4,
|
||
excessJustification: 'Доработка сложной конструкции',
|
||
}, 'user-id');
|
||
|
||
expect(result.hourlyBudgetCheck!.exceeded).toBe(true);
|
||
expect(prisma.workLog.create).toHaveBeenCalled();
|
||
const createCall = prisma.workLog.create.mock.calls[0][0];
|
||
expect(createCall.data.excessJustification).toBe('Доработка сложной конструкции');
|
||
});
|
||
|
||
it('T-TRUD3-05c: HOURLY costAmount = hours × hourly rate', async () => {
|
||
const prisma = mockPrisma({
|
||
orderLaborOperation: {
|
||
findUnique: vi.fn().mockResolvedValue(makeOperation({
|
||
payType: 'HOURLY', baremeUnitPrice: 375, baremeTotalPrice: 750, quantity: 120, unit: 'ч',
|
||
})),
|
||
},
|
||
});
|
||
const service = new WorkLogService(prisma);
|
||
const result = await service.create({
|
||
operationId: 1, workerId: 1, workDate: '2026-06-26', quantityDone: 2.5,
|
||
}, 'user-id');
|
||
|
||
expect(result.workLog.costAmount).toBe(937.5); // 2.5h × 375 ₽/h
|
||
});
|
||
|
||
it('T-TRUD3-05d: no justification stored when ceiling NOT exceeded', async () => {
|
||
const prisma = mockPrisma();
|
||
const service = new WorkLogService(prisma);
|
||
await service.create({
|
||
operationId: 1, workerId: 1, workDate: '2026-06-26', quantityDone: 5,
|
||
excessJustification: 'This should be ignored',
|
||
}, 'user-id');
|
||
|
||
const createCall = prisma.workLog.create.mock.calls[0][0];
|
||
expect(createCall.data.excessJustification).toBeNull();
|
||
});
|
||
|
||
it('T-TRUD3-06: rejects if order not in production', async () => {
|
||
const prisma = mockPrisma({
|
||
orderLaborOperation: {
|
||
findUnique: vi.fn().mockResolvedValue(makeOperation({
|
||
order: { id: 1, quantity: 10, status: 'DRAFT' },
|
||
})),
|
||
},
|
||
});
|
||
const service = new WorkLogService(prisma);
|
||
await expect(service.create({
|
||
operationId: 1, workerId: 1, workDate: '2026-06-26', quantityDone: 5,
|
||
}, 'user-id')).rejects.toThrow('Order must be in production');
|
||
});
|
||
|
||
it('T-TRUD3-07: rejects if worker inactive', async () => {
|
||
const prisma = mockPrisma({
|
||
worker: { findUnique: vi.fn().mockResolvedValue(makeWorker({ active: false })) },
|
||
});
|
||
const service = new WorkLogService(prisma);
|
||
await expect(service.create({
|
||
operationId: 1, workerId: 1, workDate: '2026-06-26', quantityDone: 5,
|
||
}, 'user-id')).rejects.toThrow('Worker not found or inactive');
|
||
});
|
||
|
||
it('T-TRUD3-08: rejects work on parent operation (must use children)', async () => {
|
||
const prisma = mockPrisma({
|
||
orderLaborOperation: {
|
||
findUnique: vi.fn().mockResolvedValue(makeOperation({ parentOperationId: 99 })),
|
||
},
|
||
});
|
||
const service = new WorkLogService(prisma);
|
||
await expect(service.create({
|
||
operationId: 1, workerId: 1, workDate: '2026-06-26', quantityDone: 5,
|
||
}, 'user-id')).rejects.toThrow('split children');
|
||
});
|
||
});
|
||
|
||
describe('verify', () => {
|
||
it('T-TRUD3-09: verifies PENDING log and recomputes actualLabor', async () => {
|
||
const prisma = mockPrisma({
|
||
workLog: {
|
||
...mockPrisma().workLog,
|
||
findUnique: vi.fn().mockResolvedValue({ id: 1, orderId: 1, status: 'PENDING', costAmount: 89.25 }),
|
||
aggregate: vi.fn().mockResolvedValue({ _sum: { costAmount: 89.25 } }),
|
||
},
|
||
});
|
||
const service = new WorkLogService(prisma);
|
||
const result = await service.verify(1, 'verifier-id');
|
||
|
||
expect(result.status).toBe('VERIFIED');
|
||
expect(prisma.financialTracking.updateMany).toHaveBeenCalledWith(
|
||
expect.objectContaining({ where: { orderId: 1 } }),
|
||
);
|
||
});
|
||
|
||
it('T-TRUD3-10: rejects verify on non-PENDING log', async () => {
|
||
const prisma = mockPrisma({
|
||
workLog: {
|
||
...mockPrisma().workLog,
|
||
findUnique: vi.fn().mockResolvedValue({ id: 1, orderId: 1, status: 'VERIFIED' }),
|
||
},
|
||
});
|
||
const service = new WorkLogService(prisma);
|
||
await expect(service.verify(1, 'user')).rejects.toThrow('Cannot verify a VERIFIED');
|
||
});
|
||
});
|
||
|
||
describe('reject', () => {
|
||
it('T-TRUD3-11: rejects with reason', async () => {
|
||
const prisma = mockPrisma({
|
||
workLog: {
|
||
...mockPrisma().workLog,
|
||
findUnique: vi.fn().mockResolvedValue({ id: 1, orderId: 1, status: 'PENDING' }),
|
||
},
|
||
});
|
||
const service = new WorkLogService(prisma);
|
||
const result = await service.reject(1, 'user', 'Количество завышено');
|
||
|
||
expect(result.status).toBe('REJECTED');
|
||
expect(result.rejectionReason).toBe('Количество завышено');
|
||
});
|
||
});
|
||
|
||
describe('recomputeActualLabor', () => {
|
||
it('T-TRUD3-12: sums VERIFIED logs and updates FinancialTracking', async () => {
|
||
const prisma = mockPrisma({
|
||
workLog: {
|
||
...mockPrisma().workLog,
|
||
aggregate: vi.fn().mockResolvedValue({ _sum: { costAmount: 1250.50 } }),
|
||
},
|
||
});
|
||
const service = new WorkLogService(prisma);
|
||
const result = await service.recomputeActualLabor(1);
|
||
|
||
expect(result).toBe(1250.50);
|
||
expect(prisma.financialTracking.updateMany).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
where: { orderId: 1 },
|
||
data: expect.objectContaining({ actualLabor: expect.anything() }),
|
||
}),
|
||
);
|
||
});
|
||
|
||
it('T-TRUD3-13: idempotent — same verify twice → same actualLabor', async () => {
|
||
const prisma = mockPrisma({
|
||
workLog: {
|
||
...mockPrisma().workLog,
|
||
aggregate: vi.fn().mockResolvedValue({ _sum: { costAmount: 500 } }),
|
||
},
|
||
});
|
||
const service = new WorkLogService(prisma);
|
||
const r1 = await service.recomputeActualLabor(1);
|
||
const r2 = await service.recomputeActualLabor(1);
|
||
expect(r1).toBe(r2);
|
||
expect(r1).toBe(500);
|
||
});
|
||
});
|
||
|
||
describe('summaryByOrder', () => {
|
||
it('T-TRUD3-14: returns operation-level summary with progress', async () => {
|
||
const op = makeOperation();
|
||
const prisma = mockPrisma({
|
||
order: { findUnique: vi.fn().mockResolvedValue({ id: 1, quantity: 10 }) },
|
||
orderLaborOperation: {
|
||
findMany: vi.fn().mockResolvedValue([{ ...op, childOperations: [] }]),
|
||
},
|
||
workLog: {
|
||
...mockPrisma().workLog,
|
||
findMany: vi.fn().mockResolvedValue([
|
||
{ operationId: 1, quantityDone: 100, costAmount: 1785, status: 'VERIFIED' },
|
||
{ operationId: 1, quantityDone: 50, costAmount: 892.5, status: 'PENDING' },
|
||
]),
|
||
},
|
||
});
|
||
const service = new WorkLogService(prisma);
|
||
const result = await service.summaryByOrder(1);
|
||
|
||
expect(result.operations).toHaveLength(1);
|
||
const summary = result.operations[0];
|
||
expect(summary.ceiling).toBe(310); // 31 × 10
|
||
expect(summary.quantityDone).toBe(150);
|
||
expect(summary.quantityVerified).toBe(100);
|
||
expect(summary.costLogged).toBe(2677.5);
|
||
expect(summary.costVerified).toBe(1785);
|
||
expect(summary.costPending).toBe(892.5);
|
||
expect(summary.progressPercent).toBe(32.26); // VERIFIED only: 100/310 * 100
|
||
});
|
||
|
||
it('T-TRUD3-14b: HOURLY progress uses cost-based percentage', async () => {
|
||
const op = makeOperation({ payType: 'HOURLY', baremeUnitPrice: 375, baremeTotalPrice: 625, quantity: 10 });
|
||
const prisma = mockPrisma({
|
||
order: { findUnique: vi.fn().mockResolvedValue({ id: 1, quantity: 5 }) },
|
||
orderLaborOperation: {
|
||
findMany: vi.fn().mockResolvedValue([{ ...op, childOperations: [] }]),
|
||
},
|
||
workLog: {
|
||
...mockPrisma().workLog,
|
||
findMany: vi.fn().mockResolvedValue([
|
||
{ operationId: 1, quantityDone: 3, costAmount: 1125, status: 'VERIFIED' },
|
||
{ operationId: 1, quantityDone: 1, costAmount: 375, status: 'PENDING' },
|
||
]),
|
||
},
|
||
});
|
||
const service = new WorkLogService(prisma);
|
||
const result = await service.summaryByOrder(1);
|
||
const summary = result.operations[0];
|
||
// baremeTotal = 625 * 5 = 3125, costVerified = 1125
|
||
expect(summary.baremeTotal).toBe(3125);
|
||
expect(summary.progressPercent).toBe(36); // 1125/3125 * 100 = 36%
|
||
expect(summary.costPending).toBe(375);
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('WorkerService', () => {
|
||
it('T-TRUD3-15: creates worker', async () => {
|
||
const prisma = mockPrisma();
|
||
const service = new WorkerService(prisma);
|
||
const worker = await service.create({ name: 'Ivan', nameRu: 'Иван', category: 'SVARCSHIK' });
|
||
expect(worker.name).toBe('Ivan');
|
||
expect(worker.category).toBe('SVARCSHIK');
|
||
});
|
||
|
||
it('T-TRUD3-16: deactivates worker', async () => {
|
||
const prisma = mockPrisma();
|
||
const service = new WorkerService(prisma);
|
||
const worker = await service.deactivate(1);
|
||
expect(worker.active).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('recompute admin endpoint — ghost repair', () => {
|
||
it('T-TRUD3-19: recompute after VERIFIED deletion repairs actualLabor', async () => {
|
||
let aggregateSum = 500;
|
||
const prisma = mockPrisma({
|
||
workLog: {
|
||
...mockPrisma().workLog,
|
||
findUnique: vi.fn().mockResolvedValue({ id: 1, orderId: 1, status: 'PENDING', costAmount: 500 }),
|
||
aggregate: vi.fn().mockImplementation(() => ({ _sum: { costAmount: aggregateSum } })),
|
||
},
|
||
});
|
||
const service = new WorkLogService(prisma);
|
||
|
||
const r1 = await service.recomputeActualLabor(1);
|
||
expect(r1).toBe(500);
|
||
|
||
aggregateSum = 0;
|
||
const r2 = await service.recomputeActualLabor(1);
|
||
expect(r2).toBe(0);
|
||
|
||
expect(prisma.financialTracking.updateMany).toHaveBeenLastCalledWith(
|
||
expect.objectContaining({
|
||
where: { orderId: 1 },
|
||
data: expect.objectContaining({ actualLabor: expect.anything() }),
|
||
}),
|
||
);
|
||
});
|
||
|
||
it('T-TRUD3-20: recompute is idempotent (N calls → same result)', async () => {
|
||
const prisma = mockPrisma({
|
||
workLog: {
|
||
...mockPrisma().workLog,
|
||
aggregate: vi.fn().mockResolvedValue({ _sum: { costAmount: 1234.56 } }),
|
||
},
|
||
});
|
||
const service = new WorkLogService(prisma);
|
||
const results = await Promise.all([
|
||
service.recomputeActualLabor(1),
|
||
service.recomputeActualLabor(1),
|
||
service.recomputeActualLabor(1),
|
||
]);
|
||
expect(results).toEqual([1234.56, 1234.56, 1234.56]);
|
||
});
|
||
});
|
||
|
||
describe('wiring proof', () => {
|
||
it('T-TRUD3-17: workLogRoutes registered in server.ts', async () => {
|
||
const fs = await import('fs');
|
||
const content = fs.readFileSync('src/server.ts', 'utf-8');
|
||
expect(content).toContain('workLogRoutes');
|
||
expect(content).toContain("work-log/work-log.routes");
|
||
});
|
||
|
||
it('T-TRUD3-18a: recompute admin endpoint exists in routes', async () => {
|
||
const fs = await import('fs');
|
||
const content = fs.readFileSync('src/modules/work-log/work-log.routes.ts', 'utf-8');
|
||
expect(content).toContain("'/work-logs/recompute/:orderId'");
|
||
expect(content).toContain('recomputeActualLabor');
|
||
expect(content).toContain("authorize(['ADMIN']");
|
||
});
|
||
|
||
it('T-TRUD3-18: verify triggers recomputeActualLabor (actualLabor integration)', async () => {
|
||
const fs = await import('fs');
|
||
const content = fs.readFileSync('src/modules/work-log/work-log.service.ts', 'utf-8');
|
||
expect(content).toContain('recomputeActualLabor');
|
||
// Verify calls recomputeActualLabor
|
||
const verifyMethod = content.slice(content.indexOf('async verify('));
|
||
expect(verifyMethod).toContain('this.recomputeActualLabor');
|
||
});
|
||
});
|