Add payments tracking

This commit is contained in:
DariusIII
2026-03-31 16:11:27 +02:00
parent 0524ce9bd6
commit 254e8cbdb7
6 changed files with 394 additions and 4 deletions
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\BasePageController;
use App\Http\Controllers\BtcPaymentController;
use App\Models\Payment;
use Illuminate\Http\Request;
use Illuminate\View\View;
class AdminPaymentController extends BasePageController
{
/**
* @var array<int, string>
*/
private const ALLOWED_SORT_COLUMNS = [
'id',
'created_at',
'username',
'email',
'invoice_amount',
'payment_status',
'invoice_status',
'order_id',
];
public function index(Request $request): View
{
$this->setAdminPrefs();
$meta_title = $title = 'Payments';
$filters = [
'username' => $request->string('username')->trim()->value(),
'email' => $request->string('email')->trim()->value(),
'payment_status' => $request->string('payment_status')->trim()->value(),
'invoice_status' => $request->string('invoice_status')->trim()->value(),
];
$sort = $request->string('sort')->value();
if (! \in_array($sort, self::ALLOWED_SORT_COLUMNS, true)) {
$sort = 'created_at';
}
$order = strtolower($request->string('order')->value()) === 'asc' ? 'asc' : 'desc';
$payments = Payment::query()
->filter($filters)
->orderBy($sort, $order)
->paginate((int) config('nntmux.items_per_page'))
->withQueryString();
$paymentStatuses = BtcPaymentController::paymentStatusesForAdminFilter();
$invoiceStatuses = BtcPaymentController::invoiceStatusesForAdminFilter();
$this->viewData = array_merge($this->viewData, [
'payments' => $payments,
'filters' => $filters,
'sort' => $sort,
'order' => $order,
'paymentStatuses' => $paymentStatuses,
'invoiceStatuses' => $invoiceStatuses,
'title' => $title,
'meta_title' => $meta_title,
'page_title' => $title,
]);
return view('admin.payments.index', $this->viewData);
}
}
+33 -4
View File
@@ -28,7 +28,7 @@ class BtcPaymentController extends BasePageController
if ($payload['type'] === 'InvoicePaymentSettled') {
$user = User::query()->where('email', '=', $payload['metadata']['buyerEmail'])->first();
if ($user) {
$checkOrder = Payment::query()->where('invoice_id', '=', $payload['invoiceId'])->where('payment_status', '=', 'Settled')->first();
$checkOrder = Payment::query()->where('invoice_id', '=', $payload['invoiceId'])->where('payment_status', '=', Payment::PAYMENT_STATUS_SETTLED)->first();
if ($checkOrder !== null) {
Log::channel('btc_payment')->error('Duplicate BTCPay webhook: '.$payload['webhookId']);
@@ -59,8 +59,8 @@ class BtcPaymentController extends BasePageController
if ($payload['type'] === 'InvoiceSettled') {
// Check if we have the invoice_id in payments table and if we do, update the user account
$checkOrder = Payment::query()->where('invoice_id', '=', $payload['invoiceId'])->where('payment_status', '=', 'Settled')->where(function ($query) {
return $query->where('invoice_status', 'Pending')->orWhereNull('invoice_status');
$checkOrder = Payment::query()->where('invoice_id', '=', $payload['invoiceId'])->where('payment_status', '=', Payment::PAYMENT_STATUS_SETTLED)->where(function ($query) {
return $query->where('invoice_status', Payment::INVOICE_STATUS_PENDING)->orWhereNull('invoice_status');
})->first();
if ($checkOrder !== null) {
$user = User::query()->where('email', '=', $checkOrder->email)->first();
@@ -85,7 +85,7 @@ class BtcPaymentController extends BasePageController
}
User::updateUserRole($user->id, $roleName, addYears: $addYears);
$checkOrder->update(['invoice_status' => 'Settled']);
$checkOrder->update(['invoice_status' => Payment::INVOICE_STATUS_SETTLED]);
Log::channel('btc_payment')->info('User: '.$user->username.' upgraded to '.$roleName.' (+'.$addYears.' years) for BTCPay webhook: '.$checkOrder->webhook_id);
return response('OK', 200);
@@ -99,4 +99,33 @@ class BtcPaymentController extends BasePageController
return response('OK', 200);
}
/**
* Payment.status values from BTCPay Greenfield API (payload.payment.status). Used for admin filters.
*
* @return list<string>
*/
public static function paymentStatusesForAdminFilter(): array
{
return [
'Pending',
'Processing',
Payment::PAYMENT_STATUS_SETTLED,
'Invalid',
'Manual',
];
}
/**
* invoice_status values used in our DB (migration default Pending; webhook sets Settled after role upgrade).
*
* @return list<string>
*/
public static function invoiceStatusesForAdminFilter(): array
{
return [
Payment::INVOICE_STATUS_PENDING,
Payment::INVOICE_STATUS_SETTLED,
];
}
}
+42
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
@@ -12,4 +13,45 @@ class Payment extends Model
use HasFactory; // @phpstan-ignore missingType.generics
protected $guarded = [];
/** Stored from BTCPay webhook payload.payment.status; duplicate checks use Settled. */
public const PAYMENT_STATUS_SETTLED = 'Settled';
/** Migration default; webhook clears to Settled after role upgrade. */
public const INVOICE_STATUS_PENDING = 'Pending';
public const INVOICE_STATUS_SETTLED = 'Settled';
/**
* @param Builder<Payment> $query
* @param array{username?: string, email?: string, payment_status?: string, invoice_status?: string} $filters
* @return Builder<Payment>
*/
public function scopeFilter(Builder $query, array $filters): Builder
{
if (! empty($filters['username'])) {
$query->where('username', 'like', '%'.$filters['username'].'%');
}
if (! empty($filters['email'])) {
$query->where('email', 'like', '%'.$filters['email'].'%');
}
if (! empty($filters['payment_status'])) {
$query->where('payment_status', $filters['payment_status']);
}
if (! empty($filters['invoice_status'])) {
if ($filters['invoice_status'] === self::INVOICE_STATUS_PENDING) {
$query->where(function (Builder $q): void {
$q->whereNull('invoice_status')
->orWhere('invoice_status', self::INVOICE_STATUS_PENDING);
});
} else {
$query->where('invoice_status', $filters['invoice_status']);
}
}
return $query;
}
}