fix batch issue

This commit is contained in:
root
2026-06-08 23:20:06 -04:00
parent 384ae8b719
commit 6e8da3cc2c
2 changed files with 210 additions and 96 deletions
+162 -96
View File
@@ -601,153 +601,219 @@ class ReimbursementController extends BaseController
]); ]);
} }
public function updateBatchAssignment() public function updateBatchAssignment()
{ {
if (strtolower($this->request->getMethod()) !== 'post') { if (strtolower($this->request->getMethod()) !== 'post') {
return $this->response->setStatusCode(405)->setJSON([ return $this->response->setStatusCode(405)->setJSON([
'success' => false, 'success' => false,
'error' => 'Method not allowed', 'error' => 'Method not allowed',
]); ]);
} }
$expenseId = (int) $this->request->getPost('expense_id'); $expenseId = (int) $this->request->getPost('expense_id');
$batchIdRaw = $this->request->getPost('batch_id');
$batchId = (int) $batchIdRaw;
$fallbackBatchNumber = $this->request->getPost('batch_number');
if ($batchId <= 0 && $fallbackBatchNumber !== null && trim((string) $fallbackBatchNumber) !== '') {
$batchId = (int) $fallbackBatchNumber;
}
$adminIdRaw = $this->request->getPost('admin_id');
$adminId = ($adminIdRaw === '' || $adminIdRaw === null) ? null : (int) $adminIdRaw;
$reimbursementId = (int) $this->request->getPost('reimbursement_id') ?: null;
if ($expenseId <= 0) { $batchIdRaw = $this->request->getPost('batch_id');
return $this->response->setStatusCode(422)->setJSON([ $batchId = (int) $batchIdRaw;
'success' => false,
'error' => 'Invalid expense id.',
]);
}
$now = date('Y-m-d H:i:s'); $fallbackBatchNumber = $this->request->getPost('batch_number');
if ($batchId <= 0 && $fallbackBatchNumber !== null && trim((string) $fallbackBatchNumber) !== '') {
$batchId = (int) $fallbackBatchNumber;
}
$adminIdRaw = $this->request->getPost('admin_id');
$adminId = ($adminIdRaw === '' || $adminIdRaw === null) ? null : (int) $adminIdRaw;
$reimbursementId = (int) $this->request->getPost('reimbursement_id') ?: null;
if ($expenseId <= 0) {
return $this->response->setStatusCode(422)->setJSON([
'success' => false,
'error' => 'Invalid expense id.',
]);
}
$now = date('Y-m-d H:i:s');
$newHashResponse = function (array $payload = []) {
$newHash = function_exists('csrf_hash') ? csrf_hash() : null;
return $this->response
->setHeader('X-CSRF-HASH', (string) $newHash)
->setJSON(array_merge($payload, [
'csrf_hash' => $newHash,
]));
};
$this->db->transBegin();
try {
$activeItem = $this->batchItemModel $activeItem = $this->batchItemModel
->where('expense_id', $expenseId) ->where('expense_id', $expenseId)
->where('unassigned_at IS NULL', null, false) ->where('unassigned_at IS NULL', null, false)
->first(); ->first();
/**
* If no batch is provided, remove the item from active batch processing.
* This means "unassigned from batch", not "unassigned admin".
*/
if ($batchId <= 0) { if ($batchId <= 0) {
if ($activeItem) { if ($activeItem) {
$this->batchItemModel->update((int) $activeItem['id'], ['unassigned_at' => $now]); $this->batchItemModel->update((int) $activeItem['id'], [
'unassigned_at' => $now,
]);
if (!$reimbursementId && !empty($activeItem['reimbursement_id'])) { if (!$reimbursementId && !empty($activeItem['reimbursement_id'])) {
$reimbursementId = (int) $activeItem['reimbursement_id']; $reimbursementId = (int) $activeItem['reimbursement_id'];
} }
} }
$newHash = function_exists('csrf_hash') ? csrf_hash() : null; if (!$this->db->transStatus()) {
return $this->response throw new \RuntimeException('Transaction failed while unassigning batch item.');
->setHeader('X-CSRF-HASH', (string) $newHash) }
->setJSON([
'success' => true, $this->db->transCommit();
'batch_id' => 0,
'admin_id' => null, return $newHashResponse([
'reimbursement_id' => $reimbursementId, 'success' => true,
'csrf_hash' => $newHash, 'batch_id' => 0,
]); 'admin_id' => null,
'reimbursement_id' => $reimbursementId,
]);
} }
$batch = $this->batchModel->find($batchId); $batch = $this->batchModel->find($batchId);
if (!$batch || strtolower((string) ($batch['status'] ?? 'open')) !== 'open') { if (!$batch || strtolower((string) ($batch['status'] ?? 'open')) !== 'open') {
$this->db->transRollback();
return $this->response->setStatusCode(404)->setJSON([ return $this->response->setStatusCode(404)->setJSON([
'success' => false, 'success' => false,
'error' => 'Batch not found or already closed.', 'error' => 'Batch not found or already closed.',
]); ]);
} }
$currentAdmin = $activeItem ? (int) ($activeItem['admin_id'] ?? 0) : null; if (!$reimbursementId) {
$activeSameBatch = $activeItem && (int) ($activeItem['batch_id'] ?? 0) === $batchId; if ($activeItem && !empty($activeItem['reimbursement_id'])) {
if ($activeSameBatch && ($currentAdmin === ($adminId ?? 0))) { $reimbursementId = (int) $activeItem['reimbursement_id'];
$newHash = function_exists('csrf_hash') ? csrf_hash() : null; } else {
return $this->response $reimbursementId = $this->lookupReimbursementId($expenseId);
->setHeader('X-CSRF-HASH', (string) $newHash) }
->setJSON([
'success' => true,
'batch_id' => $batchId,
'admin_id' => $adminId,
'reimbursement_id' => $activeItem['reimbursement_id'] ?? null,
'csrf_hash' => $newHash,
]);
} }
if ($activeSameBatch) { $activeSameBatch = $activeItem && (int) ($activeItem['batch_id'] ?? 0) === $batchId;
if (!$reimbursementId) { $currentAdmin = $activeItem ? (int) ($activeItem['admin_id'] ?? 0) : null;
$reimbursementId = $activeItem['reimbursement_id'] ? (int) $activeItem['reimbursement_id'] : $this->lookupReimbursementId($expenseId); $requestedAdmin = $adminId ?? 0;
/**
* If the item is already active in the same batch/admin slot, just return success.
*/
if ($activeSameBatch && $currentAdmin === $requestedAdmin) {
if (!$this->db->transStatus()) {
throw new \RuntimeException('Transaction failed while checking existing assignment.');
} }
$updatePayload = [
$this->db->transCommit();
return $newHashResponse([
'success' => true,
'batch_id' => $batchId,
'admin_id' => $adminId,
'reimbursement_id' => $activeItem['reimbursement_id'] ?? $reimbursementId,
]);
}
/**
* If same batch but different admin, update the existing active row.
*
* Important:
* admin_id = null means active in the batch but not assigned to an admin.
* unassigned_at != null means removed from active batch processing.
*/
if ($activeSameBatch) {
$this->batchItemModel->update((int) $activeItem['id'], [
'admin_id' => $adminId, 'admin_id' => $adminId,
'assigned_at' => $now, 'assigned_at' => $now,
'reimbursement_id' => $reimbursementId, 'reimbursement_id' => $reimbursementId,
'unassigned_at' => $adminId === null ? $now : null, 'unassigned_at' => null,
]; ]);
$this->batchItemModel->update((int) $activeItem['id'], $updatePayload);
$newHash = function_exists('csrf_hash') ? csrf_hash() : null;
return $this->response
->setHeader('X-CSRF-HASH', (string) $newHash)
->setJSON([
'success' => true,
'batch_id' => $batchId,
'admin_id' => $adminId,
'reimbursement_id' => $reimbursementId,
'csrf_hash' => $newHash,
]);
}
if ($activeItem) { if (!$this->db->transStatus()) {
$this->batchItemModel->update((int) $activeItem['id'], ['unassigned_at' => $now]); throw new \RuntimeException('Transaction failed while updating existing assignment.');
if (!$reimbursementId && !empty($activeItem['reimbursement_id'])) {
$reimbursementId = (int) $activeItem['reimbursement_id'];
} }
$this->db->transCommit();
return $newHashResponse([
'success' => true,
'batch_id' => $batchId,
'admin_id' => $adminId,
'reimbursement_id' => $reimbursementId,
]);
} }
if (!$reimbursementId) { /**
$reimbursementId = $this->lookupReimbursementId($expenseId); * If the item is active in another batch, soft-unassign that row first.
*/
if ($activeItem) {
$this->batchItemModel->update((int) $activeItem['id'], [
'unassigned_at' => $now,
]);
} }
$insertData = [ /**
* Critical fix:
* Check whether this expense was previously assigned to this batch.
* Because uq_batch_expense(batch_id, expense_id) blocks duplicate rows,
* we must reactivate/update the old row instead of inserting again.
*/
$existingBatchItem = $this->batchItemModel
->where('batch_id', $batchId)
->where('expense_id', $expenseId)
->first();
$assignmentData = [
'batch_id' => $batchId, 'batch_id' => $batchId,
'expense_id' => $expenseId, 'expense_id' => $expenseId,
'reimbursement_id' => $reimbursementId, 'reimbursement_id' => $reimbursementId,
'admin_id' => $adminId, 'admin_id' => $adminId,
'assigned_at' => $now, 'assigned_at' => $now,
'unassigned_at' => null,
'school_year' => $this->schoolYear, 'school_year' => $this->schoolYear,
'semester' => $this->semester, 'semester' => $this->semester,
]; ];
try { if ($existingBatchItem) {
$this->batchItemModel->insert($insertData); $this->batchItemModel->update((int) $existingBatchItem['id'], $assignmentData);
} catch (\Throwable $e) { } else {
log_message('error', 'Failed to assign expense #{expense} to batch #{batch}: {msg}', [ $this->batchItemModel->insert($assignmentData);
'expense' => $expenseId,
'batch' => $batchId,
'msg' => $e->getMessage(),
]);
return $this->response->setStatusCode(500)->setJSON([
'success' => false,
'error' => 'Unable to update batch assignment right now.',
]);
} }
$newHash = function_exists('csrf_hash') ? csrf_hash() : null; if (!$this->db->transStatus()) {
throw new \RuntimeException('Transaction failed while saving assignment.');
}
return $this->response $this->db->transCommit();
->setHeader('X-CSRF-HASH', (string) $newHash)
->setJSON([ return $newHashResponse([
'success' => true, 'success' => true,
'batch_id' => $batchId, 'batch_id' => $batchId,
'admin_id' => $adminId, 'admin_id' => $adminId,
'reimbursement_id' => $reimbursementId, 'reimbursement_id' => $reimbursementId,
'csrf_hash' => $newHash, ]);
]); } catch (\Throwable $e) {
$this->db->transRollback();
log_message('error', 'Failed to update batch assignment for expense #{expense}, batch #{batch}: {msg}', [
'expense' => $expenseId,
'batch' => $batchId,
'msg' => $e->getMessage(),
]);
return $this->response->setStatusCode(500)->setJSON([
'success' => false,
'error' => 'Unable to update batch assignment right now.',
]);
} }
}
public function lockBatch() public function lockBatch()
{ {
+48
View File
@@ -0,0 +1,48 @@
{
"advisories": {
"phpoffice/math": [
{
"advisoryId": "PKSA-jw72-bn8m-h7rc",
"packageName": "phpoffice/math",
"affectedVersions": "<=0.2.0",
"title": "PHPOffice Math allows XXE when processing an XML file in the MathML format ",
"cve": "CVE-2025-48882",
"link": "https://github.com/advisories/GHSA-42hm-pq2f-3r7m",
"reportedAt": "2025-05-29T17:27:39+00:00",
"sources": [
{
"name": "GitHub",
"remoteId": "GHSA-42hm-pq2f-3r7m"
}
],
"severity": "high"
}
],
"phpunit/phpunit": [
{
"advisoryId": "PKSA-z3gr-8qht-p93v",
"packageName": "phpunit/phpunit",
"affectedVersions": ">=0,<8.5.52|>=9.0.0,<9.6.33|>=10.0.0,<10.5.62|>=11.0.0,<11.5.50|>=12.0.0,<12.5.8",
"title": "Unsafe Deserialization in PHPT Code Coverage Handling",
"cve": "CVE-2026-24765",
"link": "https://github.com/sebastianbergmann/phpunit/security/advisories/GHSA-vvj3-c3rp-c85p",
"reportedAt": "2026-01-27T05:21:14+00:00",
"sources": [
{
"name": "GitHub",
"remoteId": "GHSA-vvj3-c3rp-c85p"
},
{
"name": "FriendsOfPHP/security-advisories",
"remoteId": "phpunit/phpunit/CVE-2026-24765.yaml"
}
],
"severity": "high"
}
]
},
"abandoned": {
"paypal/rest-api-sdk-php": "paypal/paypal-server-sdk"
},
"filter": []
}