This commit is contained in:
Mateusz Gruszczyński
2026-04-07 11:41:05 +02:00
parent ca9c78d88d
commit dda1c47764
5 changed files with 455 additions and 255 deletions

View File

@@ -266,7 +266,8 @@ const parsePositiveInt = (value: unknown, fallback: number, max = 100) => {
return Math.min(Math.max(Math.trunc(parsed), 1), max);
};
const validateStatusTransition = (currentStatus: ExpenseStatus, nextStatus: ExpenseStatus) => (transitionMap[currentStatus] ?? []).includes(nextStatus);
const approvalNeedsProof = (nextStatus: ExpenseStatus) => nextStatus === 'APPROVED';
const isShoppingListImportedExpense = (customFields?: Record<string, string> | null) => String(customFields?.externalSource ?? '').trim().toLowerCase() === 'shopping-list-api';
const approvalNeedsProof = (nextStatus: ExpenseStatus, customFields?: Record<string, string> | null) => nextStatus === 'APPROVED' && !isShoppingListImportedExpense(customFields);
const loadOwnedExpenses = async (ids: string[], user: AuthenticatedRequest['user']) => {
const where = user?.role === 'ADMIN'
@@ -442,7 +443,7 @@ export const createExpense = async (req: AuthenticatedRequest, res: Response) =>
return res.status(400).json({ message: 'A new expense can start only as draft, pending, or approved.' });
}
if (approvalNeedsProof(parsed.data.status) && uploadedFiles.length === 0) {
if (approvalNeedsProof(parsed.data.status, parsed.data.customFields) && uploadedFiles.length === 0) {
removeUploadedFiles(uploadedFiles);
return res.status(400).json({ message: 'An attachment is required before an expense can be approved.' });
}
@@ -559,7 +560,7 @@ export const updateExpense = async (req: AuthenticatedRequest, res: Response) =>
const proofIdsToRemove = new Set(parsed.data.removeProofIds);
const remainingProofs = item.proofs.filter((proof) => !proofIdsToRemove.has(proof.id));
if (approvalNeedsProof(parsed.data.status) && remainingProofs.length + uploadedFiles.length === 0) {
if (approvalNeedsProof(parsed.data.status, parsed.data.customFields) && remainingProofs.length + uploadedFiles.length === 0) {
removeUploadedFiles(uploadedFiles);
return res.status(400).json({ message: 'Add at least one attachment before approving an expense.' });
}
@@ -646,7 +647,7 @@ export const bulkUpdateExpenseStatus = async (req: AuthenticatedRequest, res: Re
return res.status(400).json({ message: `Status transition from ${invalidTransition.status} to ${parsed.data.status} is not allowed for ${invalidTransition.title}.` });
}
const missingProof = items.find((item) => approvalNeedsProof(parsed.data.status) && !item.proofs.length);
const missingProof = items.find((item) => approvalNeedsProof(parsed.data.status, item.customFields) && !item.proofs.length);
if (missingProof) {
return res.status(400).json({ message: `Add at least one attachment before approving ${missingProof.title}.` });
}
@@ -694,7 +695,7 @@ export const updateExpenseStatus = async (req: AuthenticatedRequest, res: Respon
return res.status(400).json({ message: `Status transition from ${item.status} to ${parsed.data.status} is not allowed.` });
}
if (approvalNeedsProof(parsed.data.status) && !item.proofs.length) {
if (approvalNeedsProof(parsed.data.status, item.customFields) && !item.proofs.length) {
return res.status(400).json({ message: 'Add at least one attachment before approving an expense.' });
}