nextYearlyBatchNumber($schoolYear); $now = now(); $batch = ReimbursementBatch::query()->create([ 'title' => $title !== null && trim($title) !== '' ? trim($title) : null, 'status' => ReimbursementBatch::STATUS_OPEN, 'created_by' => $userId ?: null, 'opened_at' => $now, 'school_year' => $schoolYear, 'semester' => $semester, 'yearly_batch_number' => $sequence, ]); $label = trim((string) $batch->title) !== '' ? (string) $batch->title : ('Batch #' . $sequence); if (trim((string) $batch->title) === '') { $batch->title = $label; $batch->save(); } return [ 'batch_id' => (int) $batch->id, 'label' => $label, 'sequence' => $sequence, ]; } public function lockBatch(int $batchId, int $userId, string $schoolYear, string $semester): void { $batch = ReimbursementBatch::query()->find($batchId); if (!$batch || strtolower((string) $batch->status) !== ReimbursementBatch::STATUS_OPEN) { throw new RuntimeException('Batch not found or already closed.'); } $adminRows = ReimbursementBatchItem::query() ->selectRaw('DISTINCT COALESCE(admin_id, 0) AS admin_id') ->where('batch_id', $batchId) ->whereNull('unassigned_at') ->get(); $adminIds = array_values(array_unique($adminRows->pluck('admin_id')->map(fn ($id) => (int) $id)->all())); $requiredAdmins = array_values(array_filter($adminIds, static fn ($adminId) => $adminId > 0)); if (!empty($requiredAdmins)) { $uploadedAdminIds = ReimbursementBatchAdminFile::query() ->where('batch_id', $batchId) ->whereIn('admin_id', $requiredAdmins) ->pluck('admin_id') ->map(fn ($id) => (int) $id) ->unique() ->values() ->all(); foreach ($requiredAdmins as $adminId) { if (!in_array($adminId, $uploadedAdminIds, true)) { throw new RuntimeException('All admin sections must have a check file before submitting the batch.'); } } } DB::transaction(function () use ($batchId, $userId, $schoolYear, $semester) { $items = DB::table('reimbursement_batch_items as bi') ->selectRaw('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 as e', 'e.id', '=', 'bi.expense_id') ->where('bi.batch_id', $batchId) ->whereNull('bi.unassigned_at') ->get(); foreach ($items as $item) { $expenseId = (int) ($item->expense_id ?? 0); $recipientId = (int) ($item->purchased_by ?? 0); if ($expenseId <= 0 || $recipientId <= 0) { continue; } $reimbId = $item->batch_reimb_id ?: ($item->expense_reimb_id ?: $this->lookupService->findReimbursementIdForExpense($expenseId)); if (!$reimbId) { $payload = [ 'expense_id' => $expenseId, 'amount' => (float) ($item->amount ?? 0), 'reimbursed_to' => $recipientId, 'approved_by' => $userId ?: null, 'description' => trim((string) ($item->description ?? '')), 'status' => 'Paid', 'added_by' => $userId ?: null, 'school_year' => $item->expense_school_year ?: $schoolYear, 'semester' => $item->expense_semester ?: $semester, 'reimbursement_method' => 'Check', 'batch_number' => $batchId, 'created_at' => now(), 'updated_at' => now(), ]; $reimbId = (int) Reimbursement::query()->create($payload)->id; } if ($reimbId) { Reimbursement::query()->where('id', $reimbId)->update([ 'batch_number' => $batchId, 'approved_by' => $userId ?: null, 'status' => 'Paid', ]); Expense::query()->where('id', $expenseId)->update(['reimbursement_id' => $reimbId]); if (!empty($item->batch_item_id)) { ReimbursementBatchItem::query()->where('id', (int) $item->batch_item_id)->update([ 'reimbursement_id' => $reimbId, ]); } } } ReimbursementBatch::query()->where('id', $batchId)->update([ 'status' => ReimbursementBatch::STATUS_CLOSED, 'closed_at' => now(), 'closed_by' => $userId ?: null, ]); }); } private function nextYearlyBatchNumber(string $schoolYear): int { $year = trim($schoolYear); if ($year === '') { return 1; } $max = ReimbursementBatch::query() ->where('school_year', $year) ->max('yearly_batch_number'); return ((int) $max) + 1; } }