Files
usher-manage-stack/app/Http/Controllers/PaymentVerificationController.php
Gbanyan 642b879dd4 Add membership fee system with disability discount and fix document permissions
Features:
- Implement two fee types: entrance fee and annual fee (both NT$1,000)
- Add 50% discount for disability certificate holders
- Add disability certificate upload in member profile
- Integrate disability verification into cashier approval workflow
- Add membership fee settings in system admin

Document permissions:
- Fix hard-coded role logic in Document model
- Use permission-based authorization instead of role checks

Additional features:
- Add announcements, general ledger, and trial balance modules
- Add income management and accounting entries
- Add comprehensive test suite with factories
- Update UI translations to Traditional Chinese

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 09:56:01 +08:00

284 lines
11 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Mail\PaymentApprovedByCashierMail;
use App\Mail\PaymentApprovedByAccountantMail;
use App\Mail\PaymentFullyApprovedMail;
use App\Mail\PaymentRejectedMail;
use App\Models\MembershipPayment;
use App\Models\User;
use App\Support\AuditLogger;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
class PaymentVerificationController extends Controller
{
/**
* Show payment verification dashboard
*/
public function index(Request $request)
{
$user = Auth::user();
$tab = $request->query('tab', 'all');
// Base query with relationships
$query = MembershipPayment::with(['member', 'submittedBy', 'verifiedByCashier', 'verifiedByAccountant', 'verifiedByChair', 'rejectedBy'])
->latest();
// Filter by tab
if ($tab === 'cashier' && $user->can('verify_payments_cashier')) {
$query->where('status', MembershipPayment::STATUS_PENDING);
} elseif ($tab === 'accountant' && $user->can('verify_payments_accountant')) {
$query->where('status', MembershipPayment::STATUS_APPROVED_CASHIER);
} elseif ($tab === 'chair' && $user->can('verify_payments_chair')) {
$query->where('status', MembershipPayment::STATUS_APPROVED_ACCOUNTANT);
} elseif ($tab === 'rejected') {
$query->where('status', MembershipPayment::STATUS_REJECTED);
} elseif ($tab === 'approved') {
$query->where('status', MembershipPayment::STATUS_APPROVED_CHAIR);
}
// Filter by search
if ($search = $request->query('search')) {
$query->whereHas('member', function ($q) use ($search) {
$q->where('full_name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%");
})->orWhere('reference', 'like', "%{$search}%");
}
$payments = $query->paginate(20)->withQueryString();
// Get counts for tabs
$counts = [
'pending' => MembershipPayment::where('status', MembershipPayment::STATUS_PENDING)->count(),
'cashier_approved' => MembershipPayment::where('status', MembershipPayment::STATUS_APPROVED_CASHIER)->count(),
'accountant_approved' => MembershipPayment::where('status', MembershipPayment::STATUS_APPROVED_ACCOUNTANT)->count(),
'approved' => MembershipPayment::where('status', MembershipPayment::STATUS_APPROVED_CHAIR)->count(),
'rejected' => MembershipPayment::where('status', MembershipPayment::STATUS_REJECTED)->count(),
];
return view('admin.payment-verifications.index', compact('payments', 'tab', 'counts'));
}
/**
* Show verification form for a payment
*/
public function show(MembershipPayment $payment)
{
$payment->load(['member', 'submittedBy', 'verifiedByCashier', 'verifiedByAccountant', 'verifiedByChair', 'rejectedBy']);
return view('admin.payment-verifications.show', compact('payment'));
}
/**
* Approve payment (cashier tier)
*/
public function approveByCashier(Request $request, MembershipPayment $payment)
{
if (!Auth::user()->can('verify_payments_cashier')) {
abort(403, 'You do not have permission to verify payments as cashier.');
}
if (!$payment->canBeApprovedByCashier()) {
return back()->with('error', __('This payment cannot be approved at this stage.'));
}
$validated = $request->validate([
'notes' => ['nullable', 'string', 'max:1000'],
'disability_action' => ['nullable', 'in:approve,reject'],
'disability_rejection_reason' => ['required_if:disability_action,reject', 'nullable', 'string', 'max:500'],
]);
// Handle disability certificate verification if applicable
$member = $payment->member;
if ($member && $member->hasDisabilityCertificate() && $member->isDisabilityPending()) {
if ($validated['disability_action'] === 'approve') {
$member->approveDisabilityCertificate(Auth::user());
} elseif ($validated['disability_action'] === 'reject') {
$member->rejectDisabilityCertificate(Auth::user(), $validated['disability_rejection_reason']);
}
}
$payment->update([
'status' => MembershipPayment::STATUS_APPROVED_CASHIER,
'verified_by_cashier_id' => Auth::id(),
'cashier_verified_at' => now(),
'notes' => $validated['notes'] ?? $payment->notes,
]);
AuditLogger::log('payment.approved_by_cashier', $payment, [
'member_id' => $payment->member_id,
'amount' => $payment->amount,
'verified_by' => Auth::id(),
]);
// Send notification to member
Mail::to($payment->member->email)->queue(new PaymentApprovedByCashierMail($payment));
// Send notification to accountants
$accountants = User::permission('verify_payments_accountant')->get();
foreach ($accountants as $accountant) {
Mail::to($accountant->email)->queue(new PaymentApprovedByCashierMail($payment));
}
return redirect()->route('admin.payment-verifications.index')
->with('status', __('Payment approved by cashier. Forwarded to accountant for review.'));
}
/**
* Approve payment (accountant tier)
*/
public function approveByAccountant(Request $request, MembershipPayment $payment)
{
if (!Auth::user()->can('verify_payments_accountant')) {
abort(403, 'You do not have permission to verify payments as accountant.');
}
if (!$payment->canBeApprovedByAccountant()) {
return back()->with('error', __('This payment cannot be approved at this stage.'));
}
$validated = $request->validate([
'notes' => ['nullable', 'string', 'max:1000'],
]);
$payment->update([
'status' => MembershipPayment::STATUS_APPROVED_ACCOUNTANT,
'verified_by_accountant_id' => Auth::id(),
'accountant_verified_at' => now(),
'notes' => $validated['notes'] ?? $payment->notes,
]);
AuditLogger::log('payment.approved_by_accountant', $payment, [
'member_id' => $payment->member_id,
'amount' => $payment->amount,
'verified_by' => Auth::id(),
]);
// Send notification to member
Mail::to($payment->member->email)->queue(new PaymentApprovedByAccountantMail($payment));
// Send notification to chairs
$chairs = User::permission('verify_payments_chair')->get();
foreach ($chairs as $chair) {
Mail::to($chair->email)->queue(new PaymentApprovedByAccountantMail($payment));
}
return redirect()->route('admin.payment-verifications.index')
->with('status', __('Payment approved by accountant. Forwarded to chair for final approval.'));
}
/**
* Approve payment (chair tier - final approval)
*/
public function approveByChair(Request $request, MembershipPayment $payment)
{
if (!Auth::user()->can('verify_payments_chair')) {
abort(403, 'You do not have permission to verify payments as chair.');
}
if (!$payment->canBeApprovedByChair()) {
return back()->with('error', __('This payment cannot be approved at this stage.'));
}
$validated = $request->validate([
'notes' => ['nullable', 'string', 'max:1000'],
]);
$payment->update([
'status' => MembershipPayment::STATUS_APPROVED_CHAIR,
'verified_by_chair_id' => Auth::id(),
'chair_verified_at' => now(),
'notes' => $validated['notes'] ?? $payment->notes,
]);
// Activate member on final approval
if ($payment->member) {
$payment->member->update([
'membership_status' => \App\Models\Member::STATUS_ACTIVE,
'membership_started_at' => now(),
'membership_expires_at' => now()->addYear(),
]);
}
AuditLogger::log('payment.approved_by_chair', $payment, [
'member_id' => $payment->member_id,
'amount' => $payment->amount,
'verified_by' => Auth::id(),
]);
// Send notification to member and admins
Mail::to($payment->member->email)->queue(new PaymentFullyApprovedMail($payment));
Mail::to($payment->member->email)->queue(new \App\Mail\MembershipActivatedMail($payment->member));
// Notify membership managers
$managers = User::permission('activate_memberships')->get();
foreach ($managers as $manager) {
Mail::to($manager->email)->queue(new PaymentFullyApprovedMail($payment));
}
return redirect()->route('admin.payment-verifications.index')
->with('status', __('Payment fully approved! Member can now be activated by membership manager.'));
}
/**
* Reject payment
*/
public function reject(Request $request, MembershipPayment $payment)
{
$user = Auth::user();
// Check if user has any verification permission
if (!$user->can('verify_payments_cashier')
&& !$user->can('verify_payments_accountant')
&& !$user->can('verify_payments_chair')) {
abort(403, 'You do not have permission to reject payments.');
}
if ($payment->isFullyApproved()) {
return back()->with('error', __('Cannot reject a fully approved payment.'));
}
$validated = $request->validate([
'rejection_reason' => ['required', 'string', 'max:1000'],
]);
$payment->update([
'status' => MembershipPayment::STATUS_REJECTED,
'rejected_by_user_id' => Auth::id(),
'rejected_at' => now(),
'rejection_reason' => $validated['rejection_reason'],
]);
AuditLogger::log('payment.rejected', $payment, [
'member_id' => $payment->member_id,
'amount' => $payment->amount,
'rejected_by' => Auth::id(),
'reason' => $validated['rejection_reason'],
]);
// Send notification to member
Mail::to($payment->member->email)->queue(new PaymentRejectedMail($payment));
return redirect()->route('admin.payment-verifications.index')
->with('status', __('Payment rejected. Member has been notified.'));
}
/**
* Download payment receipt
*/
public function downloadReceipt(MembershipPayment $payment)
{
if (!$payment->receipt_path || !Storage::exists($payment->receipt_path)) {
abort(404, 'Receipt file not found.');
}
$fileName = 'payment_receipt_' . $payment->member->full_name . '_' . $payment->paid_at->format('Ymd') . '.' . pathinfo($payment->receipt_path, PATHINFO_EXTENSION);
return Storage::download($payment->receipt_path, $fileName);
}
}