diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 8cde860..3167100 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -237,6 +237,12 @@ $routes->get('Medical/master', 'Medical::master'); $routes->get('Medical/master_report/med-payment-edit', 'Payment_Medical::index'); $routes->get('Medical/master_report/(:segment)', 'Medical::master_report/$1'); $routes->get('Medical/master_link/(:segment)', 'Medical::master_link/$1'); +$routes->get('Medical/credit_payout_pool', 'Medical::credit_payout_pool'); +$routes->match(['get', 'post'], 'Medical/credit_payout_pool_datatable', 'Medical::credit_payout_pool_datatable'); +$routes->get('Medical/credit_payout_request', 'Medical::credit_payout_request'); +$routes->post('Medical/credit_payout_request_create', 'Medical::credit_payout_request_create'); +$routes->get('Medical/credit_payout_requests', 'Medical::credit_payout_requests'); +$routes->get('Medical/credit_payout_request_detail/(:num)', 'Medical::credit_payout_request_detail/$1'); $routes->get('Payment_Medical', 'Payment_Medical::index'); $routes->get('Payment_Medical/payment_log', 'Payment_Medical::payment_log'); @@ -263,6 +269,7 @@ $routes->get('Finance/scroll_items_table', 'Finance::scrollItemsTable'); $routes->get('Finance/bank_deposits', 'Finance::bankDeposits'); $routes->get('Finance/bank_deposits_table', 'Finance::bankDepositsTable'); $routes->get('Finance/bank_audit', 'Finance::bankAudit'); +$routes->get('Finance/medical_store_credit_account', 'Finance::medicalStoreCreditAccount'); $routes->get('Finance/payout/opd-consult', 'Finance::payoutOpdConsult'); $routes->get('Finance/payout/opd-consult-summary', 'Finance::payoutOpdConsultSummary'); $routes->get('Finance/payout/opd-consult-drafts-table', 'Finance::payoutOpdConsultDraftsTable'); @@ -270,6 +277,7 @@ $routes->post('Finance/payout/opd-consult-draft-create', 'Finance::payoutOpdCons $routes->post('Finance/payout/opd-consult-draft-update', 'Finance::payoutOpdConsultDraftUpdate'); $routes->post('Finance/payout/opd-consult-draft-delete', 'Finance::payoutOpdConsultDraftDelete'); $routes->get('Finance/bank_audit_direct_payments_table', 'Finance::bankAuditDirectPaymentsTable'); +$routes->get('Finance/bank_audit_outgoing_payments_table', 'Finance::bankAuditOutgoingPaymentsTable'); $routes->get('Finance/bank_statement_entries_table', 'Finance::bankStatementEntriesTable'); $routes->get('Finance/bank_settlement_entries_table', 'Finance::bankSettlementEntriesTable'); $routes->get('Finance/bank_settlement_linked_payments_table', 'Finance::bankSettlementLinkedPaymentsTable'); @@ -285,11 +293,22 @@ $routes->post('Finance/bank_deposit_status_update', 'Finance::bankDepositStatusU $routes->post('Finance/bank_statement_entry_create', 'Finance::bankStatementEntryCreate'); $routes->post('Finance/bank_reconcile_match', 'Finance::bankReconcileMatch'); $routes->post('Finance/bank_reconcile_batch_match', 'Finance::bankReconcileBatchMatch'); +$routes->post('Finance/bank_reconcile_outgoing_single_match', 'Finance::bankReconcileOutgoingSingleMatch'); +$routes->post('Finance/bank_reconcile_outgoing_batch_match', 'Finance::bankReconcileOutgoingBatchMatch'); $routes->post('Finance/bank_settlement_create', 'Finance::bankSettlementCreate'); $routes->post('Finance/bank_settlement_match_statement', 'Finance::bankSettlementMatchStatement'); $routes->post('Finance/bank_reconcile_unmatch', 'Finance::bankReconcileUnmatch'); $routes->post('Finance/bank_pos_settlement_create', 'Finance::bankPosSettlementCreate'); $routes->post('Finance/bank_pos_settlement_accept', 'Finance::bankPosSettlementAccept'); +$routes->get('Finance/medical_store_requests', 'Finance::medicalStoreRequests'); +$routes->get('Finance/medical_store_requests_table', 'Finance::medicalStoreRequestsTable'); +$routes->get('Finance/medical_store_request_detail/(:num)', 'Finance::medicalStoreRequestDetail/$1'); +$routes->get('Finance/medical_store_request_lines_table', 'Finance::medicalStoreRequestLinesTable'); +$routes->post('Finance/medical_store_request_review', 'Finance::medicalStoreRequestReview'); +$routes->post('Finance/medical_store_request_approve', 'Finance::medicalStoreRequestApprove'); +$routes->post('Finance/medical_store_request_reject', 'Finance::medicalStoreRequestReject'); +$routes->post('Finance/medical_store_payment_create', 'Finance::medicalStorePaymentCreate'); +$routes->get('Finance/medical_store_dashboard_card', 'Finance::medicalStoreDashboardCard'); service('auth')->routes($routes); diff --git a/app/Controllers/Finance.php b/app/Controllers/Finance.php index c552b42..bf6fc3c 100644 --- a/app/Controllers/Finance.php +++ b/app/Controllers/Finance.php @@ -19,6 +19,11 @@ use App\Models\FinanceScrollSubmissionItemModel; use App\Models\FinanceScrollSubmissionModel; use App\Models\FinanceVendorInvoiceModel; use App\Models\FinancePharmacyBillModel; +use App\Models\FinancePayoutRequestAuditModel; +use App\Models\FinancePayoutRequestLineModel; +use App\Models\FinancePayoutRequestModel; +use App\Models\FinanceOutgoingPaymentAllocationModel; +use App\Models\FinanceOutgoingPaymentHistoryModel; use App\Models\FinanceVendorModel; class Finance extends BaseController @@ -40,6 +45,11 @@ class Finance extends BaseController private FinanceDoctorAgreementModel $doctorAgreementModel; private FinanceDoctorPayoutModel $doctorPayoutModel; private FinancePharmacyBillModel $pharmBillModel; + private FinancePayoutRequestModel $payoutRequestModel; + private FinancePayoutRequestLineModel $payoutRequestLineModel; + private FinanceOutgoingPaymentHistoryModel $outgoingPaymentHistoryModel; + private FinanceOutgoingPaymentAllocationModel $outgoingPaymentAllocationModel; + private FinancePayoutRequestAuditModel $payoutRequestAuditModel; public function __construct() { @@ -60,6 +70,11 @@ class Finance extends BaseController $this->doctorAgreementModel = new FinanceDoctorAgreementModel(); $this->doctorPayoutModel = new FinanceDoctorPayoutModel(); $this->pharmBillModel = new FinancePharmacyBillModel(); + $this->payoutRequestModel = new FinancePayoutRequestModel(); + $this->payoutRequestLineModel = new FinancePayoutRequestLineModel(); + $this->outgoingPaymentHistoryModel = new FinanceOutgoingPaymentHistoryModel(); + $this->outgoingPaymentAllocationModel = new FinanceOutgoingPaymentAllocationModel(); + $this->payoutRequestAuditModel = new FinancePayoutRequestAuditModel(); } public function bankDeposits() @@ -3567,6 +3582,607 @@ class Finance extends BaseController return $builder->get()->getResultArray(); } + public function medicalStoreRequests() + { + if (! $this->canFinanceAny(['finance.workflow.view', 'finance.cash.accounts.accept', 'finance.cash.accounts.verify', 'finance.doctor_payout.manage'])) { + return $this->response->setStatusCode(403)->setJSON(['status' => 0, 'message' => 'Access denied']); + } + + if (! $this->db->tableExists('finance_payout_requests')) { + return $this->response->setJSON(['status' => 1, 'rows' => [], 'message' => 'Payout request table not available']); + } + + $status = trim((string) ($this->request->getGet('status') ?? '')); + $fromDate = $this->normalizeDateInput((string) ($this->request->getGet('from_date') ?? '')); + $toDate = $this->normalizeDateInput((string) ($this->request->getGet('to_date') ?? '')); + + $builder = $this->payoutRequestModel->where('request_type', 'medical_store_credit')->orderBy('id', 'DESC'); + if ($status !== '') { + $builder->where('status', $status); + } + if ($fromDate !== '') { + $builder->where('request_date >=', $fromDate); + } + if ($toDate !== '') { + $builder->where('request_date <=', $toDate); + } + + $rows = $builder->findAll(300); + + return $this->response->setJSON([ + 'status' => 1, + 'rows' => $rows, + ]); + } + + public function medicalStoreCreditAccount() + { + if (! $this->canFinanceAny(['finance.workflow.view', 'finance.cash.accounts.accept', 'finance.cash.accounts.verify', 'finance.doctor_payout.manage'])) { + return $this->response->setStatusCode(403)->setBody('Access denied'); + } + + $summary = [ + 'total_requested' => 0.0, + 'total_paid' => 0.0, + 'total_pending' => 0.0, + 'open_requests' => 0, + ]; + $rows = []; + + if ($this->db->tableExists('finance_payout_requests')) { + $summaryRow = $this->db->table('finance_payout_requests') + ->select('COALESCE(SUM(requested_amount),0) AS total_requested, COALESCE(SUM(paid_amount),0) AS total_paid, COALESCE(SUM(pending_amount),0) AS total_pending', false) + ->where('request_type', 'medical_store_credit') + ->get()->getRowArray(); + + $summary['total_requested'] = (float) ($summaryRow['total_requested'] ?? 0); + $summary['total_paid'] = (float) ($summaryRow['total_paid'] ?? 0); + $summary['total_pending'] = (float) ($summaryRow['total_pending'] ?? 0); + $summary['open_requests'] = $this->db->table('finance_payout_requests') + ->where('request_type', 'medical_store_credit') + ->whereIn('status', ['submitted', 'finance_review', 'approved', 'partially_paid']) + ->countAllResults(); + + $rows = $this->payoutRequestModel + ->where('request_type', 'medical_store_credit') + ->orderBy('id', 'DESC') + ->findAll(60); + } + + return view('finance/medical_store_credit_account', [ + 'summary' => $summary, + 'rows' => $rows, + ]); + } + + public function medicalStoreRequestsTable() + { + if (! $this->canFinanceAny(['finance.workflow.view', 'finance.cash.accounts.accept', 'finance.cash.accounts.verify', 'finance.doctor_payout.manage'])) { + return $this->response->setStatusCode(403)->setBody('Access denied'); + } + + $status = trim((string) ($this->request->getGet('status') ?? '')); + $rows = []; + + if ($this->db->tableExists('finance_payout_requests')) { + $builder = $this->payoutRequestModel + ->where('request_type', 'medical_store_credit') + ->orderBy('id', 'DESC'); + if ($status !== '') { + $builder->where('status', $status); + } + $rows = $builder->findAll(200); + } + + return view('finance/partials/medical_store_requests_table', [ + 'rows' => $rows, + ]); + } + + public function medicalStoreRequestLinesTable() + { + if (! $this->canFinanceAny(['finance.workflow.view', 'finance.cash.accounts.accept', 'finance.cash.accounts.verify', 'finance.doctor_payout.manage'])) { + return $this->response->setStatusCode(403)->setBody('Access denied'); + } + + $requestId = (int) ($this->request->getGet('request_id') ?? 0); + if ($requestId <= 0) { + return view('finance/partials/medical_store_request_lines_table', [ + 'request' => null, + 'lines' => [], + 'payments' => [], + ]); + } + + $request = $this->payoutRequestModel->find($requestId); + $lines = $this->db->tableExists('finance_payout_request_lines') + ? $this->payoutRequestLineModel->where('request_id', $requestId)->orderBy('line_order', 'ASC')->orderBy('id', 'ASC')->findAll() + : []; + $payments = $this->db->tableExists('finance_outgoing_payment_history') + ? $this->outgoingPaymentHistoryModel->where('request_id', $requestId)->orderBy('id', 'DESC')->findAll() + : []; + + return view('finance/partials/medical_store_request_lines_table', [ + 'request' => $request, + 'lines' => $lines, + 'payments' => $payments, + ]); + } + + public function medicalStoreRequestDetail($requestId = 0) + { + if (! $this->canFinanceAny(['finance.workflow.view', 'finance.cash.accounts.accept', 'finance.cash.accounts.verify', 'finance.doctor_payout.manage'])) { + return $this->response->setStatusCode(403)->setJSON(['status' => 0, 'message' => 'Access denied']); + } + + $requestId = (int) $requestId; + if ($requestId <= 0) { + return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Request ID required']); + } + + $request = $this->payoutRequestModel->find($requestId); + if (! is_array($request)) { + return $this->response->setStatusCode(404)->setJSON(['status' => 0, 'message' => 'Request not found']); + } + + $lines = []; + $payments = []; + if ($this->db->tableExists('finance_payout_request_lines')) { + $lines = $this->payoutRequestLineModel + ->where('request_id', $requestId) + ->orderBy('line_order', 'ASC') + ->orderBy('id', 'ASC') + ->findAll(); + } + if ($this->db->tableExists('finance_outgoing_payment_history')) { + $payments = $this->outgoingPaymentHistoryModel + ->where('request_id', $requestId) + ->orderBy('id', 'DESC') + ->findAll(); + } + + return $this->response->setJSON([ + 'status' => 1, + 'request' => $request, + 'lines' => $lines, + 'payments' => $payments, + ]); + } + + public function medicalStoreRequestReview() + { + return $this->transitionMedicalStoreRequestStatus('finance_review', ['submitted']); + } + + public function medicalStoreRequestApprove() + { + return $this->transitionMedicalStoreRequestStatus('approved', ['finance_review']); + } + + public function medicalStoreRequestReject() + { + return $this->transitionMedicalStoreRequestStatus('rejected', ['finance_review', 'submitted']); + } + + public function medicalStorePaymentCreate() + { + if (! $this->canFinanceAny(['finance.cash.accounts.accept', 'finance.cash.accounts.verify', 'finance.doctor_payout.manage'])) { + return $this->response->setStatusCode(403)->setJSON(['status' => 0, 'message' => 'Access denied']); + } + + $requestId = (int) ($this->request->getPost('request_id') ?? 0); + $amount = round((float) ($this->request->getPost('amount') ?? 0), 2); + $paymentMode = (int) ($this->request->getPost('payment_mode') ?? 1); + $remark = trim((string) ($this->request->getPost('remark') ?? '')); + + if ($requestId <= 0 || $amount <= 0) { + return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Request and amount are required.']); + } + + $request = $this->payoutRequestModel->find($requestId); + if (! is_array($request)) { + return $this->response->setStatusCode(404)->setJSON(['status' => 0, 'message' => 'Request not found.']); + } + + $status = (string) ($request['status'] ?? ''); + if (! in_array($status, ['approved', 'partially_paid'], true)) { + return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Only approved or partially paid requests can be paid.']); + } + + $pending = round((float) ($request['pending_amount'] ?? 0), 2); + if ($pending <= 0) { + return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Request has no pending amount.']); + } + if ($amount > $pending) { + return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Payment amount exceeds pending amount.']); + } + + $userId = $this->currentUserId(); + $userName = $this->currentUserName(); + $now = date('Y-m-d H:i:s'); + $paymentNo = 'MSP-' . date('YmdHis') . '-' . random_int(1000, 9999); + + $this->db->transStart(); + + $this->outgoingPaymentHistoryModel->insert([ + 'payment_no' => $paymentNo, + 'payout_type' => 'medical_store', + 'payee_label' => 'Medical Store Credit Settlement', + 'request_id' => $requestId, + 'payment_date' => $now, + 'amount' => $amount, + 'payment_mode' => $paymentMode, + 'pay_bank_id' => (int) ($this->request->getPost('pay_bank_id') ?? 0), + 'card_bank' => trim((string) ($this->request->getPost('card_bank') ?? '')), + 'card_remark' => trim((string) ($this->request->getPost('card_remark') ?? '')), + 'cust_card' => trim((string) ($this->request->getPost('cust_card') ?? '')), + 'card_tran_id' => trim((string) ($this->request->getPost('card_tran_id') ?? '')), + 'bankcard_machine' => trim((string) ($this->request->getPost('bankcard_machine') ?? '')), + 'insert_code' => trim((string) ($this->request->getPost('insert_code') ?? '')), + 'credit_debit' => 0, + 'remark' => $remark, + 'status' => 'paid', + 'created_by' => $userName, + 'created_by_id' => $userId, + 'created_at' => $now, + 'updated_at' => $now, + ]); + + $paymentId = (int) $this->outgoingPaymentHistoryModel->getInsertID(); + + $openLines = $this->payoutRequestLineModel + ->where('request_id', $requestId) + ->where('pending_amount >', 0) + ->orderBy('line_order', 'ASC') + ->orderBy('id', 'ASC') + ->findAll(); + + $remaining = $amount; + $allocOrder = 1; + foreach ($openLines as $line) { + $lineId = (int) ($line['id'] ?? 0); + $linePending = round((float) ($line['pending_amount'] ?? 0), 2); + if ($lineId <= 0 || $linePending <= 0 || $remaining <= 0) { + continue; + } + + $alloc = min($linePending, $remaining); + $alloc = round($alloc, 2); + + $this->outgoingPaymentAllocationModel->insert([ + 'payment_id' => $paymentId, + 'request_id' => $requestId, + 'request_line_id' => $lineId, + 'allocated_amount' => $alloc, + 'allocation_order' => $allocOrder, + 'allocation_note' => 'Auto FIFO allocation', + 'created_by' => $userName, + 'created_by_id' => $userId, + 'created_at' => $now, + ]); + + $newAllocated = round((float) ($line['allocated_amount'] ?? 0) + $alloc, 2); + $newPending = round(max(0, (float) ($line['line_amount'] ?? 0) - $newAllocated), 2); + $newLineStatus = $newPending <= 0 ? 'closed' : ($newAllocated > 0 ? 'partial' : 'open'); + + $this->payoutRequestLineModel->update($lineId, [ + 'allocated_amount' => $newAllocated, + 'pending_amount' => $newPending, + 'line_status' => $newLineStatus, + 'updated_at' => $now, + ]); + + $remaining = round($remaining - $alloc, 2); + $allocOrder++; + } + + if ($remaining > 0) { + $this->db->transRollback(); + return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Payment exceeds open line capacity.']); + } + + $this->recalculateMedicalStoreRequestTotals($requestId, $userId, $userName); + $this->logPayoutRequestAudit($requestId, 'payment_create', $status, null, 'Payment #' . $paymentNo . ' amount: ' . number_format($amount, 2, '.', '')); + + $this->db->transComplete(); + + if (! $this->db->transStatus()) { + return $this->response->setStatusCode(500)->setJSON(['status' => 0, 'message' => 'Failed to save payout payment.']); + } + + return $this->response->setJSON([ + 'status' => 1, + 'message' => 'Payment saved and allocated successfully.', + 'payment_id' => $paymentId, + 'payment_no' => $paymentNo, + ]); + } + + public function medicalStoreDashboardCard() + { + if (! $this->canFinanceAny(['finance.workflow.view', 'finance.cash.accounts.accept', 'finance.cash.accounts.verify', 'finance.doctor_payout.manage'])) { + return $this->response->setStatusCode(403)->setJSON(['status' => 0, 'message' => 'Access denied']); + } + + if (! $this->db->tableExists('finance_payout_requests')) { + return $this->response->setJSON([ + 'status' => 1, + 'summary' => [ + 'total_requested' => 0, + 'total_paid' => 0, + 'total_pending' => 0, + 'open_requests' => 0, + ], + ]); + } + + $summaryRow = $this->db->table('finance_payout_requests') + ->select('COALESCE(SUM(requested_amount),0) AS total_requested, COALESCE(SUM(paid_amount),0) AS total_paid, COALESCE(SUM(pending_amount),0) AS total_pending', false) + ->where('request_type', 'medical_store_credit') + ->get()->getRowArray(); + + $openRequests = $this->db->table('finance_payout_requests') + ->where('request_type', 'medical_store_credit') + ->whereIn('status', ['submitted', 'finance_review', 'approved', 'partially_paid']) + ->countAllResults(); + + return $this->response->setJSON([ + 'status' => 1, + 'summary' => [ + 'total_requested' => (float) ($summaryRow['total_requested'] ?? 0), + 'total_paid' => (float) ($summaryRow['total_paid'] ?? 0), + 'total_pending' => (float) ($summaryRow['total_pending'] ?? 0), + 'open_requests' => $openRequests, + ], + ]); + } + + public function bankAuditOutgoingPaymentsTable() + { + if (! $this->canFinance('finance.bank.audit')) { + return $this->response->setStatusCode(403)->setJSON(['status' => 0, 'message' => 'Access denied']); + } + + if (! $this->db->tableExists('finance_outgoing_payment_history')) { + return $this->response->setJSON(['status' => 1, 'rows' => []]); + } + + $status = trim((string) ($this->request->getGet('status') ?? 'unmatched')); + $fromDate = $this->normalizeDateInput((string) ($this->request->getGet('from_date') ?? '')); + $toDate = $this->normalizeDateInput((string) ($this->request->getGet('to_date') ?? '')); + + $q = $this->db->table('finance_outgoing_payment_history p') + ->select('p.id, p.payment_no, p.payment_date, p.amount, p.payment_mode, p.card_tran_id, p.card_remark, p.cust_card, p.remark, p.created_by, p.created_by_id, p.bank_reconcile_status, p.bank_statement_entry_id, p.bank_reconcile_batch_ref, p.bank_settlement_entry_id, p.insert_code, p.request_id, p.payout_type') + ->where('p.payment_mode', 2) + ->where('p.credit_debit', 0) + ->orderBy('p.id', 'DESC'); + + if ($status === 'unmatched') { + $q->groupStart()->where('p.bank_reconcile_status', null)->orWhere('p.bank_reconcile_status', '')->groupEnd(); + } elseif ($status === 'matched') { + $q->where('p.bank_reconcile_status', 'matched'); + } + + if ($fromDate !== '') { + $q->where('DATE(p.payment_date) >=', $fromDate); + } + if ($toDate !== '') { + $q->where('DATE(p.payment_date) <=', $toDate); + } + + return $this->response->setJSON([ + 'status' => 1, + 'rows' => $q->get()->getResultArray(), + ]); + } + + public function bankReconcileOutgoingSingleMatch() + { + if (! $this->canFinance('finance.bank.audit')) { + return $this->response->setStatusCode(403)->setJSON(['status' => 0, 'message' => 'Access denied']); + } + + $paymentId = (int) ($this->request->getPost('payment_id') ?? 0); + if ($paymentId <= 0) { + return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Payment ID required.']); + } + + $payment = $this->outgoingPaymentHistoryModel->find($paymentId); + if (! is_array($payment) || (int) ($payment['payment_mode'] ?? 0) !== 2 || (int) ($payment['credit_debit'] ?? 0) !== 0) { + return $this->response->setStatusCode(404)->setJSON(['status' => 0, 'message' => 'Outgoing bank payment not found.']); + } + + if ((string) ($payment['bank_reconcile_status'] ?? '') === 'matched') { + return $this->response->setStatusCode(409)->setJSON(['status' => 0, 'message' => 'Payment already matched.']); + } + + $this->outgoingPaymentHistoryModel->update($paymentId, [ + 'bank_reconcile_status' => 'matched', + 'bank_statement_entry_id' => null, + 'bank_reconcile_batch_ref' => null, + 'bank_settlement_entry_id' => null, + 'updated_at' => date('Y-m-d H:i:s'), + ]); + + return $this->response->setJSON(['status' => 1, 'message' => 'Outgoing payment marked as matched.']); + } + + public function bankReconcileOutgoingBatchMatch() + { + if (! $this->canFinance('finance.bank.audit')) { + return $this->response->setStatusCode(403)->setJSON(['status' => 0, 'message' => 'Access denied']); + } + + $paymentIds = $this->request->getPost('payment_ids'); + if (! is_array($paymentIds)) { + $paymentIds = []; + } + + $ids = []; + foreach ($paymentIds as $id) { + $v = (int) $id; + if ($v > 0) { + $ids[$v] = $v; + } + } + $ids = array_values($ids); + if ($ids === []) { + return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Select at least one payment.']); + } + + $rows = $this->outgoingPaymentHistoryModel + ->select('id, bank_reconcile_status') + ->whereIn('id', $ids) + ->where('payment_mode', 2) + ->where('credit_debit', 0) + ->findAll(); + + if ($rows === []) { + return $this->response->setStatusCode(404)->setJSON(['status' => 0, 'message' => 'No eligible outgoing bank payments found.']); + } + + $batchRef = 'OBR-' . date('YmdHis') . '-' . random_int(1000, 9999); + $matched = 0; + $now = date('Y-m-d H:i:s'); + foreach ($rows as $row) { + if ((string) ($row['bank_reconcile_status'] ?? '') === 'matched') { + continue; + } + + $this->outgoingPaymentHistoryModel->update((int) ($row['id'] ?? 0), [ + 'bank_reconcile_status' => 'matched', + 'bank_statement_entry_id' => null, + 'bank_reconcile_batch_ref' => $batchRef, + 'bank_settlement_entry_id' => null, + 'updated_at' => $now, + ]); + $matched++; + } + + return $this->response->setJSON([ + 'status' => 1, + 'message' => 'Matched outgoing payments: ' . $matched, + 'batch_ref' => $batchRef, + ]); + } + + private function transitionMedicalStoreRequestStatus(string $newStatus, array $allowedFrom) + { + if (! $this->canFinanceAny(['finance.workflow.view', 'finance.cash.accounts.accept', 'finance.cash.accounts.verify', 'finance.doctor_payout.manage'])) { + return $this->response->setStatusCode(403)->setJSON(['status' => 0, 'message' => 'Access denied']); + } + + $requestId = (int) ($this->request->getPost('request_id') ?? 0); + $remark = trim((string) ($this->request->getPost('remark') ?? '')); + if ($requestId <= 0) { + return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Request ID required.']); + } + + $request = $this->payoutRequestModel->find($requestId); + if (! is_array($request)) { + return $this->response->setStatusCode(404)->setJSON(['status' => 0, 'message' => 'Request not found.']); + } + + $oldStatus = (string) ($request['status'] ?? ''); + if (! in_array($oldStatus, $allowedFrom, true)) { + return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Invalid status transition from ' . $oldStatus . ' to ' . $newStatus . '.']); + } + + $now = date('Y-m-d H:i:s'); + $update = [ + 'status' => $newStatus, + 'updated_at' => $now, + 'updated_by' => $this->currentUserName(), + 'updated_by_id' => $this->currentUserId(), + ]; + if ($newStatus === 'finance_review') { + $update['finance_reviewed_at'] = $now; + } + if ($newStatus === 'approved') { + $update['approved_at'] = $now; + if ((float) ($request['approved_amount'] ?? 0) <= 0) { + $update['approved_amount'] = (float) ($request['requested_amount'] ?? 0); + } + } + + $this->payoutRequestModel->update($requestId, $update); + $this->logPayoutRequestAudit($requestId, 'status_change', $oldStatus, $newStatus, $remark); + + return $this->response->setJSON([ + 'status' => 1, + 'message' => 'Request status updated to ' . $newStatus . '.', + ]); + } + + private function recalculateMedicalStoreRequestTotals(int $requestId, ?int $userId = null, ?string $userName = null): void + { + $sumLines = $this->db->table('finance_payout_request_lines') + ->select('COALESCE(SUM(line_amount),0) AS line_total, COALESCE(SUM(allocated_amount),0) AS allocated_total, COALESCE(SUM(pending_amount),0) AS pending_total', false) + ->where('request_id', $requestId) + ->get()->getRowArray(); + + $requested = round((float) ($sumLines['line_total'] ?? 0), 2); + $paid = round((float) ($sumLines['allocated_total'] ?? 0), 2); + $pending = round((float) ($sumLines['pending_total'] ?? 0), 2); + + $newStatus = 'approved'; + if ($paid <= 0) { + $newStatus = 'approved'; + } elseif ($pending > 0) { + $newStatus = 'partially_paid'; + } else { + $newStatus = 'paid'; + } + + $update = [ + 'requested_amount' => $requested, + 'paid_amount' => $paid, + 'pending_amount' => $pending, + 'status' => $newStatus, + 'updated_at' => date('Y-m-d H:i:s'), + ]; + + if ($userName !== null) { + $update['updated_by'] = $userName; + } + if ($userId !== null) { + $update['updated_by_id'] = $userId; + } + if ($newStatus === 'paid') { + $update['closed_at'] = date('Y-m-d H:i:s'); + } + + $this->payoutRequestModel->update($requestId, $update); + } + + private function logPayoutRequestAudit(int $requestId, string $actionType, ?string $oldStatus, ?string $newStatus, string $note = ''): void + { + if (! $this->db->tableExists('finance_payout_request_audit')) { + return; + } + + $this->payoutRequestAuditModel->insert([ + 'request_id' => $requestId, + 'action_type' => $actionType, + 'old_status' => $oldStatus, + 'new_status' => $newStatus, + 'action_note' => $note !== '' ? $note : null, + 'action_by' => $this->currentUserName(), + 'action_by_id' => $this->currentUserId(), + 'action_at' => date('Y-m-d H:i:s'), + ]); + } + + private function currentUserId(): ?int + { + $user = function_exists('auth') ? auth()->user() : null; + if (! $user || ! isset($user->id)) { + return null; + } + + return (int) $user->id; + } + private function summarizePaymentHistoryRows(array $rows): array { $total = 0.0; diff --git a/app/Controllers/Medical.php b/app/Controllers/Medical.php index fda8795..041643f 100644 --- a/app/Controllers/Medical.php +++ b/app/Controllers/Medical.php @@ -11139,6 +11139,457 @@ class Medical extends BaseController ]; } + public function credit_payout_pool() + { + if ($deny = $this->ensurePharmacyAccess()) { + return $deny; + } + + $scope = strtolower(trim((string) ($this->request->getGet('scope') ?? 'all'))); + if (! in_array($scope, ['all', 'ipd', 'org'], true)) { + $scope = 'all'; + } + $fromDate = $this->normalizeDate((string) ($this->request->getGet('from_date') ?? '')); + $toDate = $this->normalizeDate((string) ($this->request->getGet('to_date') ?? '')); + + $baseSql = $this->buildMedicalCreditPoolBaseSql($scope, $fromDate, $toDate); + if ($baseSql === '') { + return $this->response->setJSON(['status' => 1, 'rows' => []]); + } + + $rows = $this->db->query('SELECT * FROM (' . $baseSql . ') t ORDER BY t.inv_date DESC, t.source_ref_id DESC LIMIT 300')->getResultArray(); + $rows = array_map(function (array $row): array { + return $this->normalizeMedicalCreditPoolRow($row); + }, $rows); + + return $this->response->setJSON([ + 'status' => 1, + 'rows' => $rows, + ]); + } + + public function credit_payout_pool_datatable() + { + if ($deny = $this->ensurePharmacyAccess()) { + return $deny; + } + + $draw = (int) ($this->request->getPost('draw') ?? $this->request->getGet('draw') ?? 1); + $start = max(0, (int) ($this->request->getPost('start') ?? $this->request->getGet('start') ?? 0)); + $length = (int) ($this->request->getPost('length') ?? $this->request->getGet('length') ?? 25); + if ($length <= 0 || $length > 500) { + $length = 25; + } + + $scope = strtolower(trim((string) ($this->request->getPost('scope') ?? $this->request->getGet('scope') ?? 'all'))); + if (! in_array($scope, ['all', 'ipd', 'org'], true)) { + $scope = 'all'; + } + + $groupBy = strtolower(trim((string) ($this->request->getPost('group_by') ?? $this->request->getGet('group_by') ?? 'none'))); + if (! in_array($groupBy, ['none', 'ipd', 'case'], true)) { + $groupBy = 'none'; + } + + $fromDate = $this->normalizeDate((string) ($this->request->getPost('from_date') ?? $this->request->getGet('from_date') ?? '')); + $toDate = $this->normalizeDate((string) ($this->request->getPost('to_date') ?? $this->request->getGet('to_date') ?? '')); + + $search = $this->request->getPost('search'); + if (! is_array($search)) { + $search = ['value' => (string) ($this->request->getGet('search') ?? '')]; + } + $searchValue = trim((string) ($search['value'] ?? '')); + + $order = $this->request->getPost('order'); + if (! is_array($order)) { + $order = []; + } + $orderColIndex = isset($order[0]['column']) ? (int) $order[0]['column'] : 6; + $orderDir = strtolower((string) ($order[0]['dir'] ?? 'desc')) === 'asc' ? 'ASC' : 'DESC'; + + $orderCols = [ + 1 => 'q.source_type', + 2 => 'q.invoice_code', + 3 => 'q.ipd_code', + 4 => 'q.case_code', + 5 => 'q.credit_category', + 6 => 'q.inv_date', + 7 => 'q.line_amount', + ]; + $orderBy = $orderCols[$orderColIndex] ?? 'q.inv_date'; + + $baseSql = $this->buildMedicalCreditPoolBaseSql($scope, $fromDate, $toDate); + if ($baseSql === '') { + return $this->response->setJSON([ + 'draw' => $draw, + 'recordsTotal' => 0, + 'recordsFiltered' => 0, + 'data' => [], + ]); + } + + $whereParts = ['q.line_amount > 0']; + + if ($this->db->tableExists('finance_payout_request_lines') && $this->db->tableExists('finance_payout_requests')) { + $whereParts[] = "NOT EXISTS ( + SELECT 1 + FROM finance_payout_request_lines pl + JOIN finance_payout_requests pr ON pr.id = pl.request_id + WHERE pl.source_type = q.source_type + AND pl.source_ref_id = q.source_ref_id + AND pr.request_type = 'medical_store_credit' + AND pr.status IN ('submitted','finance_review','approved','partially_paid') + )"; + } + + if ($searchValue !== '') { + $esc = $this->db->escapeLikeString($searchValue); + $like = $this->db->escape('%' . $esc . '%'); + $whereParts[] = "( + q.source_type LIKE {$like} ESCAPE '!' + OR CAST(q.source_ref_id AS CHAR) LIKE {$like} ESCAPE '!' + OR q.invoice_code LIKE {$like} ESCAPE '!' + OR q.ipd_code LIKE {$like} ESCAPE '!' + OR q.case_code LIKE {$like} ESCAPE '!' + OR q.credit_category LIKE {$like} ESCAPE '!' + )"; + } + + $whereSql = ' WHERE ' . implode(' AND ', $whereParts); + $countTotalSql = 'SELECT COUNT(1) AS cnt FROM (' . $baseSql . ') q WHERE q.line_amount > 0'; + + $countFilteredSql = 'SELECT COUNT(1) AS cnt FROM (' . $baseSql . ') q' . $whereSql; + $orderPrefix = ''; + if ($groupBy === 'ipd') { + $orderPrefix = 'COALESCE(NULLIF(q.ipd_code,\'\'), \'ZZZ\') ASC, '; + } elseif ($groupBy === 'case') { + $orderPrefix = 'COALESCE(NULLIF(q.case_code,\'\'), \'ZZZ\') ASC, '; + } + + $dataSql = 'SELECT q.* FROM (' . $baseSql . ') q' . $whereSql + . ' ORDER BY ' . $orderPrefix . $orderBy . ' ' . $orderDir . ', q.source_ref_id DESC' + . ' LIMIT ' . $start . ', ' . $length; + + $recordsTotal = (int) (($this->db->query($countTotalSql)->getRowArray()['cnt'] ?? 0)); + $recordsFiltered = (int) (($this->db->query($countFilteredSql)->getRowArray()['cnt'] ?? 0)); + $rows = $this->db->query($dataSql)->getResultArray(); + + $data = array_map(function (array $row): array { + return $this->normalizeMedicalCreditPoolRow($row); + }, $rows); + + return $this->response->setJSON([ + 'draw' => $draw, + 'recordsTotal' => $recordsTotal, + 'recordsFiltered' => $recordsFiltered, + 'data' => $data, + ]); + } + + public function credit_payout_request() + { + if ($deny = $this->ensurePharmacyAccess()) { + return $deny; + } + + $rows = []; + if ($this->db->tableExists('finance_payout_requests')) { + $rows = $this->db->table('finance_payout_requests') + ->where('request_type', 'medical_store_credit') + ->orderBy('id', 'DESC') + ->get(80) + ->getResultArray(); + } + + return view('medical/credit_payout_request', [ + 'rows' => $rows, + ]); + } + + public function credit_payout_request_create() + { + if ($deny = $this->ensurePharmacyAccess()) { + return $deny; + } + + if (! $this->db->tableExists('finance_payout_requests') || ! $this->db->tableExists('finance_payout_request_lines')) { + return $this->response->setStatusCode(500)->setJSON(['status' => 0, 'message' => 'Finance payout tables not available.']); + } + + $requestDate = $this->normalizeDate((string) ($this->request->getPost('request_date') ?? date('Y-m-d'))); + $remarks = trim((string) ($this->request->getPost('remarks') ?? '')); + $lineItemsRaw = $this->request->getPost('line_items'); + $lineItems = []; + if (is_string($lineItemsRaw)) { + $decoded = json_decode($lineItemsRaw, true); + if (is_array($decoded)) { + $lineItems = $decoded; + } + } elseif (is_array($lineItemsRaw)) { + $lineItems = $lineItemsRaw; + } + + if ($requestDate === '' || $lineItems === []) { + return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Request date and line items are required.']); + } + + $requestNo = 'MSR-' . date('YmdHis') . '-' . random_int(1000, 9999); + $now = date('Y-m-d H:i:s'); + $userLabel = $this->pharmacyActorLabel(); + $user = service('auth')->user(); + $userId = $user && isset($user->id) ? (int) $user->id : null; + + $this->db->transStart(); + + $this->db->table('finance_payout_requests')->insert([ + 'request_no' => $requestNo, + 'request_type' => 'medical_store_credit', + 'requester_unit' => 'medical_store', + 'beneficiary_unit' => 'hospital', + 'request_date' => $requestDate, + 'requested_amount' => 0, + 'approved_amount' => 0, + 'paid_amount' => 0, + 'pending_amount' => 0, + 'status' => 'submitted', + 'remarks' => $remarks, + 'submitted_at' => $now, + 'created_by' => $userLabel, + 'created_by_id' => $userId, + 'updated_by' => $userLabel, + 'updated_by_id' => $userId, + 'created_at' => $now, + 'updated_at' => $now, + ]); + $requestId = (int) $this->db->insertID(); + + $lineOrder = 1; + $total = 0.0; + foreach ($lineItems as $line) { + $sourceType = trim((string) ($line['source_type'] ?? '')); + $sourceRefId = (int) ($line['source_ref_id'] ?? 0); + $lineAmount = round((float) ($line['line_amount'] ?? 0), 2); + if ($sourceType === '' || $sourceRefId <= 0 || $lineAmount <= 0) { + continue; + } + + $this->db->table('finance_payout_request_lines')->insert([ + 'request_id' => $requestId, + 'source_domain' => 'medical_store', + 'source_type' => $sourceType, + 'source_ref_id' => $sourceRefId, + 'invoice_id' => (int) ($line['invoice_id'] ?? 0) ?: null, + 'invoice_code' => trim((string) ($line['invoice_code'] ?? '')), + 'ipd_id' => (int) ($line['ipd_id'] ?? 0) ?: null, + 'ipd_code' => trim((string) ($line['ipd_code'] ?? '')), + 'case_id' => (int) ($line['case_id'] ?? 0) ?: null, + 'case_code' => trim((string) ($line['case_code'] ?? '')), + 'credit_category' => trim((string) ($line['credit_category'] ?? 'ipd_credit')), + 'line_amount' => $lineAmount, + 'allocated_amount' => 0, + 'pending_amount' => $lineAmount, + 'line_status' => 'open', + 'line_remarks' => trim((string) ($line['line_remarks'] ?? '')), + 'line_order' => $lineOrder, + 'created_at' => $now, + 'updated_at' => $now, + ]); + + $total += $lineAmount; + $lineOrder++; + } + + if ($total <= 0) { + $this->db->transRollback(); + return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'No valid line item found.']); + } + + $this->db->table('finance_payout_requests')->where('id', $requestId)->update([ + 'requested_amount' => round($total, 2), + 'pending_amount' => round($total, 2), + 'updated_at' => $now, + ]); + + if ($this->db->tableExists('finance_payout_request_audit')) { + $this->db->table('finance_payout_request_audit')->insert([ + 'request_id' => $requestId, + 'action_type' => 'request_create', + 'old_status' => null, + 'new_status' => 'submitted', + 'action_note' => $remarks !== '' ? $remarks : 'Medical store payout request submitted.', + 'action_by' => $userLabel, + 'action_by_id' => $userId, + 'action_at' => $now, + ]); + } + + $this->db->transComplete(); + if (! $this->db->transStatus()) { + return $this->response->setStatusCode(500)->setJSON(['status' => 0, 'message' => 'Failed to create payout request.']); + } + + return $this->response->setJSON([ + 'status' => 1, + 'message' => 'Payout request created successfully.', + 'request_id' => $requestId, + 'request_no' => $requestNo, + ]); + } + + public function credit_payout_requests() + { + if ($deny = $this->ensurePharmacyAccess()) { + return $deny; + } + + if (! $this->db->tableExists('finance_payout_requests')) { + return $this->response->setJSON(['status' => 1, 'rows' => []]); + } + + $status = trim((string) ($this->request->getGet('status') ?? '')); + $builder = $this->db->table('finance_payout_requests') + ->where('request_type', 'medical_store_credit') + ->orderBy('id', 'DESC'); + if ($status !== '') { + $builder->where('status', $status); + } + + return $this->response->setJSON([ + 'status' => 1, + 'rows' => $builder->get()->getResultArray(), + ]); + } + + public function credit_payout_request_detail($requestId = 0) + { + if ($deny = $this->ensurePharmacyAccess()) { + return $deny; + } + + $requestId = (int) $requestId; + if ($requestId <= 0) { + return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Request ID required']); + } + + $request = $this->db->table('finance_payout_requests')->where('id', $requestId)->get()->getRowArray(); + if (! is_array($request)) { + return $this->response->setStatusCode(404)->setJSON(['status' => 0, 'message' => 'Request not found']); + } + + $lines = $this->db->table('finance_payout_request_lines') + ->where('request_id', $requestId) + ->orderBy('line_order', 'ASC') + ->orderBy('id', 'ASC') + ->get()->getResultArray(); + + $payments = []; + if ($this->db->tableExists('finance_outgoing_payment_history')) { + $payments = $this->db->table('finance_outgoing_payment_history') + ->where('request_id', $requestId) + ->orderBy('id', 'DESC') + ->get()->getResultArray(); + } + + return $this->response->setJSON([ + 'status' => 1, + 'request' => $request, + 'lines' => $lines, + 'payments' => $payments, + ]); + } + + private function normalizeMedicalCreditPoolRow(array $row): array + { + return [ + 'source_type' => trim((string) ($row['source_type'] ?? '')), + 'source_ref_id' => (int) ($row['source_ref_id'] ?? 0), + 'invoice_id' => (int) ($row['invoice_id'] ?? 0), + 'invoice_code' => trim((string) ($row['invoice_code'] ?? '')), + 'ipd_id' => (int) ($row['ipd_id'] ?? 0), + 'ipd_code' => trim((string) ($row['ipd_code'] ?? '')), + 'case_id' => (int) ($row['case_id'] ?? 0), + 'case_code' => trim((string) ($row['case_code'] ?? '')), + 'credit_category' => trim((string) ($row['credit_category'] ?? 'ipd_credit')), + 'line_amount' => round((float) ($row['line_amount'] ?? 0), 2), + 'inv_date' => ! empty($row['inv_date']) ? date('Y-m-d', strtotime((string) $row['inv_date'])) : null, + ]; + } + + private function buildMedicalCreditPoolBaseSql(string $scope, string $fromDate, string $toDate): string + { + $parts = []; + + $hasInvMedGroup = $this->db->tableExists('inv_med_group'); + $hasInvMedMaster = $this->db->tableExists('invoice_med_master'); + $hasIpdMaster = $this->db->tableExists('ipd_master'); + $hasOrgCase = $this->db->tableExists('organization_case_master'); + + $dateCondIpd = ''; + $dateCondOrg = ''; + if ($fromDate !== '') { + $dateCondIpd .= ' AND DATE(m.inv_date) >= ' . $this->db->escape($fromDate); + $dateCondOrg .= ' AND DATE(m.inv_date) >= ' . $this->db->escape($fromDate); + } + if ($toDate !== '') { + $dateCondIpd .= ' AND DATE(m.inv_date) <= ' . $this->db->escape($toDate); + $dateCondOrg .= ' AND DATE(m.inv_date) <= ' . $this->db->escape($toDate); + } + + if (($scope === 'all' || $scope === 'ipd') && $hasInvMedGroup && $hasInvMedMaster && $hasIpdMaster) { + $parts[] = " + SELECT + 'inv_med_group' AS source_type, + g.med_group_id AS source_ref_id, + MAX(m.id) AS invoice_id, + MAX(COALESCE(m.inv_med_code, '')) AS invoice_code, + MAX(COALESCE(m.ipd_id, 0)) AS ipd_id, + MAX(COALESCE(m.ipd_code, '')) AS ipd_code, + MAX(COALESCE(m.case_id, 0)) AS case_id, + '' AS case_code, + IF(g.med_type = 3, 'ipd_package', 'ipd_credit') AS credit_category, + DATE(MAX(m.inv_date)) AS inv_date, + ROUND(COALESCE(NULLIF(MAX(COALESCE(g.net_amount, 0)), 0), NULLIF(SUM(COALESCE(m.net_amount, 0)), 0), SUM(COALESCE(m.gross_amount, 0))), 2) AS line_amount + FROM inv_med_group g + JOIN invoice_med_master m ON m.med_group_id = g.med_group_id + JOIN ipd_master ip ON ip.id = g.ipd_id + WHERE g.med_type IN (2, 3) + AND IFNULL(ip.ipd_status, 0) <> 0 + AND IFNULL(m.sale_return, 0) = 0 + {$dateCondIpd} + GROUP BY g.med_group_id, g.med_type + "; + } + + if (($scope === 'all' || $scope === 'org') && $hasInvMedMaster && $hasOrgCase) { + $orgFields = $this->db->getFieldNames('organization_case_master') ?? []; + $caseCodeExpr = in_array('case_id_code', $orgFields, true) ? "COALESCE(o.case_id_code, '')" : "''"; + + $parts[] = " + SELECT + 'invoice_med_master' AS source_type, + m.id AS source_ref_id, + m.id AS invoice_id, + COALESCE(m.inv_med_code, '') AS invoice_code, + COALESCE(m.ipd_id, 0) AS ipd_id, + COALESCE(m.ipd_code, '') AS ipd_code, + COALESCE(m.case_id, 0) AS case_id, + {$caseCodeExpr} AS case_code, + 'org_case_credit' AS credit_category, + DATE(m.inv_date) AS inv_date, + ROUND(COALESCE(NULLIF(m.net_amount, 0), m.gross_amount, 0), 2) AS line_amount + FROM invoice_med_master m + JOIN organization_case_master o ON o.id = m.case_id + WHERE m.case_id > 0 + AND IFNULL(m.case_credit, 0) = 1 + AND IFNULL(o.status, 0) = 2 + AND IFNULL(m.sale_return, 0) = 0 + {$dateCondOrg} + "; + } + + return $parts === [] ? '' : implode(' UNION ALL ', $parts); + } + public function master() { if ($deny = $this->ensurePharmacyAccess()) { diff --git a/app/Database/Migrations/2026-04-30-000105_CreateFinanceMedicalPayoutWorkflowTables.php b/app/Database/Migrations/2026-04-30-000105_CreateFinanceMedicalPayoutWorkflowTables.php new file mode 100644 index 0000000..b604d0e --- /dev/null +++ b/app/Database/Migrations/2026-04-30-000105_CreateFinanceMedicalPayoutWorkflowTables.php @@ -0,0 +1,518 @@ +db->query('SHOW TABLES LIKE ' . $this->db->escape($table))->getRowArray(); + + return ! empty($row); + } + + public function up() + { + if (! $this->hasTable('finance_payout_requests')) { + $this->forge->addField([ + 'id' => [ + 'type' => 'BIGINT', + 'constraint' => 20, + 'unsigned' => true, + 'auto_increment' => true, + ], + 'request_no' => [ + 'type' => 'VARCHAR', + 'constraint' => 40, + 'null' => false, + ], + 'request_type' => [ + 'type' => 'VARCHAR', + 'constraint' => 30, + 'default' => 'medical_store_credit', + ], + 'requester_unit' => [ + 'type' => 'VARCHAR', + 'constraint' => 30, + 'default' => 'medical_store', + ], + 'beneficiary_unit' => [ + 'type' => 'VARCHAR', + 'constraint' => 30, + 'default' => 'hospital', + ], + 'request_date' => [ + 'type' => 'DATE', + 'null' => false, + ], + 'requested_amount' => [ + 'type' => 'DECIMAL', + 'constraint' => '12,2', + 'default' => 0, + ], + 'approved_amount' => [ + 'type' => 'DECIMAL', + 'constraint' => '12,2', + 'default' => 0, + ], + 'paid_amount' => [ + 'type' => 'DECIMAL', + 'constraint' => '12,2', + 'default' => 0, + ], + 'pending_amount' => [ + 'type' => 'DECIMAL', + 'constraint' => '12,2', + 'default' => 0, + ], + 'status' => [ + 'type' => 'VARCHAR', + 'constraint' => 30, + 'default' => 'draft', + ], + 'priority' => [ + 'type' => 'VARCHAR', + 'constraint' => 20, + 'null' => true, + ], + 'remarks' => [ + 'type' => 'TEXT', + 'null' => true, + ], + 'submitted_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + 'finance_reviewed_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + 'approved_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + 'closed_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + 'created_by' => [ + 'type' => 'VARCHAR', + 'constraint' => 120, + 'null' => true, + ], + 'created_by_id' => [ + 'type' => 'INT', + 'constraint' => 11, + 'null' => true, + ], + 'updated_by' => [ + 'type' => 'VARCHAR', + 'constraint' => 120, + 'null' => true, + ], + 'updated_by_id' => [ + 'type' => 'INT', + 'constraint' => 11, + 'null' => true, + ], + 'created_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + 'updated_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + ]); + $this->forge->addKey('id', true); + $this->forge->addUniqueKey('request_no'); + $this->forge->addKey(['status', 'request_date']); + $this->forge->addKey(['request_type', 'status']); + $this->forge->createTable('finance_payout_requests', true); + } + + if (! $this->hasTable('finance_payout_request_lines')) { + $this->forge->addField([ + 'id' => [ + 'type' => 'BIGINT', + 'constraint' => 20, + 'unsigned' => true, + 'auto_increment' => true, + ], + 'request_id' => [ + 'type' => 'BIGINT', + 'constraint' => 20, + 'unsigned' => true, + 'null' => false, + ], + 'source_domain' => [ + 'type' => 'VARCHAR', + 'constraint' => 30, + 'default' => 'medical_store', + ], + 'source_type' => [ + 'type' => 'VARCHAR', + 'constraint' => 30, + 'null' => false, + ], + 'source_ref_id' => [ + 'type' => 'BIGINT', + 'constraint' => 20, + 'unsigned' => true, + 'null' => false, + ], + 'invoice_id' => [ + 'type' => 'BIGINT', + 'constraint' => 20, + 'unsigned' => true, + 'null' => true, + ], + 'invoice_code' => [ + 'type' => 'VARCHAR', + 'constraint' => 50, + 'null' => true, + ], + 'ipd_id' => [ + 'type' => 'BIGINT', + 'constraint' => 20, + 'unsigned' => true, + 'null' => true, + ], + 'ipd_code' => [ + 'type' => 'VARCHAR', + 'constraint' => 50, + 'null' => true, + ], + 'case_id' => [ + 'type' => 'BIGINT', + 'constraint' => 20, + 'unsigned' => true, + 'null' => true, + ], + 'case_code' => [ + 'type' => 'VARCHAR', + 'constraint' => 50, + 'null' => true, + ], + 'credit_category' => [ + 'type' => 'VARCHAR', + 'constraint' => 30, + 'null' => false, + ], + 'line_amount' => [ + 'type' => 'DECIMAL', + 'constraint' => '12,2', + 'default' => 0, + ], + 'allocated_amount' => [ + 'type' => 'DECIMAL', + 'constraint' => '12,2', + 'default' => 0, + ], + 'pending_amount' => [ + 'type' => 'DECIMAL', + 'constraint' => '12,2', + 'default' => 0, + ], + 'line_status' => [ + 'type' => 'VARCHAR', + 'constraint' => 20, + 'default' => 'open', + ], + 'line_remarks' => [ + 'type' => 'VARCHAR', + 'constraint' => 250, + 'null' => true, + ], + 'line_order' => [ + 'type' => 'INT', + 'constraint' => 11, + 'default' => 0, + ], + 'created_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + 'updated_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + ]); + $this->forge->addKey('id', true); + $this->forge->addKey('request_id'); + $this->forge->addKey(['line_status', 'pending_amount']); + $this->forge->addKey(['source_type', 'source_ref_id']); + $this->forge->addUniqueKey(['request_id', 'source_type', 'source_ref_id']); + $this->forge->createTable('finance_payout_request_lines', true); + } + + if (! $this->hasTable('finance_outgoing_payment_history')) { + $this->forge->addField([ + 'id' => [ + 'type' => 'BIGINT', + 'constraint' => 20, + 'unsigned' => true, + 'auto_increment' => true, + ], + 'payment_no' => [ + 'type' => 'VARCHAR', + 'constraint' => 40, + 'null' => false, + ], + 'payout_type' => [ + 'type' => 'VARCHAR', + 'constraint' => 30, + 'null' => false, + ], + 'payee_label' => [ + 'type' => 'VARCHAR', + 'constraint' => 150, + 'null' => true, + ], + 'request_id' => [ + 'type' => 'BIGINT', + 'constraint' => 20, + 'unsigned' => true, + 'null' => true, + ], + 'payment_date' => [ + 'type' => 'DATETIME', + 'null' => false, + ], + 'amount' => [ + 'type' => 'DECIMAL', + 'constraint' => '12,2', + 'default' => 0, + ], + 'payment_mode' => [ + 'type' => 'INT', + 'constraint' => 11, + 'default' => 1, + ], + 'pay_bank_id' => [ + 'type' => 'INT', + 'constraint' => 11, + 'default' => 0, + ], + 'card_bank' => [ + 'type' => 'VARCHAR', + 'constraint' => 80, + 'null' => true, + ], + 'card_remark' => [ + 'type' => 'VARCHAR', + 'constraint' => 80, + 'null' => true, + ], + 'cust_card' => [ + 'type' => 'VARCHAR', + 'constraint' => 80, + 'null' => true, + ], + 'card_tran_id' => [ + 'type' => 'VARCHAR', + 'constraint' => 120, + 'null' => true, + ], + 'bankcard_machine' => [ + 'type' => 'VARCHAR', + 'constraint' => 120, + 'null' => true, + ], + 'insert_code' => [ + 'type' => 'VARCHAR', + 'constraint' => 40, + 'null' => true, + ], + 'credit_debit' => [ + 'type' => 'INT', + 'constraint' => 11, + 'default' => 0, + ], + 'remark' => [ + 'type' => 'TEXT', + 'null' => true, + ], + 'status' => [ + 'type' => 'VARCHAR', + 'constraint' => 20, + 'default' => 'paid', + ], + 'bank_reconcile_status' => [ + 'type' => 'VARCHAR', + 'constraint' => 20, + 'null' => true, + ], + 'bank_statement_entry_id' => [ + 'type' => 'BIGINT', + 'constraint' => 20, + 'unsigned' => true, + 'null' => true, + ], + 'bank_reconcile_batch_ref' => [ + 'type' => 'VARCHAR', + 'constraint' => 50, + 'null' => true, + ], + 'bank_settlement_entry_id' => [ + 'type' => 'BIGINT', + 'constraint' => 20, + 'unsigned' => true, + 'null' => true, + ], + 'created_by' => [ + 'type' => 'VARCHAR', + 'constraint' => 120, + 'null' => true, + ], + 'created_by_id' => [ + 'type' => 'INT', + 'constraint' => 11, + 'null' => true, + ], + 'created_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + 'updated_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + ]); + $this->forge->addKey('id', true); + $this->forge->addUniqueKey('payment_no'); + $this->forge->addKey(['payout_type', 'payment_date']); + $this->forge->addKey(['payment_mode', 'credit_debit']); + $this->forge->addKey('bank_reconcile_status'); + $this->forge->addKey('request_id'); + $this->forge->createTable('finance_outgoing_payment_history', true); + } + + if (! $this->hasTable('finance_outgoing_payment_allocations')) { + $this->forge->addField([ + 'id' => [ + 'type' => 'BIGINT', + 'constraint' => 20, + 'unsigned' => true, + 'auto_increment' => true, + ], + 'payment_id' => [ + 'type' => 'BIGINT', + 'constraint' => 20, + 'unsigned' => true, + 'null' => false, + ], + 'request_id' => [ + 'type' => 'BIGINT', + 'constraint' => 20, + 'unsigned' => true, + 'null' => false, + ], + 'request_line_id' => [ + 'type' => 'BIGINT', + 'constraint' => 20, + 'unsigned' => true, + 'null' => false, + ], + 'allocated_amount' => [ + 'type' => 'DECIMAL', + 'constraint' => '12,2', + 'default' => 0, + ], + 'allocation_order' => [ + 'type' => 'INT', + 'constraint' => 11, + 'default' => 0, + ], + 'allocation_note' => [ + 'type' => 'VARCHAR', + 'constraint' => 250, + 'null' => true, + ], + 'created_by' => [ + 'type' => 'VARCHAR', + 'constraint' => 120, + 'null' => true, + ], + 'created_by_id' => [ + 'type' => 'INT', + 'constraint' => 11, + 'null' => true, + ], + 'created_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + ]); + $this->forge->addKey('id', true); + $this->forge->addKey('payment_id'); + $this->forge->addKey(['request_id', 'request_line_id']); + $this->forge->addUniqueKey(['payment_id', 'request_line_id']); + $this->forge->createTable('finance_outgoing_payment_allocations', true); + } + + if (! $this->hasTable('finance_payout_request_audit')) { + $this->forge->addField([ + 'id' => [ + 'type' => 'BIGINT', + 'constraint' => 20, + 'unsigned' => true, + 'auto_increment' => true, + ], + 'request_id' => [ + 'type' => 'BIGINT', + 'constraint' => 20, + 'unsigned' => true, + 'null' => false, + ], + 'action_type' => [ + 'type' => 'VARCHAR', + 'constraint' => 40, + 'null' => false, + ], + 'old_status' => [ + 'type' => 'VARCHAR', + 'constraint' => 30, + 'null' => true, + ], + 'new_status' => [ + 'type' => 'VARCHAR', + 'constraint' => 30, + 'null' => true, + ], + 'action_note' => [ + 'type' => 'TEXT', + 'null' => true, + ], + 'action_by' => [ + 'type' => 'VARCHAR', + 'constraint' => 120, + 'null' => true, + ], + 'action_by_id' => [ + 'type' => 'INT', + 'constraint' => 11, + 'null' => true, + ], + 'action_at' => [ + 'type' => 'DATETIME', + 'null' => false, + ], + ]); + $this->forge->addKey('id', true); + $this->forge->addKey(['request_id', 'action_at']); + $this->forge->createTable('finance_payout_request_audit', true); + } + } + + public function down() + { + $this->forge->dropTable('finance_payout_request_audit', true); + $this->forge->dropTable('finance_outgoing_payment_allocations', true); + $this->forge->dropTable('finance_outgoing_payment_history', true); + $this->forge->dropTable('finance_payout_request_lines', true); + $this->forge->dropTable('finance_payout_requests', true); + } +} diff --git a/app/Models/FinanceOutgoingPaymentAllocationModel.php b/app/Models/FinanceOutgoingPaymentAllocationModel.php new file mode 100644 index 0000000..4fc8919 --- /dev/null +++ b/app/Models/FinanceOutgoingPaymentAllocationModel.php @@ -0,0 +1,25 @@ +