84 lines
3.5 KiB
TypeScript
84 lines
3.5 KiB
TypeScript
import { readFile } from 'node:fs/promises'
|
|
import { join } from 'node:path'
|
|
import { z } from 'zod'
|
|
import { SalaryError } from './personnel-salary'
|
|
import type { ArtPointAdjustment, ArtPointConfiguration } from './art-workflow-points'
|
|
|
|
const configRows = z.array(z.object({
|
|
workflowKey: z.string(), name: z.string().optional(), role: z.string().optional(),
|
|
type: z.string().optional(), points: z.number().finite().nonnegative().optional(), enabled: z.boolean().optional(),
|
|
}))
|
|
const dashboardSchema = z.object({
|
|
generatedAt: z.string().trim().min(1),
|
|
workflows: z.array(z.object({
|
|
businessId: z.string().nullish(), processInstanceId: z.string().nullish(),
|
|
scoreContributions: z.array(z.object({
|
|
workflowKey: z.string().trim().min(1), name: z.string().trim().min(1),
|
|
role: z.enum(['planner', 'fullCase', 'manual']), overtimeExempted: z.boolean(),
|
|
})),
|
|
})),
|
|
})
|
|
|
|
const CACHE_MS = 60_000
|
|
const TIMEOUT_MS = 10_000
|
|
let cached: { url: string; expiresAt: number; rows: ArtPointAdjustment[] } | undefined
|
|
let inFlight: { url: string; promise: Promise<ArtPointAdjustment[]> } | undefined
|
|
|
|
async function fetchExemptions(url: string): Promise<ArtPointAdjustment[]> {
|
|
const controller = new AbortController()
|
|
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS)
|
|
try {
|
|
const response = await fetch(url, { cache: 'no-store', signal: controller.signal })
|
|
if (!response.ok) throw new Error('Dashboard request failed')
|
|
const payload = dashboardSchema.parse(await response.json())
|
|
const unique = new Map<string, ArtPointAdjustment>()
|
|
for (const workflow of payload.workflows) {
|
|
for (const contribution of workflow.scoreContributions) {
|
|
if (!contribution.overtimeExempted) continue
|
|
const workflowKey = workflow.businessId?.trim() || workflow.processInstanceId?.trim()
|
|
if (contribution.workflowKey !== workflowKey || contribution.role === 'manual') {
|
|
throw new Error('Invalid exemption identity')
|
|
}
|
|
const row = { workflowKey, name: contribution.name, role: contribution.role }
|
|
unique.set(JSON.stringify([row.workflowKey, row.name, row.role]), row)
|
|
}
|
|
}
|
|
return Array.from(unique.values())
|
|
} finally {
|
|
clearTimeout(timeout)
|
|
}
|
|
}
|
|
|
|
async function readRemoteExemptions(url: string): Promise<ArtPointAdjustment[]> {
|
|
if (cached?.url === url && cached.expiresAt > Date.now()) return cached.rows
|
|
if (inFlight?.url === url) return inFlight.promise
|
|
const promise = fetchExemptions(url).then(rows => {
|
|
cached = { url, expiresAt: Date.now() + CACHE_MS, rows }
|
|
return rows
|
|
})
|
|
inFlight = { url, promise }
|
|
try {
|
|
return await promise
|
|
} finally {
|
|
if (inFlight?.promise === promise) inFlight = undefined
|
|
}
|
|
}
|
|
|
|
/** Remote contributions are authoritative for exemptions; local manual adjustments stay independent. */
|
|
export async function readArtPointConfiguration(): Promise<ArtPointConfiguration> {
|
|
const directory = process.env.ART_WORKFLOW_POINTS_CONFIG_DIR
|
|
const remote = process.env.ART_WORKFLOW_POINTS_DASHBOARD_URL?.trim()
|
|
const readLocal = async (file: string) => directory
|
|
? configRows.parse(JSON.parse(await readFile(join(directory, file), 'utf8')))
|
|
: []
|
|
try {
|
|
const [deductions, overtimeExemptions] = await Promise.all([
|
|
readLocal('deductions.json'),
|
|
remote ? readRemoteExemptions(remote) : readLocal('overtime_exemptions.json'),
|
|
])
|
|
return { deductions, overtimeExemptions }
|
|
} catch {
|
|
throw new SalaryError('流程积分免扣配置暂时无法读取,请稍后重试。', 503, 'WORKFLOW_POINTS_CONFIG_UNAVAILABLE')
|
|
}
|
|
}
|