feat(mcp): payroll e2e parity: staged salary-run booking + absence deletion (#1075)
* feat(mcp): payroll e2e parity: staged salary-run booking + absence deletion Close the last MCP-surface gaps for running payroll end-to-end via the connector (the v1 REST API already had the full chain): - gnubok_book_salary_run: stages a high-risk book operation; on approval the executor walks review -> approved -> paid -> booked via the new lib/salary/book-run.ts (extracted from the dashboard book route, which now calls the same core) and posts the immutable salary vouchers. - gnubok_delete_absence: staged inverse of gnubok_register_absence, reusing deleteAbsenceRange with a dry-run day-count preview. - Wire the missing payroll operation types into the Granskning label map (register_absence, update_payslip_line, employee ops, vacation_year_close had translations but fell back to humanized snake_case). - Update stale 'booking happens in the web UI' prose in tool descriptions, the payroll-monthly skill, and the workflow hint; payload-size ceiling 56K -> 57K per the documented bump protocol. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mcp): widen pending_operations op-type CHECK + roster typing for book_salary_run The op-type audit (pg-real) caught the exact bug class it exists for: book_salary_run and delete_absence were staged in code without the constraint-expansion migration, so every real staging INSERT would have failed with check_violation while dry_run previewed clean. Ships the documented widen (NOT VALID) + validate migration pair. Also fixes the strict-mode cast in book-run.ts that failed the production typecheck. Verified locally against supabase/postgres 15.8.1.060 with all migrations applied: op-type audit green, pg-real 692/693 (the one failure is the pre-existing TZ-sensitive get_unlinked_1930_lines assertion, green under TZ=UTC as in CI). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
378611a2dc
commit
30771b1619
@@ -3724,6 +3724,105 @@ async function commitRegisterAbsence(
|
||||
}
|
||||
}
|
||||
|
||||
async function commitBookSalaryRun(
|
||||
supabase: SupabaseClient,
|
||||
userId: string,
|
||||
companyId: string,
|
||||
params: Record<string, unknown>
|
||||
): Promise<ExecutorResult> {
|
||||
const salaryRunId = params.salary_run_id as string
|
||||
if (!salaryRunId) return { error: 'salary_run_id is required', status: 400 }
|
||||
|
||||
try {
|
||||
const { advanceAndBookSalaryRun } = await import('@/lib/salary/book-run')
|
||||
const { getErrorEntry } = await import('@/lib/errors/structured-errors')
|
||||
const result = await advanceAndBookSalaryRun(supabase, {
|
||||
companyId,
|
||||
userId,
|
||||
salaryRunId,
|
||||
log: createLogger('commit/book_salary_run'),
|
||||
})
|
||||
if (!result.ok) {
|
||||
const entry = getErrorEntry(result.code)
|
||||
const detail =
|
||||
(result.details?.reason as string | undefined) ??
|
||||
(Array.isArray(result.details?.employees)
|
||||
? `Saknar beräkning: ${(result.details.employees as string[]).join(', ')}`
|
||||
: undefined)
|
||||
return {
|
||||
error: [entry?.message_sv ?? `Kunde inte bokföra lönekörningen: ${result.code}`, detail]
|
||||
.filter(Boolean)
|
||||
.join(' '),
|
||||
status: entry?.httpStatus ?? 500,
|
||||
}
|
||||
}
|
||||
const run = result.data.run as { period_year?: number; period_month?: number; status?: string }
|
||||
return {
|
||||
data: {
|
||||
salary_run_id: salaryRunId,
|
||||
status: run.status ?? 'booked',
|
||||
period: run.period_year
|
||||
? `${run.period_year}-${String(run.period_month).padStart(2, '0')}`
|
||||
: undefined,
|
||||
journal_entry_ids: result.data.entryIds,
|
||||
nollkorning: result.data.nollkorning,
|
||||
warnings: result.data.warnings,
|
||||
},
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
error: err instanceof Error ? err.message : 'Failed to book salary run',
|
||||
status: 500,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function commitDeleteAbsence(
|
||||
supabase: SupabaseClient,
|
||||
companyId: string,
|
||||
params: Record<string, unknown>
|
||||
): Promise<ExecutorResult> {
|
||||
const employeeId = params.employee_id as string
|
||||
const from = params.from as string
|
||||
const to = params.to as string
|
||||
if (!employeeId || !from || !to) {
|
||||
return { error: 'employee_id, from and to are required', status: 400 }
|
||||
}
|
||||
|
||||
try {
|
||||
const { deleteAbsenceRange } = await import('@/lib/salary/absence')
|
||||
const { getErrorEntry } = await import('@/lib/errors/structured-errors')
|
||||
const result = await deleteAbsenceRange(supabase, {
|
||||
companyId,
|
||||
employeeId,
|
||||
from,
|
||||
to,
|
||||
absenceType: (params.absence_type as string | undefined) || undefined,
|
||||
})
|
||||
if (!result.ok) {
|
||||
const entry = getErrorEntry(result.code)
|
||||
return {
|
||||
error: entry?.message_sv ?? `Kunde inte ta bort frånvaron: ${result.code}`,
|
||||
status: entry?.httpStatus ?? 500,
|
||||
}
|
||||
}
|
||||
return {
|
||||
data: {
|
||||
employee_id: employeeId,
|
||||
from,
|
||||
to,
|
||||
absence_type: (params.absence_type as string | undefined) ?? null,
|
||||
deleted_count: result.data.deleted_count,
|
||||
},
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
error: err instanceof Error ? err.message : 'Failed to delete absence',
|
||||
status: 500,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function commitSetEmployeeOpeningBalances(
|
||||
supabase: SupabaseClient,
|
||||
userId: string,
|
||||
@@ -4316,6 +4415,12 @@ async function commitPendingOperationInner(
|
||||
case 'register_absence':
|
||||
result = await commitRegisterAbsence(supabase, companyId, pendingOp.params)
|
||||
break
|
||||
case 'book_salary_run':
|
||||
result = await commitBookSalaryRun(supabase, userId, companyId, pendingOp.params)
|
||||
break
|
||||
case 'delete_absence':
|
||||
result = await commitDeleteAbsence(supabase, companyId, pendingOp.params)
|
||||
break
|
||||
case 'create_employee':
|
||||
result = await commitCreateEmployee(supabase, userId, companyId, pendingOp.params)
|
||||
break
|
||||
|
||||
Reference in New Issue
Block a user