@@ -9,6 +9,7 @@ use App\Models\ConfigurationModel;
|
||||
use App\Models\ReimbursementBatchModel;
|
||||
use App\Models\ReimbursementBatchItemModel;
|
||||
use App\Models\ReimbursementBatchAdminFileModel;
|
||||
use App\Libraries\FinancialStatus;
|
||||
use App\Services\EmailService;
|
||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||
use CodeIgniter\HTTP\Files\UploadedFile;
|
||||
@@ -127,10 +128,13 @@ class ReimbursementController extends BaseController
|
||||
if ($year === '') {
|
||||
return 1;
|
||||
}
|
||||
$record = $this->batchModel
|
||||
->select('MAX(yearly_batch_number) AS max_number')
|
||||
->where('school_year', $year)
|
||||
->first();
|
||||
$record = $this->db->query(
|
||||
'SELECT COALESCE(MAX(yearly_batch_number), 0) AS max_number
|
||||
FROM reimbursement_batches
|
||||
WHERE school_year = ?
|
||||
FOR UPDATE',
|
||||
[$year]
|
||||
)->getRowArray();
|
||||
|
||||
$max = (int) ($record['max_number'] ?? 0);
|
||||
return $max + 1;
|
||||
@@ -553,22 +557,38 @@ class ReimbursementController extends BaseController
|
||||
$title = trim((string) ($this->request->getPost('title') ?? ''));
|
||||
$userId = (int) (session()->get('user_id') ?? 0);
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$sequence = $this->nextYearlyBatchNumberForSchoolYear();
|
||||
|
||||
$data = [
|
||||
'title' => $title !== '' ? $title : null,
|
||||
'status' => 'open',
|
||||
'created_by' => $userId ?: null,
|
||||
'opened_at' => $now,
|
||||
'school_year' => $this->schoolYear,
|
||||
'semester' => $this->semester,
|
||||
'yearly_batch_number' => $sequence,
|
||||
];
|
||||
|
||||
$this->db->transBegin();
|
||||
try {
|
||||
$sequence = $this->nextYearlyBatchNumberForSchoolYear();
|
||||
$data = [
|
||||
'title' => $title !== '' ? $title : null,
|
||||
'status' => 'open',
|
||||
'created_by' => $userId ?: null,
|
||||
'opened_at' => $now,
|
||||
'school_year' => $this->schoolYear,
|
||||
'semester' => $this->semester,
|
||||
'yearly_batch_number' => $sequence,
|
||||
];
|
||||
|
||||
$this->batchModel->insert($data);
|
||||
$batchId = (int) $this->batchModel->getInsertID();
|
||||
if ($batchId <= 0) {
|
||||
throw new \RuntimeException('Batch insert failed.');
|
||||
}
|
||||
|
||||
$label = $title !== '' ? $title : 'Batch #' . $sequence;
|
||||
if ($title === '' && !$this->batchModel->update($batchId, ['title' => $label])) {
|
||||
throw new \RuntimeException('Batch title update failed.');
|
||||
}
|
||||
|
||||
if (!$this->db->transStatus()) {
|
||||
throw new \RuntimeException('Batch transaction failed.');
|
||||
}
|
||||
|
||||
$this->db->transCommit();
|
||||
} catch (\Throwable $e) {
|
||||
$this->db->transRollback();
|
||||
log_message('error', 'Failed to create reimbursement batch: {msg}', ['msg' => $e->getMessage()]);
|
||||
return $this->response->setStatusCode(500)->setJSON([
|
||||
'success' => false,
|
||||
@@ -576,18 +596,6 @@ class ReimbursementController extends BaseController
|
||||
]);
|
||||
}
|
||||
|
||||
if ($batchId <= 0) {
|
||||
return $this->response->setStatusCode(500)->setJSON([
|
||||
'success' => false,
|
||||
'error' => 'Failed to create batch.',
|
||||
]);
|
||||
}
|
||||
|
||||
$label = $title !== '' ? $title : 'Batch #' . $sequence;
|
||||
if ($title === '') {
|
||||
$this->batchModel->update($batchId, ['title' => $label]);
|
||||
}
|
||||
|
||||
$newHash = function_exists('csrf_hash') ? csrf_hash() : null;
|
||||
|
||||
return $this->response
|
||||
@@ -865,26 +873,55 @@ public function updateBatchAssignment()
|
||||
}
|
||||
}
|
||||
|
||||
$this->db->transBegin();
|
||||
|
||||
// Create reimbursement records for any batch items that don't yet have one
|
||||
$items = $this->db->table('reimbursement_batch_items bi')
|
||||
->select('bi.id AS batch_item_id, bi.reimbursement_id AS batch_reimb_id, bi.expense_id, e.amount, e.purchased_by, e.description, e.reimbursement_id AS expense_reimb_id, e.school_year AS expense_school_year, e.semester AS expense_semester')
|
||||
->join('expenses e', 'e.id = bi.expense_id', 'inner')
|
||||
->where('bi.batch_id', $batchId)
|
||||
->where('bi.unassigned_at IS NULL', null, false)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$userId = (int) (session()->get('user_id') ?? 0);
|
||||
|
||||
$this->db->transBegin();
|
||||
|
||||
try {
|
||||
$lockedBatch = $this->db->query('SELECT * FROM reimbursement_batches WHERE id = ? FOR UPDATE', [$batchId])->getRowArray();
|
||||
if (!$lockedBatch || strtolower((string) ($lockedBatch['status'] ?? '')) !== 'open') {
|
||||
throw new \RuntimeException('Batch is not open.');
|
||||
}
|
||||
|
||||
$items = $this->db->query(
|
||||
'SELECT bi.id AS batch_item_id,
|
||||
bi.reimbursement_id AS batch_reimb_id,
|
||||
bi.expense_id,
|
||||
e.amount,
|
||||
e.purchased_by,
|
||||
e.category,
|
||||
e.status AS expense_status,
|
||||
e.description,
|
||||
e.reimbursement_id AS expense_reimb_id,
|
||||
e.school_year AS expense_school_year,
|
||||
e.semester AS expense_semester
|
||||
FROM reimbursement_batch_items bi
|
||||
JOIN expenses e ON e.id = bi.expense_id
|
||||
WHERE bi.batch_id = ?
|
||||
AND bi.unassigned_at IS NULL
|
||||
FOR UPDATE',
|
||||
[$batchId]
|
||||
)->getResultArray();
|
||||
|
||||
if ($items === []) {
|
||||
throw new \RuntimeException('Batch has no active items.');
|
||||
}
|
||||
|
||||
foreach ($items as $item) {
|
||||
$expenseId = (int) ($item['expense_id'] ?? 0);
|
||||
$recipientId = (int) ($item['purchased_by'] ?? 0);
|
||||
if ($expenseId <= 0 || $recipientId <= 0) {
|
||||
continue;
|
||||
throw new \RuntimeException('Batch contains an invalid expense or recipient.');
|
||||
}
|
||||
if (FinancialStatus::normalize((string) ($item['expense_status'] ?? '')) !== 'approved') {
|
||||
throw new \RuntimeException('Every batch expense must be approved before closing.');
|
||||
}
|
||||
if ((float) ($item['amount'] ?? 0) <= 0) {
|
||||
throw new \RuntimeException('Every batch expense amount must be positive.');
|
||||
}
|
||||
if (strcasecmp((string) ($item['category'] ?? ''), 'Donation') === 0) {
|
||||
throw new \RuntimeException('Donation expenses cannot be reimbursed.');
|
||||
}
|
||||
|
||||
$reimbId = $item['batch_reimb_id'] ?: ($item['expense_reimb_id'] ?: $this->lookupReimbursementId($expenseId));
|
||||
@@ -895,28 +932,31 @@ public function updateBatchAssignment()
|
||||
'reimbursed_to' => $recipientId,
|
||||
'approved_by' => $userId ?: null,
|
||||
'description' => trim((string) ($item['description'] ?? '')),
|
||||
'status' => 'Paid',
|
||||
'status' => FinancialStatus::REIMBURSEMENT_PAID,
|
||||
'added_by' => $userId ?: null,
|
||||
'school_year' => $item['expense_school_year'] ?: $this->schoolYear,
|
||||
'semester' => $item['expense_semester'] ?: $this->semester,
|
||||
'reimbursement_method' => 'Check',
|
||||
'batch_number' => $batchId,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
$reimbId = $this->reimbModel->insert($payload);
|
||||
$reimbId = (int) $this->reimbModel->insert($payload);
|
||||
if ($reimbId <= 0) {
|
||||
throw new \RuntimeException('Failed to create reimbursement for batch item.');
|
||||
}
|
||||
}
|
||||
|
||||
if ($reimbId) {
|
||||
$this->reimbModel->update($reimbId, [
|
||||
'batch_number' => $batchId,
|
||||
'approved_by' => $userId ?: null,
|
||||
'status' => 'Paid',
|
||||
]);
|
||||
$this->expenseModel->update($expenseId, ['reimbursement_id' => $reimbId]);
|
||||
if (!empty($item['batch_item_id'])) {
|
||||
$this->batchItemModel->update((int) $item['batch_item_id'], ['reimbursement_id' => $reimbId]);
|
||||
}
|
||||
if (!$this->reimbModel->update($reimbId, [
|
||||
'batch_number' => $batchId,
|
||||
'approved_by' => $userId ?: null,
|
||||
'status' => FinancialStatus::REIMBURSEMENT_PAID,
|
||||
])) {
|
||||
throw new \RuntimeException('Failed to update batch reimbursement.');
|
||||
}
|
||||
if (!$this->expenseModel->update($expenseId, ['reimbursement_id' => $reimbId])) {
|
||||
throw new \RuntimeException('Failed to link batch expense reimbursement.');
|
||||
}
|
||||
if (empty($item['batch_item_id']) || !$this->batchItemModel->update((int) $item['batch_item_id'], ['reimbursement_id' => $reimbId])) {
|
||||
throw new \RuntimeException('Failed to link batch item reimbursement.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -929,7 +969,9 @@ public function updateBatchAssignment()
|
||||
$update['closed_by'] = $userId;
|
||||
}
|
||||
|
||||
$this->batchModel->update($batchId, $update);
|
||||
if (!$this->batchModel->update($batchId, $update)) {
|
||||
throw new \RuntimeException('Failed to close reimbursement batch.');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->db->transRollback();
|
||||
log_message('error', 'Failed to lock reimbursement batch #{batch}: {msg}', [
|
||||
@@ -1979,15 +2021,6 @@ public function updateBatchAssignment()
|
||||
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
if ($expenseId > 0) {
|
||||
$expense = $this->expenseModel->find($expenseId);
|
||||
if ($expense && strcasecmp($expense['category'] ?? '', 'Donation') === 0) {
|
||||
return redirect()->back()->withInput()->with('errors', [
|
||||
'expense_id' => 'Donation expenses are tracked but should not be reimbursed.'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Store file only if one was actually uploaded
|
||||
try {
|
||||
$receiptName = $this->saveReimbReceipt($this->request->getFile('receipt'));
|
||||
@@ -2001,77 +2034,34 @@ public function updateBatchAssignment()
|
||||
$userId = (int) (session()->get('user_id') ?? 0);
|
||||
$recipientId = (int) $this->request->getPost('reimbursed_to');
|
||||
|
||||
// Mark reimbursement as Paid when recorded
|
||||
$data = [
|
||||
'expense_id' => $expenseId ?: null,
|
||||
'amount' => $this->request->getPost('amount'),
|
||||
'reimbursed_to' => $recipientId,
|
||||
'description' => $this->request->getPost('description'),
|
||||
'reimbursement_method' => $method,
|
||||
'check_number' => $method === 'Check' ? $this->request->getPost('check_number') : null,
|
||||
'receipt_path' => $receiptName, // may be null for Cash
|
||||
'school_year' => $this->schoolYear,
|
||||
'semester' => $this->semester,
|
||||
'added_by' => $userId,
|
||||
'approved_by' => $userId,
|
||||
'status' => 'Paid',
|
||||
];
|
||||
|
||||
$this->reimbModel->insert($data);
|
||||
$reimbursementId = $this->reimbModel->getInsertID();
|
||||
|
||||
if ($expenseId = $this->request->getPost('expense_id')) {
|
||||
$this->expenseModel->update($expenseId, ['reimbursement_id' => $reimbursementId]);
|
||||
try {
|
||||
$this->createPaidReimbursementForExpense(
|
||||
$expenseId,
|
||||
(float) $this->request->getPost('amount'),
|
||||
$recipientId,
|
||||
$method,
|
||||
$method === 'Check' ? (string) $this->request->getPost('check_number') : null,
|
||||
$receiptName,
|
||||
(string) $this->request->getPost('description'),
|
||||
$userId
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
if ($receiptName !== null) {
|
||||
@unlink(WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . 'reimbursements' . DIRECTORY_SEPARATOR . basename($receiptName));
|
||||
}
|
||||
log_message('error', 'Reimbursement creation failed: {msg}', ['msg' => $e->getMessage()]);
|
||||
return redirect()->back()->withInput()->with('errors', [
|
||||
'expense_id' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->to('/reimbursements')->with('success', 'Reimbursement recorded as Paid.');
|
||||
}
|
||||
|
||||
|
||||
// Optional old flow kept for compatibility (also sets Paid)
|
||||
public function process()
|
||||
{
|
||||
$expenseId = (int) ($this->request->getPost('expense_id') ?? 0);
|
||||
if ($expenseId > 0) {
|
||||
$expense = $this->expenseModel->find($expenseId);
|
||||
if ($expense && strcasecmp($expense['category'] ?? '', 'Donation') === 0) {
|
||||
return redirect()->back()->withInput()->with('errors', [
|
||||
'expense_id' => 'Donation expenses are tracked but should not be reimbursed.'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$receiptName = $this->saveReimbReceipt($this->request->getFile('receipt'));
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Failed to save reimbursement receipt in process(): {msg}', ['msg' => $e->getMessage()]);
|
||||
return redirect()->back()->withInput()->with('errors', [
|
||||
'receipt' => 'Failed to save uploaded file. Please try again or contact admin.'
|
||||
]);
|
||||
}
|
||||
|
||||
$userId = (int) (session()->get('user_id') ?? 0);
|
||||
$recipientId = (int) $this->request->getPost('reimbursed_to');
|
||||
|
||||
$reimbursementId = $this->reimbModel->insert([
|
||||
'amount' => $this->request->getPost('amount'),
|
||||
'reimbursed_to' => $recipientId,
|
||||
'approved_by' => $userId,
|
||||
'receipt_path' => $receiptName,
|
||||
'description' => 'Expense reimbursement',
|
||||
'status' => 'Paid',
|
||||
'added_by' => $userId,
|
||||
'school_year' => $this->schoolYear,
|
||||
'semester' => $this->semester,
|
||||
'check_number' => $this->request->getPost('check_number'),
|
||||
'reimbursement_method' => $this->request->getPost('reimbursement_method')
|
||||
]);
|
||||
|
||||
$this->expenseModel->update($expenseId, [
|
||||
'reimbursement_id' => $reimbursementId
|
||||
]);
|
||||
|
||||
return redirect()->to('/reimbursements/under-processing')->with('success', 'Reimbursement processed!');
|
||||
return $this->store();
|
||||
}
|
||||
|
||||
public function reimbursedExpenses()
|
||||
@@ -2118,6 +2108,9 @@ public function updateBatchAssignment()
|
||||
if (!$reimb) {
|
||||
throw PageNotFoundException::forPageNotFound("Reimbursement #$id not found");
|
||||
}
|
||||
if ($this->isPaidReimbursement($reimb)) {
|
||||
return redirect()->to('/reimbursements')->with('error', 'Paid reimbursements are immutable. Reverse and replace the transaction instead.');
|
||||
}
|
||||
|
||||
$users = $this->recipientOptions();
|
||||
|
||||
@@ -2136,6 +2129,9 @@ public function updateBatchAssignment()
|
||||
if (!$reimb) {
|
||||
throw PageNotFoundException::forPageNotFound("Reimbursement #$id not found");
|
||||
}
|
||||
if ($this->isPaidReimbursement($reimb)) {
|
||||
return redirect()->to('/reimbursements')->with('error', 'Paid reimbursements are immutable. Reverse and replace the transaction instead.');
|
||||
}
|
||||
|
||||
$methodRaw = (string) $this->request->getPost('reimbursement_method');
|
||||
$method = ucfirst(strtolower($methodRaw));
|
||||
@@ -2198,6 +2194,162 @@ public function updateBatchAssignment()
|
||||
return redirect()->to('/reimbursements')->with('success', 'Reimbursement updated.');
|
||||
}
|
||||
|
||||
public function reverse(int $id)
|
||||
{
|
||||
$reason = trim((string)$this->request->getPost('reason'));
|
||||
if ($reason === '') {
|
||||
return redirect()->back()->with('error', 'Reversal reason is required.');
|
||||
}
|
||||
|
||||
$this->db->transBegin();
|
||||
try {
|
||||
$reimbursement = $this->db->query('SELECT * FROM reimbursements WHERE id = ? FOR UPDATE', [$id])->getRowArray();
|
||||
if (!$reimbursement) {
|
||||
throw new \RuntimeException('Reimbursement not found.');
|
||||
}
|
||||
if (FinancialStatus::normalizeReimbursementStatus($reimbursement['status'] ?? null) !== FinancialStatus::REIMBURSEMENT_PAID) {
|
||||
throw new \RuntimeException('Only paid reimbursements can be reversed.');
|
||||
}
|
||||
|
||||
$expenseId = (int)($reimbursement['expense_id'] ?? 0);
|
||||
if ($expenseId > 0) {
|
||||
$this->db->query('SELECT * FROM expenses WHERE id = ? FOR UPDATE', [$expenseId])->getRowArray();
|
||||
}
|
||||
|
||||
$existingReversal = $this->db->table('reimbursement_reversals')
|
||||
->where('reimbursement_id', $id)
|
||||
->get()
|
||||
->getRowArray();
|
||||
if ($existingReversal) {
|
||||
throw new \RuntimeException('Reimbursement has already been reversed.');
|
||||
}
|
||||
|
||||
$amountCents = (int)round(((float)($reimbursement['amount'] ?? 0)) * 100);
|
||||
$now = utc_now();
|
||||
if (!$this->db->table('reimbursement_reversals')->insert([
|
||||
'reimbursement_id' => $id,
|
||||
'amount_cents' => $amountCents,
|
||||
'reason' => $reason,
|
||||
'reversed_by' => (int)(session()->get('user_id') ?? 0) ?: null,
|
||||
'reversed_at' => $now,
|
||||
'created_at' => $now,
|
||||
])) {
|
||||
throw new \RuntimeException('Reimbursement reversal could not be recorded.');
|
||||
}
|
||||
|
||||
if (!$this->reimbModel->update($id, ['status' => FinancialStatus::REIMBURSEMENT_REVERSED])) {
|
||||
throw new \RuntimeException('Reimbursement status could not be updated.');
|
||||
}
|
||||
if ($expenseId > 0 && !$this->expenseModel->update($expenseId, ['reimbursement_id' => null])) {
|
||||
throw new \RuntimeException('Expense reimbursement link could not be cleared.');
|
||||
}
|
||||
if ($this->db->transStatus() === false) {
|
||||
throw new \RuntimeException('Reimbursement reversal transaction failed.');
|
||||
}
|
||||
|
||||
$this->db->transCommit();
|
||||
} catch (\Throwable $e) {
|
||||
$this->db->transRollback();
|
||||
return redirect()->back()->with('error', $e->getMessage());
|
||||
}
|
||||
|
||||
return redirect()->to('/reimbursements')->with('success', 'Reimbursement reversed.');
|
||||
}
|
||||
|
||||
private function createPaidReimbursementForExpense(
|
||||
int $expenseId,
|
||||
float $amount,
|
||||
int $recipientId,
|
||||
string $method,
|
||||
?string $checkNumber,
|
||||
?string $receiptName,
|
||||
string $description,
|
||||
int $userId,
|
||||
?int $batchId = null
|
||||
): int {
|
||||
if ($expenseId <= 0) {
|
||||
throw new \RuntimeException('A valid approved expense is required.');
|
||||
}
|
||||
|
||||
$this->db->transBegin();
|
||||
try {
|
||||
$expense = $this->db->query('SELECT * FROM expenses WHERE id = ? FOR UPDATE', [$expenseId])->getRowArray();
|
||||
if (!$expense) {
|
||||
throw new \RuntimeException('Expense not found.');
|
||||
}
|
||||
if (FinancialStatus::normalize((string) ($expense['status'] ?? '')) !== 'approved') {
|
||||
throw new \RuntimeException('Expense must be approved before reimbursement.');
|
||||
}
|
||||
if ((float) ($expense['amount'] ?? 0) <= 0 || $amount <= 0) {
|
||||
throw new \RuntimeException('Reimbursement amount must be positive.');
|
||||
}
|
||||
if (abs((float) ($expense['amount'] ?? 0) - $amount) > 0.005) {
|
||||
throw new \RuntimeException('Reimbursement amount must match the approved expense.');
|
||||
}
|
||||
if (strcasecmp((string) ($expense['category'] ?? ''), 'Donation') === 0) {
|
||||
throw new \RuntimeException('Donation expenses are tracked but should not be reimbursed.');
|
||||
}
|
||||
if ($recipientId !== (int) ($expense['purchased_by'] ?? 0)) {
|
||||
throw new \RuntimeException('Reimbursement recipient must match the expense purchaser.');
|
||||
}
|
||||
if ((string) ($expense['school_year'] ?? '') !== (string) $this->schoolYear) {
|
||||
throw new \RuntimeException('Expense is outside the active school year.');
|
||||
}
|
||||
if (!empty($expense['semester']) && (string) $expense['semester'] !== (string) $this->semester) {
|
||||
throw new \RuntimeException('Expense is outside the active semester.');
|
||||
}
|
||||
|
||||
$active = $this->db->query(
|
||||
"SELECT id FROM reimbursements
|
||||
WHERE expense_id = ?
|
||||
AND LOWER(status) NOT IN ('reversed','rejected','cancelled','canceled','voided')
|
||||
FOR UPDATE",
|
||||
[$expenseId]
|
||||
)->getRowArray();
|
||||
if ($active || !empty($expense['reimbursement_id'])) {
|
||||
throw new \RuntimeException('Expense already has an active reimbursement.');
|
||||
}
|
||||
|
||||
$reimbursementId = (int) $this->reimbModel->insert([
|
||||
'expense_id' => $expenseId,
|
||||
'amount' => $amount,
|
||||
'reimbursed_to' => $recipientId,
|
||||
'description' => $description !== '' ? $description : (string) ($expense['description'] ?? ''),
|
||||
'reimbursement_method' => $method,
|
||||
'check_number' => $checkNumber,
|
||||
'receipt_path' => $receiptName,
|
||||
'school_year' => $expense['school_year'] ?: $this->schoolYear,
|
||||
'semester' => $expense['semester'] ?: $this->semester,
|
||||
'added_by' => $userId ?: null,
|
||||
'approved_by' => $userId ?: null,
|
||||
'status' => FinancialStatus::REIMBURSEMENT_PAID,
|
||||
'batch_number' => $batchId,
|
||||
]);
|
||||
if ($reimbursementId <= 0) {
|
||||
throw new \RuntimeException('Reimbursement insert failed.');
|
||||
}
|
||||
|
||||
if (!$this->expenseModel->update($expenseId, ['reimbursement_id' => $reimbursementId])) {
|
||||
throw new \RuntimeException('Expense reimbursement link failed.');
|
||||
}
|
||||
|
||||
if (!$this->db->transStatus()) {
|
||||
throw new \RuntimeException('Reimbursement transaction failed.');
|
||||
}
|
||||
|
||||
$this->db->transCommit();
|
||||
return $reimbursementId;
|
||||
} catch (\Throwable $e) {
|
||||
$this->db->transRollback();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function isPaidReimbursement(array $reimbursement): bool
|
||||
{
|
||||
return FinancialStatus::normalizeReimbursementStatus((string) ($reimbursement['status'] ?? '')) === FinancialStatus::REIMBURSEMENT_PAID;
|
||||
}
|
||||
|
||||
private function lookupReimbursementId(int $expenseId): ?int
|
||||
{
|
||||
if ($expenseId <= 0) {
|
||||
|
||||
Reference in New Issue
Block a user