153 lines
6.0 KiB
PHP
153 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;
|
|
}
|
|
}
|