Skip to content

Commit d1c4cfe

Browse files
Add manual payment flow with admin approval for credit packages
Co-authored-by: bixmatech <bixmatech@gmail.com>
1 parent 8417117 commit d1c4cfe

File tree

6 files changed

+143
-1
lines changed

6 files changed

+143
-1
lines changed

app/Http/Controllers/Admin/PaymentAdminController.php

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,33 @@
33
namespace App\Http\Controllers\Admin;
44

55
use App\Http\Controllers\Controller;
6+
use App\Models\Payment;
67
use Illuminate\Http\Request;
8+
use Illuminate\View\View;
9+
use Illuminate\Http\RedirectResponse;
710

811
class PaymentAdminController extends Controller
912
{
10-
//
13+
public function index(): View
14+
{
15+
$payments = Payment::latest()->paginate(30);
16+
return view('admin.payments.index', compact('payments'));
17+
}
18+
19+
public function approve(Payment $payment): RedirectResponse
20+
{
21+
if ($payment->status === 'pending' && $payment->provider === 'manual') {
22+
$payment->update(['status' => 'paid']);
23+
$payment->user->increment('credits', $payment->package?->credits ?? 0);
24+
}
25+
return back()->with('status', 'Payment approved');
26+
}
27+
28+
public function fail(Payment $payment): RedirectResponse
29+
{
30+
if ($payment->status === 'pending' && $payment->provider === 'manual') {
31+
$payment->update(['status' => 'failed']);
32+
}
33+
return back()->with('status', 'Payment marked failed');
34+
}
1135
}

app/Http/Controllers/PaymentController.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
use Illuminate\Http\Request;
88
use Illuminate\Support\Facades\URL;
99
use Stripe\StripeClient;
10+
use Illuminate\View\View;
11+
use Illuminate\Http\RedirectResponse;
1012

1113
class PaymentController extends Controller
1214
{
@@ -51,6 +53,35 @@ public function checkout(Request $request)
5153
return redirect($session->url);
5254
}
5355

