Unposted Queue

{{ number_format($payments->total()) }} row(s)

@if (! empty($unpostedAutoMatchUrl) && ($canAutoMatch ?? false))
@csrf @foreach (request()->only(['q', 'channel', 'from', 'to', 'per_page', 'match_reason', 'match_status', 'payment_origin', 'paybill', 'branch_id']) as $key => $value) @if ($value !== null && $value !== '') @endif @endforeach
@endif @if (! empty($unpostedLiftCautionUrl) && ($canLiftCaution ?? false))
@csrf @foreach (request()->only(['q', 'channel', 'from', 'to', 'per_page', 'match_reason', 'match_status', 'payment_origin', 'paybill', 'branch_id']) as $key => $value) @if ($value !== null && $value !== '') @endif @endforeach
@endif

Match & Post

Starting…

Working 0%

/ payments · posted · review · unmatched · failed

Stay on this page until Match & Post finishes. Leaving will interrupt the run.

@forelse ($payments as $p) @php $client360 = $client360ByPayment[$p->id] ?? null; $isReversalSuspense = $p->isReversalSuspenseReallocation(); $isReturnedForReallocation = $p->isReturnedForReallocation(); $unpostedRowUrl = ($client360 && ! $isReturnedForReallocation) ? route('loan.clients.show', $client360) : null; $matchStatusLabel = match ((string) ($p->match_status ?? 'pending')) { 'matched' => 'Matched', 'unmatched' => 'Not matched', 'review_required' => 'Needs review', 'pending' => 'Pending', default => str($p->match_status ?? 'pending')->headline(), }; $statementMatchLabel = match ((string) ($p->match_reason ?? '')) { 'statement_reference_open_loan_match' => 'Linked to a loan', 'application_fee_suspense_hold' => 'Held for application fee', 'statement_client_matched_no_open_loan' => 'Client found (no active loan)', 'corrected_client_matched_no_open_loan' => 'Client matched (no active loan)', 'statement_client_multiple_open_loans' => 'Client has more than one loan', 'statement_client_not_found' => 'Client not found', 'statement_client_ambiguous' => 'More than one client matches', 'statement_reference_missing' => 'Payment reference missing', 'statement_reference_invalid' => 'Payment reference invalid', 'Already applied in migrated loan balance — do not post again' => 'Already in loan balance', default => null, }; $isStatementRecovery = ($p->payment_origin ?? '') === 'mpesa_statement_reconciliation' || (bool) data_get($p->meta_json, 'recovered_via_reconciliation'); $isDarajaC2b = data_get($p->meta_json, 'source') === 'daraja_c2b' || filled(data_get($p->meta_json, 'mpesa_c2b_callback_id')); $matchBadgeClass = match ($p->match_status) { 'matched' => 'border-emerald-200 bg-emerald-100 text-emerald-700', 'unmatched' => 'border-amber-200 bg-amber-100 text-amber-700', 'review_required' => 'border-purple-200 bg-purple-100 text-purple-700', default => 'border-slate-200 bg-slate-100 text-slate-700', }; $isCautioned = $p->status === \App\Models\LoanBookPayment::STATUS_EXCEPTION; $branchLabel = $p->loanBranch?->name ?? $client360?->branch ?? $p->loan?->loanClient?->branch ?? '—'; $providerLabel = str($p->provider ?? $p->channel ?? '—')->headline(); $receiptLabel = $p->mpesa_receipt_number ?? $p->reference ?? '—'; $accountNoLabel = $p->incomingAccountNumber() ?? '—'; // Reversal / returned rows keep the original M-Pesa payer identity, not the assigned loan client. if ($isReturnedForReallocation) { $payerLabel = $p->payer_name ?: '—'; $phoneLabel = $p->displayPayerMsisdn(); } else { $payerLabel = $client360?->full_name ?? ($p->match_status === 'matched' || $p->match_status === 'review_required' ? ($p->loan?->loanClient?->full_name ?: ($p->payer_name ?: '—')) : ($p->payer_name ?: '—')); $phoneLabel = $client360?->phone ?: $p->displayPayerMsisdn(); } $rowClasses = $isCautioned ? 'bg-amber-50/70' : (optional($p->transaction_at)?->lt(now()->subDay()) ? 'bg-amber-50/40' : ''); @endphp @empty @endforelse
Receipt Date / Time Amount Channel Paybill Account no. Client Phone Branch Match Status Action
@if (filled($unpostedRowUrl)) {{ $receiptLabel }} @else {{ $receiptLabel }} @endif {{ optional($p->transaction_at)->format('Y-m-d H:i') ?? '—' }} {{ $p->currency }} {{ number_format((float) $p->amount, 2) }} {{ $providerLabel }} {{ $p->business_shortcode ?? '—' }} {{ $accountNoLabel }} {{ $payerLabel }} {{ $phoneLabel }} {{ $branchLabel }}
@if ($isCautioned) Caution @endif {{ $matchStatusLabel }} @if ($statementMatchLabel) {{ $statementMatchLabel }} @endif @if ($isStatementRecovery) Recovered from M-Pesa statement @elseif ($isDarajaC2b) Received from M-Pesa @elseif ($isReversalSuspense) Returned after reversal @endif
@if ((str_ends_with((string) $p->channel, '_sms_unmatched') || str_ends_with((string) $p->channel, '_sms_disbursement_unmatched')) && is_null($p->loan_book_loan_id)) @php $smsSuggestedLoanId = (int) ($suggestedLoanByPayment[$p->id] ?? 0); $smsSuggestedLoanLabel = $smsSuggestedLoanId > 0 ? (($assignableLoanOptions ?? collect())[$smsSuggestedLoanId] ?? null) : null; @endphp
@csrf @include('loan.payments.partials.assignable-loan-select', [ 'name' => 'loan_book_loan_id', 'fieldId' => 'sms_assign_loan_'.$p->id, 'required' => true, 'compact' => true, 'containerClass' => 'min-w-[14rem] flex-1', 'emptyLabel' => 'Assign loan...', 'placeholder' => 'Search loan #, client name, or ID…', 'selectedId' => $smsSuggestedLoanLabel ? $smsSuggestedLoanId : null, 'selectedLabel' => $smsSuggestedLoanLabel, 'paymentId' => $p->id, ])
@endif @include('loan.payments.partials.workbench_row_actions', ['p' => $p])
No unposted payments match your filters.
@if ($payments->hasPages())
@endif