Files
alrahma_sunday_school_api/app/Services/Reimbursements/ReimbursementQueryService.php
T
2026-06-08 23:30:22 -04:00

500 lines
22 KiB
PHP

<?php
namespace App\Services\Reimbursements;
use App\Models\Expense;
use App\Models\ReimbursementBatchAdminFile;
use Illuminate\Support\Facades\DB;
class ReimbursementQueryService
{
public function __construct(
private ReimbursementRecipientService $recipients,
private ReimbursementFileService $files
) {}
public function underProcessing(): array
{
$pendingQuery = DB::table('expenses as e')
->selectRaw('e.id as expense_id, e.amount as expense_amount, e.description, e.receipt_path as expense_receipt, e.retailor, e.purchased_by, u.firstname as purchaser_firstname, u.lastname as purchaser_lastname')
->leftJoin('users as u', 'u.id', '=', 'e.purchased_by')
->leftJoin('reimbursement_batch_items as bi', function ($join) {
$join->on('bi.expense_id', '=', 'e.id');
$join->whereNull('bi.unassigned_at');
})
->whereNull('e.reimbursement_id')
->where('e.category', '!=', 'Donation')
->where(function ($q) {
$q->where('e.status', 'approved')
->orWhere('e.status', 'Approved')
->orWhere('e.status', 'APPROVED');
})
->whereNull('bi.id')
->orderByDesc('e.created_at');
$pendingRows = $pendingQuery->get();
$pendingItems = [];
$itemsMap = [];
foreach ($pendingRows as $row) {
$expenseId = (int) ($row->expense_id ?? 0);
if ($expenseId <= 0) {
continue;
}
$receiptUrl = $this->files->expenseReceiptUrl($row->expense_receipt ?? null);
$fullName = trim(($row->purchaser_firstname ?? '').' '.($row->purchaser_lastname ?? '')) ?: 'Unknown';
$item = [
'id' => $expenseId,
'expense_id' => $expenseId,
'reimbursement_id' => null,
'expense_amount' => (float) ($row->expense_amount ?? 0),
'vendor' => (string) ($row->retailor ?? ''),
'description' => trim((string) ($row->description ?? '')),
'purchased_by' => $fullName,
'receipt_url' => $receiptUrl,
'batch_id' => null,
'batch_label' => null,
'admin_id' => null,
'admin_name' => null,
'batch_number' => 0,
];
$pendingItems[] = $item;
$itemsMap[$expenseId] = $item;
}
$assignedQuery = DB::table('reimbursement_batch_items as bi')
->selectRaw('bi.id as batch_item_id, bi.batch_id, bi.admin_id, bi.reimbursement_id, bi.expense_id, b.title as batch_title, b.status as batch_status, b.yearly_batch_number as batch_year_number, e.amount as expense_amount, e.description, e.receipt_path as expense_receipt, e.retailor, purchaser.firstname as purchaser_firstname, purchaser.lastname as purchaser_lastname, admin.firstname as admin_firstname, admin.lastname as admin_lastname')
->join('reimbursement_batches as b', 'b.id', '=', 'bi.batch_id')
->join('expenses as e', 'e.id', '=', 'bi.expense_id')
->leftJoin('users as purchaser', 'purchaser.id', '=', 'e.purchased_by')
->leftJoin('users as admin', 'admin.id', '=', 'bi.admin_id')
->where('b.status', 'open')
->whereNull('bi.unassigned_at')
->whereNull('e.reimbursement_id')
->where('e.category', '!=', 'Donation')
->where(function ($q) {
$q->where('e.status', 'approved')
->orWhere('e.status', 'Approved')
->orWhere('e.status', 'APPROVED');
})
->orderBy('b.opened_at', 'ASC')
->orderBy('bi.assigned_at', 'ASC');
$assignedRows = $assignedQuery->get();
$batchStructures = [];
foreach ($assignedRows as $row) {
$expenseId = (int) ($row->expense_id ?? 0);
$batchId = (int) ($row->batch_id ?? 0);
if ($expenseId <= 0 || $batchId <= 0) {
continue;
}
$receiptUrl = $this->files->expenseReceiptUrl($row->expense_receipt ?? null);
$purchaser = trim(($row->purchaser_firstname ?? '').' '.($row->purchaser_lastname ?? '')) ?: 'Unknown';
$yearlyNumber = (int) ($row->batch_year_number ?? 0);
$batchLabel = trim((string) ($row->batch_title ?? ''));
if ($batchLabel === '') {
$fallbackNumber = $yearlyNumber > 0 ? $yearlyNumber : $batchId;
$batchLabel = 'Batch #'.$fallbackNumber;
}
$adminId = $row->admin_id ? (int) $row->admin_id : 0;
$adminName = $adminId > 0
? (trim(($row->admin_firstname ?? '').' '.($row->admin_lastname ?? '')) ?: 'Admin')
: 'Unassigned';
$batchStatus = strtolower(trim((string) ($row->batch_status ?? ''))) ?: 'open';
$item = [
'id' => $expenseId,
'expense_id' => $expenseId,
'reimbursement_id' => $row->reimbursement_id ? (int) $row->reimbursement_id : null,
'expense_amount' => (float) ($row->expense_amount ?? 0),
'vendor' => (string) ($row->retailor ?? ''),
'description' => trim((string) ($row->description ?? '')),
'purchased_by' => $purchaser,
'receipt_url' => $receiptUrl,
'batch_id' => $batchId,
'batch_label' => $batchLabel,
'admin_id' => $adminId ?: null,
'admin_name' => $adminId > 0 ? $adminName : null,
'batch_number' => $batchId,
];
$itemsMap[$expenseId] = $item;
if (! isset($batchStructures[$batchId])) {
$batchStructures[$batchId] = [
'batchId' => $batchId,
'label' => $batchLabel,
'slots' => [],
'status' => $batchStatus,
'yearly_batch_number' => $yearlyNumber,
];
} else {
$batchStructures[$batchId]['status'] = $batchStatus;
if ($yearlyNumber > 0) {
$batchStructures[$batchId]['yearly_batch_number'] = $yearlyNumber;
}
}
$slotKey = $adminId ?: 0;
if (! isset($batchStructures[$batchId]['slots'][$slotKey])) {
$batchStructures[$batchId]['slots'][$slotKey] = [
'admin_id' => $adminId ?: 0,
'admin_name' => $adminId > 0 ? $adminName : 'Unassigned',
'items' => [],
'check_file' => null,
];
}
$batchStructures[$batchId]['slots'][$slotKey]['items'][] = $expenseId;
}
$adminFileMap = [];
if (! empty($batchStructures)) {
$batchIds = array_keys($batchStructures);
$files = ReimbursementBatchAdminFile::query()
->select('batch_id', 'admin_id', 'filename', 'original_filename')
->whereIn('batch_id', $batchIds)
->get();
foreach ($files as $file) {
$batchId = (int) ($file->batch_id ?? 0);
$adminId = (int) ($file->admin_id ?? 0);
if ($batchId <= 0) {
continue;
}
$adminFileMap[$batchId][$adminId] = [
'filename' => $file->filename,
'original_filename' => $file->original_filename ?? null,
'url' => $this->files->adminFileUrl($file->filename ?? '', 'inline'),
];
}
}
foreach ($batchStructures as $batchId => &$batch) {
foreach ($batch['slots'] as $slotKey => &$slot) {
$adminId = $slot['admin_id'] ?? 0;
$slot['check_file'] = $adminFileMap[$batchId][$adminId] ?? null;
}
unset($slot);
}
unset($batch);
$existingBatches = array_map(static function (array $batch) {
$normalizedSlots = array_map(static function (array $slot) {
$slot['items'] = array_values(array_map('intval', $slot['items']));
return $slot;
}, array_values($batch['slots']));
$itemIds = [];
foreach ($normalizedSlots as $slot) {
$itemIds = array_merge($itemIds, $slot['items']);
}
return [
'batchId' => $batch['batchId'],
'batchNumber' => $batch['batchId'],
'label' => $batch['label'],
'sequence' => $batch['yearly_batch_number'] ?? 0,
'yearly_batch_number' => $batch['yearly_batch_number'] ?? 0,
'slots' => $normalizedSlots,
'itemIds' => array_values(array_unique($itemIds)),
'status' => $batch['status'] ?? 'open',
];
}, array_values($batchStructures));
$admins = $this->recipients->adminRoster();
return [
'pendingItems' => $pendingItems,
'existingBatches' => $existingBatches,
'adminUsers' => $admins,
'itemsPayload' => array_values($itemsMap),
];
}
public function index(array $filters): array
{
$expenses = Expense::getReimbursedExpensesWithDetails($filters)
->get()
->map(static fn ($row) => (array) $row)
->all();
$batchSummaries = [];
$batchDetails = [];
$batchAttachments = [];
$batchQuery = DB::table('reimbursement_batches as b')
->selectRaw('b.id as batch_id, b.title as batch_title, b.yearly_batch_number, b.closed_at, b.school_year as batch_school_year, e.id as expense_id, e.amount as expense_amount, e.description, e.retailor, e.receipt_path as expense_receipt, e.date_of_purchase as expense_date_of_purchase, e.purchased_by, u.firstname as purchaser_firstname, u.lastname as purchaser_lastname, r.amount as reimb_amount')
->join('reimbursement_batch_items as bi', 'bi.batch_id', '=', 'b.id')
->join('expenses as e', 'e.id', '=', 'bi.expense_id')
->leftJoin('users as u', 'u.id', '=', 'e.purchased_by')
->leftJoin('reimbursements as r', 'r.expense_id', '=', 'e.id')
->where('b.status', 'closed')
->whereNull('bi.unassigned_at');
if (! empty($filters['school_year'])) {
$batchQuery->where(function ($q) use ($filters) {
$q->where('b.school_year', $filters['school_year'])
->orWhere('e.school_year', $filters['school_year']);
});
}
if (! empty($filters['user_id'])) {
$batchQuery->where('e.purchased_by', $filters['user_id']);
}
$batchRows = $batchQuery->get();
foreach ($batchRows as $row) {
$bid = (int) ($row->batch_id ?? 0);
if ($bid <= 0) {
continue;
}
$labelNumber = (int) ($row->yearly_batch_number ?? $bid);
if (! isset($batchSummaries[$bid])) {
$batchSummaries[$bid] = [
'batch_id' => $bid,
'title' => trim((string) ($row->batch_title ?? '')) ?: ('Batch #'.$labelNumber),
'sequence' => $labelNumber,
'closed_at' => $row->closed_at ?? null,
'amount' => 0.0,
'items' => 0,
];
}
$amount = $row->reimb_amount ?? $row->expense_amount ?? 0;
$batchSummaries[$bid]['amount'] += (float) $amount;
$batchSummaries[$bid]['items'] += 1;
if (! isset($batchDetails[$bid])) {
$batchDetails[$bid] = [
'title' => $batchSummaries[$bid]['title'],
'sequence' => $labelNumber,
'closed_at' => $batchSummaries[$bid]['closed_at'],
'items' => [],
'checks' => [],
];
}
if (! isset($batchAttachments[$bid])) {
$batchAttachments[$bid] = [
'receipts' => [],
'checks' => [],
];
}
$batchDetails[$bid]['items'][] = [
'amount' => (float) $amount,
'description' => $row->description ?? '',
'vendor' => $row->retailor ?? '',
'purchased_by' => trim(($row->purchaser_firstname ?? '').' '.($row->purchaser_lastname ?? '')),
'receipt_url' => $this->files->expenseReceiptUrl($row->expense_receipt ?? null),
];
$receiptFilename = trim((string) ($row->expense_receipt ?? ''));
if ($receiptFilename !== '') {
$batchAttachments[$bid]['receipts'][] = [
'expense_id' => (int) ($row->expense_id ?? 0),
'receipt_filename' => $receiptFilename,
'receipt_url' => $this->files->expenseReceiptUrl($receiptFilename),
'purchaser_firstname' => $row->purchaser_firstname ?? '',
'purchaser_lastname' => $row->purchaser_lastname ?? '',
'vendor' => $row->retailor ?? '',
'description' => $row->description ?? '',
'amount' => (float) ($row->expense_amount ?? 0),
'date_of_purchase' => $row->expense_date_of_purchase ?? '',
];
}
}
if (! empty($batchSummaries)) {
$batchIds = array_keys($batchSummaries);
$files = ReimbursementBatchAdminFile::query()
->select('id as file_id', 'batch_id', 'admin_id', 'filename', 'original_filename', 'uploaded_at')
->whereIn('batch_id', $batchIds)
->get();
$adminNames = $this->recipients->adminRoster();
$adminNameMap = [];
foreach ($adminNames as $admin) {
$adminNameMap[(int) $admin['id']] = trim(($admin['firstname'] ?? '').' '.($admin['lastname'] ?? ''));
}
foreach ($files as $file) {
$bid = (int) ($file->batch_id ?? 0);
if ($bid <= 0 || ! isset($batchDetails[$bid])) {
continue;
}
$aid = (int) ($file->admin_id ?? 0);
$batchDetails[$bid]['checks'][] = [
'admin' => $adminNameMap[$aid] ?? ($aid > 0 ? 'Admin #'.$aid : 'Unassigned'),
'filename' => $file->filename,
'original' => $file->original_filename ?? $file->filename,
'url' => $this->files->adminFileUrl($file->filename ?? '', 'inline'),
];
if (! isset($batchAttachments[$bid])) {
$batchAttachments[$bid] = [
'receipts' => [],
'checks' => [],
];
}
$batchAttachments[$bid]['checks'][] = [
'file_id' => (int) ($file->file_id ?? 0),
'filename' => $file->filename,
'original_filename' => $file->original_filename ?? $file->filename,
'admin' => $adminNameMap[$aid] ?? ($aid > 0 ? 'Admin #'.$aid : 'Unassigned'),
'url' => $this->files->adminFileUrl($file->filename ?? '', 'inline'),
'uploaded_at' => $file->uploaded_at ?? null,
];
}
}
if (! empty($batchSummaries)) {
$batchSummaries = array_values($batchSummaries);
usort($batchSummaries, static function ($a, $b) {
$aSeq = (int) ($a['sequence'] ?? 0);
$bSeq = (int) ($b['sequence'] ?? 0);
return $bSeq <=> $aSeq;
});
}
$donationBatch = null;
$donationDetails = [
'items' => [],
'checks' => [],
];
$donationQuery = DB::table('expenses as e')
->selectRaw('e.id as expense_id, e.amount as expense_amount, e.description, e.retailor, e.receipt_path as expense_receipt, e.purchased_by, e.school_year, e.semester, e.status, u.firstname as purchaser_firstname, u.lastname as purchaser_lastname')
->leftJoin('users as u', 'u.id', '=', 'e.purchased_by')
->where('e.category', 'Donation');
if (! empty($filters['school_year'])) {
$donationQuery->where('e.school_year', $filters['school_year']);
}
if (! empty($filters['semester'])) {
$donationQuery->where('e.semester', $filters['semester']);
}
if (! empty($filters['user_id'])) {
$donationQuery->where('e.purchased_by', $filters['user_id']);
}
if (! empty($filters['status'])) {
$donationQuery->where(function ($q) use ($filters) {
$q->where('e.status', $filters['status'])
->orWhere('e.status', strtolower((string) $filters['status']))
->orWhere('e.status', strtoupper((string) $filters['status']));
});
}
$donationRows = $donationQuery->orderByDesc('e.created_at')->get();
if ($donationRows->isNotEmpty()) {
$donationTotal = 0.0;
foreach ($donationRows as $row) {
$amount = (float) ($row->expense_amount ?? 0);
$donationTotal += $amount;
$purchaser = trim(($row->purchaser_firstname ?? '').' '.($row->purchaser_lastname ?? ''));
if ($purchaser === '') {
$purchaser = 'Unknown';
}
$donationDetails['items'][] = [
'amount' => $amount,
'description' => $row->description ?? '',
'vendor' => $row->retailor ?? '',
'purchased_by' => $purchaser,
'receipt_url' => $this->files->expenseReceiptUrl($row->expense_receipt ?? null),
];
}
$donationBatch = [
'title' => 'Donations',
'items' => count($donationDetails['items']),
'amount' => $donationTotal,
];
}
foreach ($expenses as &$e) {
if (array_key_exists('expense_receipt', $e)) {
$e['expense_receipt_url'] = $this->files->expenseReceiptUrl($e['expense_receipt']);
} elseif (array_key_exists('receipt_path', $e)) {
$e['expense_receipt_url'] = $this->files->expenseReceiptUrl($e['receipt_path']);
}
if (array_key_exists('reimb_receipt', $e)) {
$e['reimb_receipt_url'] = $this->files->receiptUrl($e['reimb_receipt']);
} elseif (array_key_exists('receipt_path_reimb', $e)) {
$e['reimb_receipt_url'] = $this->files->receiptUrl($e['receipt_path_reimb']);
}
$recipientId = $e['reimb_recipient_id'] ?? ($e['reimbursed_to'] ?? null);
$hasName = trim(($e['reimb_firstname'] ?? '').' '.($e['reimb_lastname'] ?? '')) !== '';
if (! $hasName) {
$label = $this->recipients->specialRecipientLabel($recipientId);
if ($label !== null) {
$e['reimb_firstname'] = $label;
$e['reimb_lastname'] = '';
}
}
}
unset($e);
$users = $this->recipients->recipientOptions();
$years = DB::table('reimbursements')
->select('school_year')
->distinct()
->orderByDesc('school_year')
->get();
$schoolYears = array_values(array_filter(array_map(static fn ($row) => $row->school_year ?? null, $years->all())));
return [
'expenses' => $expenses,
'users' => $users,
'schoolYears' => $schoolYears,
'batchSummaries' => $batchSummaries,
'batchDetails' => $batchDetails,
'batchAttachments' => $batchAttachments,
'donationBatch' => $donationBatch,
'donationDetails' => $donationDetails,
];
}
public function reimbursedExpenses(): array
{
$rows = DB::table('reimbursements as r')
->selectRaw('e.amount as expense_amount, e.category, e.description, e.receipt_path as expense_receipt, e.purchased_by, r.amount as reimb_amount, r.reimbursement_method, r.check_number, r.receipt_path as reimb_receipt, r.status as reimb_status, r.created_at, u.firstname as purchaser_firstname, u.lastname as purchaser_lastname, a.firstname as approver_firstname, a.lastname as approver_lastname')
->join('expenses as e', 'e.id', '=', 'r.expense_id')
->leftJoin('users as u', 'u.id', '=', 'e.purchased_by')
->leftJoin('users as a', 'a.id', '=', 'r.approved_by')
->orderByDesc('r.created_at')
->get();
$expenses = [];
foreach ($rows as $row) {
$expenses[] = [
'expense_amount' => (float) ($row->expense_amount ?? 0),
'category' => $row->category ?? null,
'description' => $row->description ?? null,
'expense_receipt' => $row->expense_receipt ?? null,
'purchased_by' => $row->purchased_by ?? null,
'reimb_amount' => (float) ($row->reimb_amount ?? 0),
'reimbursement_method' => $row->reimbursement_method ?? null,
'check_number' => $row->check_number ?? null,
'reimb_receipt' => $row->reimb_receipt ?? null,
'reimb_status' => $row->reimb_status ?? null,
'created_at' => $row->created_at ?? null,
'purchaser_firstname' => $row->purchaser_firstname ?? null,
'purchaser_lastname' => $row->purchaser_lastname ?? null,
'approver_firstname' => $row->approver_firstname ?? null,
'approver_lastname' => $row->approver_lastname ?? null,
'expense_receipt_url' => $this->files->expenseReceiptUrl($row->expense_receipt ?? null),
'reimb_receipt_url' => $this->files->receiptUrl($row->reimb_receipt ?? null),
];
}
return $expenses;
}
}