Files
usher-manage-stack/app/Http/Controllers/PaymentOrderController.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

377 lines
13 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\FinanceDocument;
use App\Models\PaymentOrder;
use App\Support\AuditLogger;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
class PaymentOrderController extends Controller
{
/**
* Display a listing of payment orders
*/
public function index(Request $request)
{
$query = PaymentOrder::query()
->with([
'financeDocument',
'createdByAccountant',
'verifiedByCashier',
'executedByCashier'
])
->orderByDesc('created_at');
// Filter by status
if ($request->filled('status')) {
$query->where('status', $request->status);
}
// Filter by verification status
if ($request->filled('verification_status')) {
$query->where('verification_status', $request->verification_status);
}
// Filter by execution status
if ($request->filled('execution_status')) {
$query->where('execution_status', $request->execution_status);
}
$paymentOrders = $query->paginate(15);
return view('admin.payment-orders.index', [
'paymentOrders' => $paymentOrders,
]);
}
/**
* Show the form for creating a new payment order (accountant only)
*/
public function create(FinanceDocument $financeDocument)
{
// Check authorization
$this->authorize('create_payment_order');
// Check if document is ready for payment order creation
if (!$financeDocument->canCreatePaymentOrder()) {
return redirect()
->route('admin.finance.show', $financeDocument)
->with('error', '此報銷申請單尚未完成審核流程,無法製作付款單。');
}
// Check if payment order already exists
if ($financeDocument->paymentOrder !== null) {
return redirect()
->route('admin.payment-orders.show', $financeDocument->paymentOrder)
->with('error', '此報銷申請單已有付款單。');
}
$financeDocument->load(['member', 'submittedBy']);
return view('admin.payment-orders.create', [
'financeDocument' => $financeDocument,
]);
}
/**
* Store a newly created payment order (accountant creates)
*/
public function store(Request $request)
{
// Check authorization
$this->authorize('create_payment_order');
$financeDocument = FinanceDocument::findOrFail($request->input('finance_document_id'));
// Check if document is ready
if (!$financeDocument->canCreatePaymentOrder()) {
if (app()->environment('testing')) {
\Log::info('payment_order.store.blocked', [
'finance_document_id' => $financeDocument->id,
'status' => $financeDocument->status,
'amount_tier' => $financeDocument->amount_tier,
'payment_order_created_at' => $financeDocument->payment_order_created_at,
]);
}
return redirect()
->route('admin.finance.show', $financeDocument)
->with('error', '此報銷申請單尚未完成審核流程,無法製作付款單。');
}
$validated = $request->validate([
'finance_document_id' => ['required', 'exists:finance_documents,id'],
'payee_name' => ['required', 'string', 'max:100'],
'payee_bank_code' => ['nullable', 'string', 'max:10'],
'payee_account_number' => ['nullable', 'string', 'max:30'],
'payee_bank_name' => ['nullable', 'string', 'max:100'],
'payment_amount' => ['required', 'numeric', 'min:0'],
'payment_method' => ['required', 'in:bank_transfer,check,cash'],
'notes' => ['nullable', 'string'],
]);
DB::beginTransaction();
try {
// Generate payment order number
$paymentOrderNumber = PaymentOrder::generatePaymentOrderNumber();
// Create payment order
$paymentOrder = PaymentOrder::create([
'finance_document_id' => $financeDocument->id,
'payee_name' => $validated['payee_name'],
'payee_bank_code' => $validated['payee_bank_code'] ?? null,
'payee_account_number' => $validated['payee_account_number'] ?? null,
'payee_bank_name' => $validated['payee_bank_name'] ?? null,
'payment_amount' => $validated['payment_amount'],
'payment_method' => $validated['payment_method'],
'created_by_accountant_id' => $request->user()->id,
'payment_order_number' => $paymentOrderNumber,
'notes' => $validated['notes'] ?? null,
'status' => PaymentOrder::STATUS_PENDING_VERIFICATION,
'verification_status' => PaymentOrder::VERIFICATION_PENDING,
'execution_status' => PaymentOrder::EXECUTION_PENDING,
]);
// Update finance document
$financeDocument->update([
'payment_order_created_by_accountant_id' => $request->user()->id,
'payment_order_created_at' => now(),
'payment_method' => $validated['payment_method'],
'payee_name' => $validated['payee_name'],
'payee_account_number' => $validated['payee_account_number'] ?? null,
'payee_bank_name' => $validated['payee_bank_name'] ?? null,
]);
AuditLogger::log('payment_order.created', $paymentOrder, $validated);
DB::commit();
return redirect()
->route('admin.payment-orders.show', $paymentOrder)
->with('status', "付款單 {$paymentOrderNumber} 已建立,等待出納覆核。");
} catch (\Exception $e) {
if (app()->environment('testing')) {
\Log::error('payment_order.store.failed', [
'finance_document_id' => $financeDocument->id ?? null,
'error' => $e->getMessage(),
]);
}
DB::rollBack();
return redirect()
->back()
->withInput()
->with('error', '建立付款單時發生錯誤:' . $e->getMessage());
}
}
/**
* Display the specified payment order
*/
public function show(PaymentOrder $paymentOrder)
{
$paymentOrder->load([
'financeDocument.member',
'financeDocument.submittedBy',
'createdByAccountant',
'verifiedByCashier',
'executedByCashier'
]);
return view('admin.payment-orders.show', [
'paymentOrder' => $paymentOrder,
]);
}
/**
* Cashier verifies the payment order
*/
public function verify(Request $request, PaymentOrder $paymentOrder)
{
// Check authorization
$this->authorize('verify_payment_order');
// Check if can be verified
if (!$paymentOrder->canBeVerifiedByCashier()) {
return redirect()
->route('admin.payment-orders.show', $paymentOrder)
->with('error', '此付款單無法覆核。');
}
$validated = $request->validate([
'action' => ['required', 'in:approve,reject'],
'verification_notes' => ['nullable', 'string'],
]);
DB::beginTransaction();
try {
if ($validated['action'] === 'approve') {
// Approve
$paymentOrder->update([
'verified_by_cashier_id' => $request->user()->id,
'verified_at' => now(),
'verification_status' => PaymentOrder::VERIFICATION_APPROVED,
'verification_notes' => $validated['verification_notes'] ?? null,
'status' => PaymentOrder::STATUS_VERIFIED,
]);
// Update finance document
$paymentOrder->financeDocument->update([
'payment_verified_by_cashier_id' => $request->user()->id,
'payment_verified_at' => now(),
]);
AuditLogger::log('payment_order.verified_approved', $paymentOrder, $validated);
$message = '付款單已覆核通過,可以執行付款。';
} else {
// Reject
$paymentOrder->update([
'verified_by_cashier_id' => $request->user()->id,
'verified_at' => now(),
'verification_status' => PaymentOrder::VERIFICATION_REJECTED,
'verification_notes' => $validated['verification_notes'] ?? null,
'status' => PaymentOrder::STATUS_CANCELLED,
]);
AuditLogger::log('payment_order.verified_rejected', $paymentOrder, $validated);
$message = '付款單已駁回。';
}
DB::commit();
return redirect()
->route('admin.payment-orders.show', $paymentOrder)
->with('status', $message);
} catch (\Exception $e) {
DB::rollBack();
return redirect()
->back()
->with('error', '覆核付款單時發生錯誤:' . $e->getMessage());
}
}
/**
* Cashier executes the payment
*/
public function execute(Request $request, PaymentOrder $paymentOrder)
{
// Check authorization
$this->authorize('execute_payment');
// Check if can be executed
if (!$paymentOrder->canBeExecuted()) {
return redirect()
->route('admin.payment-orders.show', $paymentOrder)
->with('error', '此付款單無法執行。');
}
$validated = $request->validate([
'transaction_reference' => ['required', 'string', 'max:100'],
'payment_receipt' => ['nullable', 'file', 'max:10240'], // 10MB max
'execution_notes' => ['nullable', 'string'],
]);
DB::beginTransaction();
try {
// Handle receipt upload
$receiptPath = null;
if ($request->hasFile('payment_receipt')) {
$receiptPath = $request->file('payment_receipt')->store('payment-receipts', 'local');
}
// Execute payment
$paymentOrder->update([
'executed_by_cashier_id' => $request->user()->id,
'executed_at' => now(),
'execution_status' => PaymentOrder::EXECUTION_COMPLETED,
'transaction_reference' => $validated['transaction_reference'],
'payment_receipt_path' => $receiptPath,
'status' => PaymentOrder::STATUS_EXECUTED,
]);
// Update finance document
$paymentOrder->financeDocument->update([
'payment_executed_by_cashier_id' => $request->user()->id,
'payment_executed_at' => now(),
'payment_transaction_id' => $validated['transaction_reference'],
'payment_receipt_path' => $receiptPath,
'actual_payment_amount' => $paymentOrder->payment_amount,
]);
AuditLogger::log('payment_order.executed', $paymentOrder, $validated);
DB::commit();
return redirect()
->route('admin.payment-orders.show', $paymentOrder)
->with('status', '付款已執行完成。');
} catch (\Exception $e) {
DB::rollBack();
return redirect()
->back()
->with('error', '執行付款時發生錯誤:' . $e->getMessage());
}
}
/**
* Download payment receipt
*/
public function downloadReceipt(PaymentOrder $paymentOrder)
{
if (!$paymentOrder->payment_receipt_path) {
abort(404, '找不到付款憑證');
}
if (!Storage::disk('local')->exists($paymentOrder->payment_receipt_path)) {
abort(404, '付款憑證檔案不存在');
}
return Storage::disk('local')->download($paymentOrder->payment_receipt_path);
}
/**
* Cancel a payment order
*/
public function cancel(Request $request, PaymentOrder $paymentOrder)
{
// Check authorization
$this->authorize('create_payment_order'); // Only accountant can cancel
// Cannot cancel if already executed
if ($paymentOrder->isExecuted()) {
return redirect()
->route('admin.payment-orders.show', $paymentOrder)
->with('error', '已執行的付款單無法取消。');
}
DB::beginTransaction();
try {
$paymentOrder->update([
'status' => PaymentOrder::STATUS_CANCELLED,
]);
AuditLogger::log('payment_order.cancelled', $paymentOrder, [
'cancelled_by' => $request->user()->id,
]);
DB::commit();
return redirect()
->route('admin.payment-orders.index')
->with('status', '付款單已取消。');
} catch (\Exception $e) {
DB::rollBack();
return redirect()
->back()
->with('error', '取消付款單時發生錯誤:' . $e->getMessage());
}
}
}