56+
public function manualCreate(): View
57+
{
58+
$packages = CreditPackage::where('is_active', true)->orderBy('price')->get();
59+
return view('payments.manual', compact('packages'));
60+
}
61+
62+
public function manualStore(Request $request): RedirectResponse
63+
{
64+
$data = $request->validate([
65+
'package_id' => 'required|exists:credit_packages,id',
66+
'reference' => 'required|string|max:255',
67+
'notes' => 'nullable|string',
68+
]);
69+
$package = CreditPackage::findOrFail($data['package_id']);
70+
71+
Payment::create([
72+
'user_id' => $request->user()->id,
73+
'credit_package_id' => $package->id,
74+
'provider' => 'manual',
75+
'provider_ref' => $data['reference'],
76+
'amount' => $package->price,
77+
'currency' => $package->currency,
78+
'status' => 'pending',
79+
'meta' => ['notes' => $data['notes'] ?? null],
80+
]);
81+
82+
return redirect()->route('dashboard')->with('status', 'Manual payment submitted. Awaiting approval.');
83+
}
84+
5485
public function webhook(Request $request)
5586
{
5687
$sig = $request->header('Stripe-Signature');
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<x-app-layout>
2+
<x-slot name="header">
3+
<h2 class="font-semibold text-xl leading-tight">Payments</h2>
4+
</x-slot>
5+
<div class="py-6">
6+
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
7+
<div class="bg-white dark:bg-gray-900 overflow-hidden shadow-sm sm:rounded-lg">
8+
<div class="p-6 text-gray-900 dark:text-gray-100">
9+
<table class="w-full text-left">
10+
<thead>
11+
<tr>
12+
<th class="py-2">User</th>
13+
<th>Provider</th>
14+
<th>Reference</th>
15+
<th>Amount</th>
16+
<th>Status</th>
17+
<th></th>
18+
</tr>
19+
</thead>
20+
<tbody>
21+
@foreach($payments as $p)
22+
<tr class="border-t">
23+
<td class="py-2">{{ $p->user->email }}</td>
24+
<td>{{ ucfirst($p->provider) }}</td>
25+
<td>{{ $p->provider_ref }}</td>
26+
<td>{{ $p->amount }} {{ $p->currency }}</td>
27+
<td>{{ $p->status }}</td>
28+
<td class="text-right space-x-2">
29+
@if($p->provider === 'manual' && $p->status === 'pending')
30+
<form method="POST" action="{{ route('admin.payments.approve', $p) }}" class="inline">@csrf<button class="text-green-600">Approve</button></form>
31+
<form method="POST" action="{{ route('admin.payments.fail', $p) }}" class="inline">@csrf<button class="text-red-600">Fail</button></form>
32+
@endif
33+
</td>
34+
</tr>
35+
@endforeach
36+
</tbody>
37+
</table>
38+
<div class="mt-4">{{ $payments->links() }}</div>
39+
</div>
40+
</div>
41+
</div>
42+
</div>
43+
</x-app-layout>

resources/views/dashboard.blade.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
</select>
2222
<button class="px-3 py-2 bg-indigo-600 text-white rounded">Buy with Stripe</button>
2323
</form>
24+
<div class="mt-2">
25+
<a href="{{ route('payments.manual.create') }}" class="text-sm text-gray-600 underline">Manual bank payment</a>
26+
</div>
2427
<div class="mt-6">
2528
@php($latest = \App\Models\ChatThread::where('user_id', Auth::id())->latest()->first())
2629
@if($latest)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<x-app-layout>
2+
<x-slot name="header">
3+
<h2 class="font-semibold text-xl leading-tight">Manual Payment</h2>
4+
</x-slot>
5+
<div class="py-6">
6+
<div class="max-w-3xl mx-auto sm:px-6 lg:px-8">
7+
<div class="bg-white dark:bg-gray-900 overflow-hidden shadow-sm sm:rounded-lg">
8+
<div class="p-6 text-gray-900 dark:text-gray-100">
9+
<p class="mb-4">After making a bank transfer, submit the reference below for admin approval.</p>
10+
<form method="POST" action="{{ route('payments.manual.store') }}" class="space-y-4">
11+
@csrf
12+
<div>
13+
<label class="block">Package</label>
14+
<select name="package_id" class="w-full border rounded p-2" required>
15+
@foreach($packages as $pkg)
16+
<option value="{{ $pkg->id }}">{{ $pkg->name }}{{ $pkg->credits }} credits — {{ $pkg->price }} {{ $pkg->currency }}</option>
17+
@endforeach
18+
</select>
19+
</div>
20+
<div>
21+
<label class="block">Bank Reference</label>
22+
<input name="reference" class="w-full border rounded p-2" required>
23+
</div>
24+
<div>
25+
<label class="block">Notes (optional)</label>
26+
<textarea name="notes" class="w-full border rounded p-2" rows="3"></textarea>
27+
</div>
28+
<button class="px-4 py-2 bg-indigo-600 text-white rounded">Submit</button>
29+
<a href="{{ route('dashboard') }}" class="ml-2 px-4 py-2 border rounded">Cancel</a>
30+
</form>
31+
</div>
32+
</div>
33+
</div>
34+
</div>
35+
</x-app-layout>

routes/web.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use App\Http\Controllers\BlogPostController;
99
use App\Http\Controllers\PageController;
1010
use App\Http\Controllers\ThreadController;
11+
use App\Http\Controllers\Admin\PaymentAdminController;
1112
use Illuminate\Support\Facades\Route;
1213

1314
Route::get('/', function () {
@@ -27,10 +28,15 @@
2728
Route::resource('packages', CreditPackageController::class)->except(['show']);
2829
Route::resource('blog', BlogPostController::class);
2930
Route::resource('pages', PageController::class);
31+
Route::get('payments', [PaymentAdminController::class, 'index'])->name('payments.index');
32+
Route::post('payments/{payment}/approve', [PaymentAdminController::class, 'approve'])->name('payments.approve');
33+
Route::post('payments/{payment}/fail', [PaymentAdminController::class, 'fail'])->name('payments.fail');
3034
});
3135

3236
Route::middleware('auth')->group(function () {
3337
Route::post('/checkout', [PaymentController::class, 'checkout'])->name('checkout');
38+
Route::get('/payments/manual', [PaymentController::class, 'manualCreate'])->name('payments.manual.create');
39+
Route::post('/payments/manual', [PaymentController::class, 'manualStore'])->name('payments.manual.store');
3440
Route::get('/threads/{thread}/share', [ThreadController::class, 'share'])->name('threads.share');
3541
});
3642

0 commit comments

Comments
 (0)