feat(imageright): finalize two-pass audit engine and task-audit UI polish
Uncommitted work-in-progress from 2026-07-08, carried forward: - audit-engine: refine two-pass metadata + content-inspection audit model - audit-matching, content-inspector, shape-audit-spec: supporting matching and spec updates for the two-pass model - task-audit route/panel: UI and API polish - add @napi-rs/canvas dependency, Dockerfile/next.config adjustments for it - test updates and additions (audit-engine, audit-decision-logic, run-task-audit) - add loose dev notes (clone runbook, issues implementation, overdue filter bug report, shared count helper plan)
This commit is contained in:
parent
6dd7e3e836
commit
01ecfacd04
17 changed files with 1278 additions and 112 deletions
|
|
@ -42,6 +42,10 @@ COPY --from=builder /app/.next/standalone ./
|
|||
COPY --from=builder /app/.next/static ./.next/static
|
||||
COPY --from=builder /app/prisma ./prisma
|
||||
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
|
||||
# Next's standalone output file-tracer doesn't follow @napi-rs/canvas's platform-specific
|
||||
# optional dependency (resolved dynamically at require() time) — copy it in full, same as
|
||||
# .prisma above. Needed by pdf-parse (see src/lib/imageright/content-inspector.ts).
|
||||
COPY --from=builder /app/node_modules/@napi-rs ./node_modules/@napi-rs
|
||||
|
||||
# Set correct permissions
|
||||
RUN chown -R nextjs:nodejs /app
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ const nextConfig: NextConfig = {
|
|||
/* config options here */
|
||||
reactCompiler: true,
|
||||
output: 'standalone',
|
||||
// pdf-parse and its optional native canvas dependency must not be bundled by
|
||||
// Turbopack/webpack — see src/lib/imageright/content-inspector.ts.
|
||||
serverExternalPackages: ['pdf-parse', '@napi-rs/canvas'],
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
|
|
|||
586
ondeck/package-lock.json
generated
586
ondeck/package-lock.json
generated
|
|
@ -13,6 +13,7 @@
|
|||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@kenjiuno/msgreader": "^1.28.0",
|
||||
"@napi-rs/canvas": "^1.0.2",
|
||||
"@prisma/adapter-pg": "^7.2.0",
|
||||
"@prisma/client": "^7.2.0",
|
||||
"@radix-ui/react-avatar": "^1.1.11",
|
||||
|
|
@ -2898,9 +2899,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.80.tgz",
|
||||
"integrity": "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-1.0.2.tgz",
|
||||
"integrity": "sha512-EYEqlMYaCbpZDz+IgDH5xp9MTd3ui4dmGqbQYryhMLnSRxrhHKq5KQWHHKxFUcEP4Hp8/BWgvqXocX4j7iSbOQ==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"e2e/*"
|
||||
|
|
@ -2908,23 +2909,28 @@
|
|||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@napi-rs/canvas-android-arm64": "0.1.80",
|
||||
"@napi-rs/canvas-darwin-arm64": "0.1.80",
|
||||
"@napi-rs/canvas-darwin-x64": "0.1.80",
|
||||
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80",
|
||||
"@napi-rs/canvas-linux-arm64-gnu": "0.1.80",
|
||||
"@napi-rs/canvas-linux-arm64-musl": "0.1.80",
|
||||
"@napi-rs/canvas-linux-riscv64-gnu": "0.1.80",
|
||||
"@napi-rs/canvas-linux-x64-gnu": "0.1.80",
|
||||
"@napi-rs/canvas-linux-x64-musl": "0.1.80",
|
||||
"@napi-rs/canvas-win32-x64-msvc": "0.1.80"
|
||||
"@napi-rs/canvas-android-arm64": "1.0.2",
|
||||
"@napi-rs/canvas-darwin-arm64": "1.0.2",
|
||||
"@napi-rs/canvas-darwin-x64": "1.0.2",
|
||||
"@napi-rs/canvas-linux-arm-gnueabihf": "1.0.2",
|
||||
"@napi-rs/canvas-linux-arm64-gnu": "1.0.2",
|
||||
"@napi-rs/canvas-linux-arm64-musl": "1.0.2",
|
||||
"@napi-rs/canvas-linux-riscv64-gnu": "1.0.2",
|
||||
"@napi-rs/canvas-linux-x64-gnu": "1.0.2",
|
||||
"@napi-rs/canvas-linux-x64-musl": "1.0.2",
|
||||
"@napi-rs/canvas-win32-arm64-msvc": "1.0.2",
|
||||
"@napi-rs/canvas-win32-x64-msvc": "1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-android-arm64": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz",
|
||||
"integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-1.0.2.tgz",
|
||||
"integrity": "sha512-IMXKVQod0ol4vt3gmClUfXz4JAgHYESGPCUqmH3lQxBoL0K/2greJaQE1HVBVxWWFKfLc4OLZVdxg7kXVyXv+g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -2935,12 +2941,16 @@
|
|||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-darwin-arm64": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz",
|
||||
"integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-1.0.2.tgz",
|
||||
"integrity": "sha512-Sc8tPi6cF+5lqOzCCKFALJHhDiRwyMzTPYm3bbhdXsOunU0lQO5f05ucyOzN2r55I23Hg5bsjH63uSCvWp3EgQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -2951,12 +2961,16 @@
|
|||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-darwin-x64": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz",
|
||||
"integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-1.0.2.tgz",
|
||||
"integrity": "sha512-niDXZ9LhKB1zLrUdYB64RHQFDGz9rr0eGx061qtJJU3U20EMMIx28ADF5fVYbhtOgkWQrBjFicfaye1yM0U62A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -2967,12 +2981,16 @@
|
|||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz",
|
||||
"integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-1.0.2.tgz",
|
||||
"integrity": "sha512-sgatQL9JxGRH/Amzcvu0P3t8Am3duou74CisfuJ41Dwt8cWy723z/9KZ8LlgmxfypEwEZxSTNFJtU8d281lmhQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
|
|
@ -2983,12 +3001,16 @@
|
|||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz",
|
||||
"integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-1.0.2.tgz",
|
||||
"integrity": "sha512-dgKuX0peF3xwY6ZF5QxGS4wbfDqpoFAJYXiLSp+guZKARQUKMkRqZSDrXKj7nfrec3UCMzC0PFCPte0ES98AiA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -2999,12 +3021,16 @@
|
|||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz",
|
||||
"integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-1.0.2.tgz",
|
||||
"integrity": "sha512-qwROoDIC9upfvDoRLuPn2aNg9CGW1x0Ygr4k2Or+8paA9d0qBLwk87U+g8KQpoOviKoPoiwl97kvBYuYD7qZoA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -3015,12 +3041,16 @@
|
|||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz",
|
||||
"integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-1.0.2.tgz",
|
||||
"integrity": "sha512-fXRjnPihdnbO6qy1QQOgxAonb68A0TCEG7rj1x7v7rxNElsE8EVIKIEUTvyDtU+sthYSbX+8e7g3oZiLGnOmxw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
|
|
@ -3031,12 +3061,16 @@
|
|||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz",
|
||||
"integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-1.0.2.tgz",
|
||||
"integrity": "sha512-nPR97DXhbWIAy7yazF3jc06kEPMqYMLmPzFOVNlwKPfIoSChnI+x7dc0hTLaihz3jxrjL6j4BbA7earxfx4X3g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -3047,12 +3081,16 @@
|
|||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-x64-musl": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz",
|
||||
"integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-1.0.2.tgz",
|
||||
"integrity": "sha512-l7zZY5+jL5qnBZtDz7CoBtY6p7EkHu422g/0zWwrOrzIwWyWxZFRfZZORY1UG7YApymPLx+UbOkN206xXn/c1Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -3063,12 +3101,36 @@
|
|||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-win32-arm64-msvc": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-1.0.2.tgz",
|
||||
"integrity": "sha512-yE0koHCFF4PIbMc2o2SEALhnipz7WBISh5glLvQiomtIoCcW0np3H4Lw93ceJAfJttTTeIIWFbwH84F7EVzjMQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz",
|
||||
"integrity": "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-1.0.2.tgz",
|
||||
"integrity": "sha512-okU8/t2foV6C31n0GtvEMbfD5rOFc70+/6xUNME9Guld29sgSOIGUEDScAWFlcP3k5TYQRl9TNkwJEEjh15w8A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -3079,6 +3141,10 @@
|
|||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
|
|
@ -13967,6 +14033,190 @@
|
|||
"url": "https://github.com/sponsors/mehmet-kozan"
|
||||
}
|
||||
},
|
||||
"node_modules/pdf-parse/node_modules/@napi-rs/canvas": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.80.tgz",
|
||||
"integrity": "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"e2e/*"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@napi-rs/canvas-android-arm64": "0.1.80",
|
||||
"@napi-rs/canvas-darwin-arm64": "0.1.80",
|
||||
"@napi-rs/canvas-darwin-x64": "0.1.80",
|
||||
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80",
|
||||
"@napi-rs/canvas-linux-arm64-gnu": "0.1.80",
|
||||
"@napi-rs/canvas-linux-arm64-musl": "0.1.80",
|
||||
"@napi-rs/canvas-linux-riscv64-gnu": "0.1.80",
|
||||
"@napi-rs/canvas-linux-x64-gnu": "0.1.80",
|
||||
"@napi-rs/canvas-linux-x64-musl": "0.1.80",
|
||||
"@napi-rs/canvas-win32-x64-msvc": "0.1.80"
|
||||
}
|
||||
},
|
||||
"node_modules/pdf-parse/node_modules/@napi-rs/canvas-android-arm64": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz",
|
||||
"integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/pdf-parse/node_modules/@napi-rs/canvas-darwin-arm64": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz",
|
||||
"integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/pdf-parse/node_modules/@napi-rs/canvas-darwin-x64": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz",
|
||||
"integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/pdf-parse/node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz",
|
||||
"integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/pdf-parse/node_modules/@napi-rs/canvas-linux-arm64-gnu": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz",
|
||||
"integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/pdf-parse/node_modules/@napi-rs/canvas-linux-arm64-musl": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz",
|
||||
"integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/pdf-parse/node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz",
|
||||
"integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/pdf-parse/node_modules/@napi-rs/canvas-linux-x64-gnu": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz",
|
||||
"integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/pdf-parse/node_modules/@napi-rs/canvas-linux-x64-musl": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz",
|
||||
"integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/pdf-parse/node_modules/@napi-rs/canvas-win32-x64-msvc": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz",
|
||||
"integrity": "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/pdfjs-dist": {
|
||||
"version": "5.4.296",
|
||||
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz",
|
||||
|
|
@ -13979,6 +14229,256 @@
|
|||
"@napi-rs/canvas": "^0.1.80"
|
||||
}
|
||||
},
|
||||
"node_modules/pdfjs-dist/node_modules/@napi-rs/canvas": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.100.tgz",
|
||||
"integrity": "sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"workspaces": [
|
||||
"e2e/*"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@napi-rs/canvas-android-arm64": "0.1.100",
|
||||
"@napi-rs/canvas-darwin-arm64": "0.1.100",
|
||||
"@napi-rs/canvas-darwin-x64": "0.1.100",
|
||||
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100",
|
||||
"@napi-rs/canvas-linux-arm64-gnu": "0.1.100",
|
||||
"@napi-rs/canvas-linux-arm64-musl": "0.1.100",
|
||||
"@napi-rs/canvas-linux-riscv64-gnu": "0.1.100",
|
||||
"@napi-rs/canvas-linux-x64-gnu": "0.1.100",
|
||||
"@napi-rs/canvas-linux-x64-musl": "0.1.100",
|
||||
"@napi-rs/canvas-win32-arm64-msvc": "0.1.100",
|
||||
"@napi-rs/canvas-win32-x64-msvc": "0.1.100"
|
||||
}
|
||||
},
|
||||
"node_modules/pdfjs-dist/node_modules/@napi-rs/canvas-android-arm64": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.100.tgz",
|
||||
"integrity": "sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/pdfjs-dist/node_modules/@napi-rs/canvas-darwin-arm64": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.100.tgz",
|
||||
"integrity": "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/pdfjs-dist/node_modules/@napi-rs/canvas-darwin-x64": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.100.tgz",
|
||||
"integrity": "sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/pdfjs-dist/node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.100.tgz",
|
||||
"integrity": "sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/pdfjs-dist/node_modules/@napi-rs/canvas-linux-arm64-gnu": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.100.tgz",
|
||||
"integrity": "sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/pdfjs-dist/node_modules/@napi-rs/canvas-linux-arm64-musl": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.100.tgz",
|
||||
"integrity": "sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/pdfjs-dist/node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.100.tgz",
|
||||
"integrity": "sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/pdfjs-dist/node_modules/@napi-rs/canvas-linux-x64-gnu": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.100.tgz",
|
||||
"integrity": "sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/pdfjs-dist/node_modules/@napi-rs/canvas-linux-x64-musl": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.100.tgz",
|
||||
"integrity": "sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/pdfjs-dist/node_modules/@napi-rs/canvas-win32-arm64-msvc": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.100.tgz",
|
||||
"integrity": "sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/pdfjs-dist/node_modules/@napi-rs/canvas-win32-x64-msvc": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.100.tgz",
|
||||
"integrity": "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/perfect-debounce": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@kenjiuno/msgreader": "^1.28.0",
|
||||
"@napi-rs/canvas": "^1.0.2",
|
||||
"@prisma/adapter-pg": "^7.2.0",
|
||||
"@prisma/client": "^7.2.0",
|
||||
"@radix-ui/react-avatar": "^1.1.11",
|
||||
|
|
|
|||
|
|
@ -40,10 +40,14 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
|||
include: { runByUser: { select: { displayName: true, email: true } } },
|
||||
})
|
||||
|
||||
// Dedup by task, not specItemKey: runTaskAudit now produces one result per real open
|
||||
// Task row, so multiple simultaneously-open tasks can legitimately share a specItemKey
|
||||
// (e.g. duplicate "Request 125 day loss runs" tasks from multiple linked policies).
|
||||
const latestByKey = new Map<string, (typeof audits)[number]>()
|
||||
for (const audit of audits) {
|
||||
if (!latestByKey.has(audit.specItemKey)) {
|
||||
latestByKey.set(audit.specItemKey, audit)
|
||||
const key = audit.taskId ?? `${audit.specItemKey}:${audit.targetDate.toISOString()}`
|
||||
if (!latestByKey.has(key)) {
|
||||
latestByKey.set(key, audit)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ const SPEC_TITLES: Record<string, string> = {
|
|||
review_reserves: 'Review reserves and negotiate adjustments where applicable',
|
||||
claim_review_180: 'Claim Review (180 days)',
|
||||
project_exp_mod_factor: 'Project experience modification factor; send to Account Executive',
|
||||
request_120_day_loss_runs: 'Request 120 day loss runs',
|
||||
request_125_day_loss_runs: 'Request 125 day loss runs',
|
||||
captive_claims_worksheet: 'Assist with captive claims worksheet (if applicable)',
|
||||
loss_summary_pre_renewal: 'Prepare loss summary/analysis for internal pre-renewal meeting',
|
||||
request_90_day_loss_runs: 'Request 90 day loss runs',
|
||||
|
|
@ -78,6 +78,7 @@ export function TaskAuditPanel({ clientId }: { clientId: string }) {
|
|||
const [loading, setLoading] = useState(true)
|
||||
const [running, setRunning] = useState(false)
|
||||
const [fileNotFound, setFileNotFound] = useState(false)
|
||||
const [hasRun, setHasRun] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const loadResults = async () => {
|
||||
|
|
@ -87,6 +88,7 @@ export function TaskAuditPanel({ clientId }: { clientId: string }) {
|
|||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setItems(data.items ?? [])
|
||||
setHasRun((data.items ?? []).length > 0)
|
||||
setFileNotFound(
|
||||
data.items?.length > 0 && data.items.every((i: TaskAuditItem) => i.errorMessage === 'No ImageRight file found for this client')
|
||||
)
|
||||
|
|
@ -113,8 +115,9 @@ export function TaskAuditPanel({ clientId }: { clientId: string }) {
|
|||
throw new Error(data.error || 'Audit run failed')
|
||||
}
|
||||
setItems(data.items.map((i: any) => ({ ...i, targetDate: i.targetDate })))
|
||||
setHasRun(true)
|
||||
setFileNotFound(!data.fileFound)
|
||||
toast.success('ImageRight audit complete')
|
||||
toast.success(data.items.length === 0 ? 'No open/overdue SHAPE tasks to audit' : 'ImageRight audit complete')
|
||||
} catch (err: any) {
|
||||
setError(err.message)
|
||||
toast.error(err.message || 'Audit run failed')
|
||||
|
|
@ -150,13 +153,15 @@ export function TaskAuditPanel({ clientId }: { clientId: string }) {
|
|||
</div>
|
||||
) : items.length === 0 ? (
|
||||
<div className="rounded-md border border-dashed p-6 text-center text-sm text-muted-foreground">
|
||||
No audit has been run yet. Click "Run Audit" to check ImageRight against the SHAPE checklist.
|
||||
{hasRun
|
||||
? 'No open or overdue SHAPE tasks for this client — nothing to audit right now.'
|
||||
: 'No audit has been run yet. Click "Run Audit" to check ImageRight against this client\'s open/overdue SHAPE tasks.'}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{items.map((item) => (
|
||||
<div
|
||||
key={item.specItemKey}
|
||||
key={item.id ?? `${item.specItemKey}-${item.targetDate}`}
|
||||
className="flex flex-col gap-1 rounded-md border p-3 sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
|
|
|
|||
|
|
@ -178,11 +178,11 @@ describe('auditSpecItem — fail missing', () => {
|
|||
expect(item.classification).toBe('FAIL_MISSING')
|
||||
})
|
||||
|
||||
it('classifies NOT_APPLICABLE instead, for conditional items with no candidate', async () => {
|
||||
it('classifies FAIL_MISSING (not NOT_APPLICABLE) for a conditional item with no candidate — runTaskAudit only ever calls this for a real open task, whose existence already establishes applicability', async () => {
|
||||
const captiveSpec = SHAPE_AUDIT_SPEC.find((s) => s.key === 'captive_claims_worksheet')!
|
||||
const client = makeFakeClient({ findDocuments: jest.fn().mockResolvedValue([]) })
|
||||
const { item } = await auditSpecItem(client, files as any, captiveSpec, targetDate, renewalYear)
|
||||
expect(item.classification).toBe('NOT_APPLICABLE')
|
||||
expect(item.classification).toBe('FAIL_MISSING')
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
matchesDocType,
|
||||
normalizeForMatch,
|
||||
docTypesToExtensions,
|
||||
matchTaskToSpec,
|
||||
findPolicyTermFolder,
|
||||
findChildFolder,
|
||||
type ImageRightFolder,
|
||||
|
|
@ -93,6 +94,35 @@ describe('docTypesToExtensions', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('matchTaskToSpec', () => {
|
||||
it('strips a trailing conditional qualifier before matching', () => {
|
||||
const match = matchTaskToSpec(
|
||||
'SHAPE Onboarding Checklist (required after first renewal or when changing carriers)',
|
||||
SHAPE_AUDIT_SPEC
|
||||
)
|
||||
expect(match?.key).toBe('shape_onboarding_checklist')
|
||||
})
|
||||
|
||||
it('matches "Claims Review" (plural) to a "Claim Review" spec item', () => {
|
||||
const match = matchTaskToSpec('Claims Review', SHAPE_AUDIT_SPEC)
|
||||
expect(match?.task).toBe('Claim Review')
|
||||
})
|
||||
|
||||
it('matches the live TaskTemplate name "Request 125 day loss runs" exactly', () => {
|
||||
const match = matchTaskToSpec('Request 125 day loss runs', SHAPE_AUDIT_SPEC)
|
||||
expect(match?.key).toBe('request_125_day_loss_runs')
|
||||
})
|
||||
|
||||
it('falls back to a prefix match for an older/renamed template', () => {
|
||||
const match = matchTaskToSpec('Claim Review in conjunction with 120 day loss summary. Confirm if being marketed.', SHAPE_AUDIT_SPEC)
|
||||
expect(match?.task).toBe('Claim Review')
|
||||
})
|
||||
|
||||
it('returns undefined for a title with no plausible spec match', () => {
|
||||
expect(matchTaskToSpec('Completely unrelated ad-hoc task', SHAPE_AUDIT_SPEC)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('matchesDocType', () => {
|
||||
it('matches when actual type is contained in an expected type', () => {
|
||||
expect(matchesDocType('EMAIL', ['EMAIL'])).toBe(true)
|
||||
|
|
|
|||
163
ondeck/src/lib/imageright/__tests__/run-task-audit.test.ts
Normal file
163
ondeck/src/lib/imageright/__tests__/run-task-audit.test.ts
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
const mockClientFindUnique = jest.fn()
|
||||
const mockTaskFindMany = jest.fn()
|
||||
const mockTaskAuditCreateMany = jest.fn().mockResolvedValue({ count: 0 })
|
||||
|
||||
jest.mock('@/lib/db', () => ({
|
||||
prisma: {
|
||||
client: { findUnique: (...args: any[]) => mockClientFindUnique(...args) },
|
||||
task: { findMany: (...args: any[]) => mockTaskFindMany(...args) },
|
||||
taskAudit: { createMany: (...args: any[]) => mockTaskAuditCreateMany(...args) },
|
||||
},
|
||||
}))
|
||||
|
||||
const mockAuthenticate = jest.fn().mockResolvedValue(undefined)
|
||||
const mockFindFilesByFileNumber = jest.fn().mockResolvedValue([{ id: 10820357 }])
|
||||
const mockGetSortedFolders = jest.fn().mockResolvedValue([])
|
||||
const mockFindDocuments = jest.fn().mockResolvedValue([])
|
||||
|
||||
jest.mock('../client', () => ({
|
||||
ImageRightClient: {
|
||||
fromEnv: () => ({
|
||||
authenticate: mockAuthenticate,
|
||||
findFilesByFileNumber: mockFindFilesByFileNumber,
|
||||
getSortedFolders: mockGetSortedFolders,
|
||||
findDocuments: mockFindDocuments,
|
||||
getDocumentPages: jest.fn().mockResolvedValue([]),
|
||||
getPageImageContent: jest.fn().mockResolvedValue(Buffer.from('')),
|
||||
}),
|
||||
},
|
||||
ImageRightConfigError: class ImageRightConfigError extends Error {},
|
||||
}))
|
||||
|
||||
jest.mock('../content-inspector', () => ({
|
||||
inspectContent: jest.fn(),
|
||||
}))
|
||||
|
||||
import { runTaskAudit } from '../audit-engine'
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000
|
||||
const now = new Date('2026-07-15T00:00:00Z')
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
jest.useFakeTimers().setSystemTime(now)
|
||||
mockTaskAuditCreateMany.mockResolvedValue({ count: 0 })
|
||||
mockAuthenticate.mockResolvedValue(undefined)
|
||||
mockFindFilesByFileNumber.mockResolvedValue([{ id: 10820357 }])
|
||||
mockGetSortedFolders.mockResolvedValue([])
|
||||
mockFindDocuments.mockResolvedValue([])
|
||||
mockClientFindUnique.mockResolvedValue({ id: 'client-1', amsCustomerNumber: 7620 })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
describe('runTaskAudit', () => {
|
||||
it('throws when the client has no AMS customer number', async () => {
|
||||
mockClientFindUnique.mockResolvedValue({ id: 'client-1', amsCustomerNumber: null })
|
||||
await expect(runTaskAudit('client-1')).rejects.toThrow(/AMS customer number/)
|
||||
})
|
||||
|
||||
it('queries only open tasks due now or overdue (excludes future, completed, NA, cancelled)', async () => {
|
||||
mockTaskFindMany.mockResolvedValue([])
|
||||
await runTaskAudit('client-1')
|
||||
expect(mockTaskFindMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
clientId: 'client-1',
|
||||
dueDate: { lte: now },
|
||||
status: { notIn: ['COMPLETED', 'NA', 'CANCELLED'] },
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('uses each open task\'s own dueDate as targetDate — not a value recomputed from Client.renewalDate', async () => {
|
||||
const dueDate = new Date('2026-07-09T00:00:00Z')
|
||||
mockTaskFindMany.mockResolvedValue([
|
||||
{ id: 'task-1', title: 'Claim Review', dueDate, daysOffset: -90 },
|
||||
])
|
||||
|
||||
const result = await runTaskAudit('client-1')
|
||||
|
||||
expect(result.items).toHaveLength(1)
|
||||
expect(result.items[0].targetDate).toEqual(dueDate)
|
||||
expect(result.items[0].linkedTaskId).toBe('task-1')
|
||||
expect(result.items[0].specItemKey).toBe('claim_review_90')
|
||||
})
|
||||
|
||||
it('derives the ImageRight Policy Term folder year from the task\'s own (negative) daysOffset, not dueDate.getFullYear()', async () => {
|
||||
// Due late one calendar year, prepping for a renewal early the next — dueDate.getFullYear()
|
||||
// alone would pick the wrong Policy Term folder.
|
||||
const dueDate = new Date('2026-12-20T00:00:00Z')
|
||||
const daysOffset = -45 // implied renewal: 2026-12-20 + 45 days = 2027-02-03
|
||||
mockTaskFindMany.mockResolvedValue([{ id: 'task-1', title: 'Claims Review', dueDate, daysOffset }])
|
||||
mockGetSortedFolders.mockResolvedValue([])
|
||||
|
||||
await runTaskAudit('client-1')
|
||||
|
||||
// findPolicyTermFolder is called indirectly via resolveParentId -> getSortedFolders;
|
||||
// the folderChecked label on the FAIL_MISSING result surfaces the year it looked for.
|
||||
const impliedYear = new Date(dueDate.getTime() - daysOffset * DAY_MS).getFullYear()
|
||||
expect(impliedYear).toBe(2027)
|
||||
})
|
||||
|
||||
it('matches "Claims Review" (plural) and strips a trailing conditional qualifier before matching', async () => {
|
||||
mockTaskFindMany.mockResolvedValue([
|
||||
{ id: 'task-1', title: 'Claims Review', dueDate: new Date('2026-07-09'), daysOffset: -90 },
|
||||
{
|
||||
id: 'task-2',
|
||||
title: 'SHAPE Onboarding Checklist (required after first renewal or when changing carriers)',
|
||||
dueDate: new Date('2026-01-01'),
|
||||
daysOffset: -335,
|
||||
},
|
||||
])
|
||||
|
||||
const result = await runTaskAudit('client-1')
|
||||
|
||||
expect(result.items.find((i) => i.linkedTaskId === 'task-1')?.specItemKey).toBe('claim_review_90')
|
||||
expect(result.items.find((i) => i.linkedTaskId === 'task-2')?.specItemKey).toBe('shape_onboarding_checklist')
|
||||
})
|
||||
|
||||
it('flags a task with no matching SHAPE spec item as MANUAL_REVIEW instead of silently dropping it', async () => {
|
||||
mockTaskFindMany.mockResolvedValue([
|
||||
{ id: 'task-1', title: 'Completely unrelated ad-hoc task', dueDate: new Date('2026-07-09'), daysOffset: 0 },
|
||||
])
|
||||
|
||||
const result = await runTaskAudit('client-1')
|
||||
|
||||
expect(result.items).toHaveLength(1)
|
||||
expect(result.items[0].classification).toBe('MANUAL_REVIEW')
|
||||
expect(result.items[0].specItemKey).toBe('unmatched:task-1')
|
||||
expect(result.items[0].linkedTaskId).toBe('task-1')
|
||||
expect(result.items[0].errorMessage).toContain('No SHAPE audit spec item matches')
|
||||
})
|
||||
|
||||
it('persists one taskAudit row per open task, linked directly via taskId (no fuzzy re-matching)', async () => {
|
||||
mockTaskFindMany.mockResolvedValue([
|
||||
{ id: 'task-1', title: 'Claim Review', dueDate: new Date('2026-07-09'), daysOffset: -90 },
|
||||
])
|
||||
|
||||
await runTaskAudit('client-1', 'user-1')
|
||||
|
||||
expect(mockTaskAuditCreateMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: [expect.objectContaining({ clientId: 'client-1', taskId: 'task-1', runBy: 'user-1' })],
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('returns FAIL_MISSING for a matched spec item when no ImageRight file exists for the client', async () => {
|
||||
mockFindFilesByFileNumber.mockResolvedValue([])
|
||||
mockTaskFindMany.mockResolvedValue([
|
||||
{ id: 'task-1', title: 'Claim Review', dueDate: new Date('2026-07-09'), daysOffset: -90 },
|
||||
])
|
||||
|
||||
const result = await runTaskAudit('client-1')
|
||||
|
||||
expect(result.fileFound).toBe(false)
|
||||
expect(result.items[0].classification).toBe('FAIL_MISSING')
|
||||
expect(result.items[0].errorMessage).toContain('No ImageRight file found')
|
||||
})
|
||||
})
|
||||
|
|
@ -5,6 +5,7 @@ import {
|
|||
computeWindow,
|
||||
matchesKeywords,
|
||||
matchesDocType,
|
||||
matchTaskToSpec,
|
||||
findPolicyTermFolder,
|
||||
findChildFolder,
|
||||
type ImageRightFolder,
|
||||
|
|
@ -260,7 +261,6 @@ export async function auditSpecItem(
|
|||
renewalYear: number
|
||||
): Promise<AuditCheckResult> {
|
||||
const { start, end } = computeWindow(spec, targetDate)
|
||||
const isConditional = !!spec.condition?.toLowerCase().match(/applicable|conditional/)
|
||||
|
||||
let folderChecked = spec.folderPath.join(' > ') || 'ALL_TIME (whole file)'
|
||||
let bestInWindow: ScoredCandidate | null = null
|
||||
|
|
@ -292,13 +292,14 @@ export async function auditSpecItem(
|
|||
const base = { specItemKey: spec.key, task: spec.task, targetDate, folderChecked }
|
||||
|
||||
if (!bestAnyTime) {
|
||||
// No NOT_APPLICABLE short-circuit here: runTaskAudit only ever calls this for a
|
||||
// client's real, already-generated open Task — its existence already establishes
|
||||
// applicability (conditional items like "captive claims worksheet" simply won't
|
||||
// have an open task if they don't apply), so a missing filing is a genuine gap.
|
||||
return {
|
||||
item: {
|
||||
...base,
|
||||
classification: isConditional ? 'NOT_APPLICABLE' : 'FAIL_MISSING',
|
||||
errorMessage: isConditional
|
||||
? 'No matching document found; item is conditional — confirm applicability manually'
|
||||
: undefined,
|
||||
classification: 'FAIL_MISSING',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -420,33 +421,8 @@ export async function auditSpecItem(
|
|||
}
|
||||
}
|
||||
|
||||
/** Best-effort link to an existing Task: same client, title match, closest dueDate to targetDate. */
|
||||
async function findLinkedTask(clientId: string, taskTitle: string, targetDate: Date): Promise<string | undefined> {
|
||||
const candidates = await prisma.task.findMany({
|
||||
where: { clientId, title: taskTitle },
|
||||
select: { id: true, dueDate: true },
|
||||
})
|
||||
if (candidates.length === 0) return undefined
|
||||
candidates.sort(
|
||||
(a, b) => Math.abs(a.dueDate.getTime() - targetDate.getTime()) - Math.abs(b.dueDate.getTime() - targetDate.getTime())
|
||||
)
|
||||
return candidates[0].id
|
||||
}
|
||||
|
||||
async function resolveEffectiveDate(clientId: string): Promise<Date | null> {
|
||||
const client = await prisma.client.findUnique({
|
||||
where: { id: clientId },
|
||||
select: { renewalDate: true },
|
||||
})
|
||||
if (client?.renewalDate) return client.renewalDate
|
||||
|
||||
const earliestGroup = await prisma.policyGroup.findFirst({
|
||||
where: { clientId },
|
||||
orderBy: { renewalDate: 'asc' },
|
||||
select: { renewalDate: true },
|
||||
})
|
||||
return earliestGroup?.renewalDate ?? null
|
||||
}
|
||||
/** Task statuses that mean a task is closed out and no longer needs auditing — mirrors the definition used by /api/metrics' overdueTasks gauge. */
|
||||
const CLOSED_TASK_STATUSES = ['COMPLETED', 'NA', 'CANCELLED'] as const
|
||||
|
||||
export async function runTaskAudit(clientId: string, runByUserId?: string): Promise<TaskAuditRunResult> {
|
||||
const client = await prisma.client.findUnique({
|
||||
|
|
@ -460,50 +436,74 @@ export async function runTaskAudit(clientId: string, runByUserId?: string): Prom
|
|||
)
|
||||
}
|
||||
|
||||
const effectiveDate = await resolveEffectiveDate(clientId)
|
||||
if (!effectiveDate) {
|
||||
throw new Error('Client has no renewal date configured — cannot compute audit target dates')
|
||||
}
|
||||
// Audit the client's real, already-generated open tasks (overdue or due now) rather
|
||||
// than recomputing target dates from Client.renewalDate — see shape-audit-spec.ts's
|
||||
// doc comment on `daysAfterRenewal` for why that would silently compute dates a full
|
||||
// renewal cycle off from reality.
|
||||
const openTasks = await prisma.task.findMany({
|
||||
where: {
|
||||
clientId,
|
||||
dueDate: { lte: new Date() },
|
||||
status: { notIn: [...CLOSED_TASK_STATUSES] },
|
||||
},
|
||||
select: { id: true, title: true, dueDate: true, daysOffset: true },
|
||||
orderBy: { dueDate: 'asc' },
|
||||
})
|
||||
|
||||
const irClient = ImageRightClient.fromEnv()
|
||||
await irClient.authenticate()
|
||||
|
||||
const files = await irClient.findFilesByFileNumber(String(client.amsCustomerNumber))
|
||||
const fileFound = files.length > 0
|
||||
const renewalYear = effectiveDate.getFullYear()
|
||||
|
||||
const items: TaskAuditItemResult[] = []
|
||||
|
||||
for (const spec of SHAPE_AUDIT_SPEC) {
|
||||
const targetDate = new Date(effectiveDate.getTime() + spec.daysAfterRenewal * DAY_MS)
|
||||
for (const task of openTasks) {
|
||||
const spec = matchTaskToSpec(task.title, SHAPE_AUDIT_SPEC)
|
||||
const folderChecked = spec?.folderPath.join(' > ') || 'ALL_TIME (whole file)'
|
||||
|
||||
let result: TaskAuditItemResult
|
||||
try {
|
||||
if (!fileFound) {
|
||||
result = {
|
||||
specItemKey: spec.key,
|
||||
task: spec.task,
|
||||
targetDate,
|
||||
classification: 'FAIL_MISSING',
|
||||
folderChecked: spec.folderPath.join(' > ') || 'ALL_TIME (whole file)',
|
||||
errorMessage: 'No ImageRight file found for this client',
|
||||
}
|
||||
} else {
|
||||
const { item } = await auditSpecItem(irClient, files, spec, targetDate, renewalYear)
|
||||
result = item
|
||||
if (!spec) {
|
||||
result = {
|
||||
specItemKey: `unmatched:${task.id}`,
|
||||
task: task.title,
|
||||
targetDate: task.dueDate,
|
||||
classification: 'MANUAL_REVIEW',
|
||||
folderChecked: '(no matching SHAPE spec item)',
|
||||
errorMessage: `No SHAPE audit spec item matches this task's title ("${task.title}") — confirm manually`,
|
||||
}
|
||||
} catch (err: any) {
|
||||
} else if (!fileFound) {
|
||||
result = {
|
||||
specItemKey: spec.key,
|
||||
task: spec.task,
|
||||
targetDate,
|
||||
classification: 'MANUAL_REVIEW',
|
||||
folderChecked: spec.folderPath.join(' > ') || 'ALL_TIME (whole file)',
|
||||
errorMessage: `Audit check failed: ${err.message}`,
|
||||
targetDate: task.dueDate,
|
||||
classification: 'FAIL_MISSING',
|
||||
folderChecked,
|
||||
errorMessage: 'No ImageRight file found for this client',
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
// The Policy Term folder to check is named for the renewal the task is
|
||||
// preparing for, not necessarily the calendar year the task happens to be due
|
||||
// in (a pre-renewal task due late one year can prep for a renewal early the
|
||||
// next) — recover it from the task's own stored (negative) offset instead of
|
||||
// guessing from dueDate alone.
|
||||
const impliedRenewalDate = new Date(task.dueDate.getTime() - task.daysOffset * DAY_MS)
|
||||
const { item } = await auditSpecItem(irClient, files, spec, task.dueDate, impliedRenewalDate.getFullYear())
|
||||
result = item
|
||||
} catch (err: any) {
|
||||
result = {
|
||||
specItemKey: spec.key,
|
||||
task: spec.task,
|
||||
targetDate: task.dueDate,
|
||||
classification: 'MANUAL_REVIEW',
|
||||
folderChecked,
|
||||
errorMessage: `Audit check failed: ${err.message}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.linkedTaskId = await findLinkedTask(clientId, spec.task, targetDate)
|
||||
result.linkedTaskId = task.id
|
||||
items.push(result)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,30 @@ export function matchesDocType(actualType: string, expectedTypes: string[]): boo
|
|||
return expectedTypes.some((t) => lower.includes(stripParenthetical(t).toLowerCase()))
|
||||
}
|
||||
|
||||
/** Strip a trailing conditional qualifier, e.g. "Claim Review (can be included with ...)" -> "Claim Review". */
|
||||
function stripTrailingParenthetical(s: string): string {
|
||||
return s.replace(/\s*\([^)]*\)\s*$/, '').trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Match a real Task.title (generated from TaskTemplate) to its corresponding
|
||||
* ShapeAuditSpecItem. Task titles carry a trailing conditional qualifier not present
|
||||
* in the spec's `task` field (e.g. "SHAPE Onboarding Checklist (required after first
|
||||
* renewal or when changing carriers)" -> "SHAPE Onboarding Checklist"), and use
|
||||
* "Claims Review"/"Claim Review" interchangeably — both handled by normalizeForMatch's
|
||||
* plural/singular tolerance. Falls back to a prefix match for older/renamed templates
|
||||
* (e.g. "Claim Review in conjunction with 120 day loss summary...").
|
||||
*/
|
||||
export function matchTaskToSpec(taskTitle: string, specs: ShapeAuditSpecItem[]): ShapeAuditSpecItem | undefined {
|
||||
const normalizedTitle = normalizeForMatch(stripTrailingParenthetical(taskTitle))
|
||||
const exact = specs.find((s) => normalizeForMatch(s.task) === normalizedTitle)
|
||||
if (exact) return exact
|
||||
return specs.find((s) => {
|
||||
const normalizedTask = normalizeForMatch(s.task)
|
||||
return normalizedTitle.startsWith(normalizedTask) || normalizedTask.startsWith(normalizedTitle)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a spec `doc_types` label to the file extensions it plausibly refers to. Real
|
||||
* ImageRight document types (e.g. "Pre-Renewal Information", "Correspondences") don't
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@
|
|||
*/
|
||||
import MsgReader from '@kenjiuno/msgreader'
|
||||
import * as XLSX from 'xlsx'
|
||||
// Must be imported before 'pdf-parse' — provides the DOMMatrix/Path2D/ImageData shims
|
||||
// (backed by @napi-rs/canvas) that pdfjs-dist needs in Node.js; without it, pdf-parse
|
||||
// throws `ReferenceError: DOMMatrix is not defined` at module-evaluation time in
|
||||
// Next.js's server bundle. See pdf-parse's troubleshooting docs (Next.js/Vercel section).
|
||||
import 'pdf-parse/worker'
|
||||
import { PDFParse } from 'pdf-parse'
|
||||
|
||||
export interface InspectedAttachment {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@ export const SHAPE_DESIGNATION_NAMES = ['shape', 'shape 2']
|
|||
*
|
||||
* `windowBeforeDays`/`windowAfterDays` are null for the one ALL_TIME item
|
||||
* (search the entire file history instead of a date window).
|
||||
*
|
||||
* `daysAfterRenewal` is informational only (documents the checklist's intended timing
|
||||
* per the source spec doc) — audit-engine.ts's runTaskAudit() does NOT use it to compute
|
||||
* target dates. It audits the client's real, already-generated open Task rows instead,
|
||||
* using each task's actual `dueDate` (itself computed elsewhere from TaskTemplate.daysOffset,
|
||||
* a *negative*, pre-renewal offset — a different sign convention than this field).
|
||||
*/
|
||||
export interface ShapeAuditSpecItem {
|
||||
key: string
|
||||
|
|
@ -77,8 +83,12 @@ export const SHAPE_AUDIT_SPEC: ShapeAuditSpecItem[] = [
|
|||
keywords: ['MOD'],
|
||||
},
|
||||
{
|
||||
key: 'request_120_day_loss_runs',
|
||||
task: 'Request 120 day loss runs',
|
||||
// Live TaskTemplate data (task_templates.name, is_active=true) says "125", not the
|
||||
// "120" this item was originally ported from SHAPE_IR_Filing_Audit_Spec.md as —
|
||||
// the live template is treated as ground truth since it's what actually generates
|
||||
// real Task rows.
|
||||
key: 'request_125_day_loss_runs',
|
||||
task: 'Request 125 day loss runs',
|
||||
daysAfterRenewal: 240,
|
||||
windowBeforeDays: 7,
|
||||
windowAfterDays: 7,
|
||||
|
|
@ -88,7 +98,7 @@ export const SHAPE_AUDIT_SPEC: ShapeAuditSpecItem[] = [
|
|||
},
|
||||
{
|
||||
key: 'captive_claims_worksheet',
|
||||
task: 'Assist with captive claims worksheet (if applicable)',
|
||||
task: 'Assist with captive claims worksheet',
|
||||
condition: 'Conditional — only applies if client participates in a captive program',
|
||||
daysAfterRenewal: 245,
|
||||
windowBeforeDays: 21,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue