Files
alrahma_sunday_school_api/app/Services/Reimbursements/ReimbursementBatchService.php
T
2026-06-08 23:45:55 -04:00

155 lines
6.0 KiB
PHP

<?php
namespace App\Services\Reimbursements;
use App\Models\Expense;
use App\Models\Reimbursement;
use App\Models\ReimbursementBatch;
use App\Models\ReimbursementBatchAdminFile;
use App\Models\ReimbursementBatchItem;
use Illuminate\Support\Facades\DB;
use RuntimeException;
class ReimbursementBatchService
{
public function __construct(private ReimbursementLookupService $lookupService)
{
}
public function createBatch(?string $title, int $userId, string $schoolYear, string $semester): array
{
$sequence = $this->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;
}
}