收紧报销草稿读取边界 [09-04-reimbursement-state-gap-finance-view][R2]
这个提交包含在:
@@ -26,6 +26,11 @@ const { mapInvoiceDocumentMeta } = require("../services/invoiceDocumentMeta");
|
||||
const { dispatchNotificationBatch, flushNotificationDispatchTasks } = require("../services/notificationDispatcher");
|
||||
const { FEATURE_FLAG_KEYS, evaluateFeatureFlag } = require("../services/featureFlags");
|
||||
const { ensureCutoverAllowed } = require("../services/cutover");
|
||||
const {
|
||||
buildReadableReimbursementWhere,
|
||||
canReadReimbursementBusinessFact,
|
||||
isGlobalReimbursementViewRole,
|
||||
} = require("../services/reimbursementVisibility");
|
||||
const { resolveApplicableBudgetPoolByContext } = require("../services/budgetPool");
|
||||
const {
|
||||
buildPersistedRiskFlag,
|
||||
@@ -245,9 +250,12 @@ const ATTACHMENT_MIME_ALLOWLIST = new Set([
|
||||
"image/webp",
|
||||
]);
|
||||
|
||||
function ensureOwnOrCanViewAll(fastify, request, reimbursement) {
|
||||
async function ensureOwnOrCanViewAll(fastify, request, reimbursement) {
|
||||
const canViewAll = fastify.hasPermission(request.employee, "expense.view_all");
|
||||
if (canViewAll) {
|
||||
if (
|
||||
canViewAll &&
|
||||
await canReadReimbursementBusinessFact(fastify.prisma, reimbursement, request.employee.id)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -275,7 +283,11 @@ async function ensureOwnCanViewAllOrWorkflowReviewer(fastify, request, reimburse
|
||||
const canViewAll = fastify.hasPermission(request.employee, "expense.view_all");
|
||||
const isOwn = reimbursement.owner_id === request.employee.id || reimbursement.submitter_id === request.employee.id;
|
||||
const canViewOwn = fastify.hasPermission(request.employee, "expense.view_own") && isOwn;
|
||||
if (canViewAll || canViewOwn) return;
|
||||
if (canViewOwn) return;
|
||||
if (
|
||||
canViewAll &&
|
||||
await canReadReimbursementBusinessFact(fastify.prisma, reimbursement, request.employee.id)
|
||||
) return;
|
||||
|
||||
const delegatedPrincipalIds = await listActiveDelegationPrincipals(fastify.prisma, {
|
||||
delegateEmployeeId: request.employee.id,
|
||||
@@ -852,7 +864,7 @@ function buildReimbursementStatsConditions({
|
||||
conditions.push(Prisma.sql`${sqlIdentifier(formAlias, "department_id")} = ${departmentId}`);
|
||||
}
|
||||
|
||||
if (!isGlobalViewRole(request.employee.roleCodes ?? [])) {
|
||||
if (!isGlobalReimbursementViewRole(request.employee.roleCodes ?? [])) {
|
||||
conditions.push(
|
||||
Prisma.sql`(
|
||||
${sqlIdentifier(formAlias, "owner_id")} = ${request.employee.id}
|
||||
@@ -862,6 +874,20 @@ function buildReimbursementStatsConditions({
|
||||
);
|
||||
}
|
||||
|
||||
conditions.push(
|
||||
Prisma.sql`(
|
||||
${sqlIdentifier(formAlias, "status")}::text <> 'draft'
|
||||
OR ${sqlIdentifier(formAlias, "owner_id")} = ${request.employee.id}
|
||||
OR ${sqlIdentifier(formAlias, "submitter_id")} = ${request.employee.id}
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM "workflow_instances" visibility_wi
|
||||
WHERE visibility_wi.entity_type = 'reimbursement'
|
||||
AND visibility_wi.entity_id = ${sqlIdentifier(formAlias, "id")}
|
||||
)
|
||||
)`,
|
||||
);
|
||||
|
||||
return conditions;
|
||||
}
|
||||
|
||||
@@ -1030,32 +1056,6 @@ function parseReimbursementListQuery(query) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildOwnScopeWhere(employeeId) {
|
||||
return {
|
||||
OR: [{ owner_id: employeeId }, { submitter_id: employeeId }],
|
||||
};
|
||||
}
|
||||
|
||||
function isGlobalViewRole(roleCodes) {
|
||||
return roleCodes.some((code) =>
|
||||
["admin", "ceo", "finance", "finance_mgr", "hr"].includes(code),
|
||||
);
|
||||
}
|
||||
|
||||
function buildViewAllScopeWhere(request) {
|
||||
if (isGlobalViewRole(request.employee.roleCodes ?? [])) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
OR: [
|
||||
{ owner_id: request.employee.id },
|
||||
{ submitter_id: request.employee.id },
|
||||
{ owner: { direct_manager_id: request.employee.id } },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function mapReimbursementSummary(item) {
|
||||
return {
|
||||
id: item.id,
|
||||
@@ -2042,12 +2042,11 @@ async function reimbursementRoutes(fastify) {
|
||||
delegateEmployeeId: request.employee.id,
|
||||
allowedScopes: ["all"],
|
||||
});
|
||||
const where = {
|
||||
...(query.status ? { status: query.status } : {}),
|
||||
...(scope === "all"
|
||||
? buildViewAllScopeWhere(request)
|
||||
: buildOwnScopeWhere(request.employee.id)),
|
||||
};
|
||||
const where = buildReadableReimbursementWhere({
|
||||
employee: request.employee,
|
||||
scope,
|
||||
filters: query.status ? { status: query.status } : {},
|
||||
});
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
fastify.prisma.reimbursementForm.findMany({
|
||||
@@ -3335,6 +3334,7 @@ async function reimbursementRoutes(fastify) {
|
||||
select: {
|
||||
owner_id: true,
|
||||
submitter_id: true,
|
||||
status: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -3435,7 +3435,7 @@ async function reimbursementRoutes(fastify) {
|
||||
sensitivity_level: true,
|
||||
attachment_type: true,
|
||||
download_policy: true,
|
||||
reimbursement_form: { select: { owner_id: true, submitter_id: true } },
|
||||
reimbursement_form: { select: { owner_id: true, submitter_id: true, status: true } },
|
||||
},
|
||||
});
|
||||
if (!attachment) {
|
||||
@@ -3508,6 +3508,7 @@ async function reimbursementRoutes(fastify) {
|
||||
id: true,
|
||||
owner_id: true,
|
||||
submitter_id: true,
|
||||
status: true,
|
||||
invoice_pool_items: {
|
||||
orderBy: [{ created_at: "asc" }, { id: "asc" }],
|
||||
select: {
|
||||
@@ -4132,12 +4133,12 @@ async function reimbursementRoutes(fastify) {
|
||||
const reimbursementId = parseReimbursementId(request.params?.id);
|
||||
const reimbursement = await fastify.prisma.reimbursementForm.findUnique({
|
||||
where: { id: reimbursementId },
|
||||
select: { id: true, owner_id: true, submitter_id: true },
|
||||
select: { id: true, owner_id: true, submitter_id: true, status: true },
|
||||
});
|
||||
if (!reimbursement) {
|
||||
throw new AppError({ code: "SYS_404", message: "Not Found", statusCode: 404 });
|
||||
}
|
||||
ensureOwnOrCanViewAll(fastify, request, reimbursement);
|
||||
await ensureOwnOrCanViewAll(fastify, request, reimbursement);
|
||||
const precheck = await resolveEntityPrecheck({
|
||||
prisma: fastify.prisma,
|
||||
config: fastify.config,
|
||||
|
||||
在新工单中引用
屏蔽一个用户