diff --git a/app/Console/Commands/CheckBillplzPayments.php b/app/Console/Commands/CheckBillplzPayments.php new file mode 100644 index 00000000..53133b74 --- /dev/null +++ b/app/Console/Commands/CheckBillplzPayments.php @@ -0,0 +1,112 @@ +billplzService = $billplzService; + } + + public function handle() +{ + $date = $this->option('date') ?? now()->toDateString(); + $preview = $this->option('preview'); + + // Validate date + try { + $dateObj = Carbon::createFromFormat('Y-m-d', $date); + } catch (\Exception $e) { + $this->error("Invalid date format. Use YYYY-MM-DD."); + return 1; + } + + $this->info("Retrying Billplz check for date: {$dateObj->toDateString()}"); + + $date = $dateObj->toDateString(); + + $pendingResits = DB::table('resit as r') + ->join('payment as p', 'p.resit_id', '=', 'r.id') + ->join(DB::raw('( + SELECT user_id, total_payment, MIN(id) AS min_id + FROM payment + GROUP BY user_id, total_payment + ) as unique_payment'), 'p.id', '=', 'unique_payment.min_id') + ->whereNull('r.status') + ->where('p.status', 0) + ->whereNotNull('r.billplz_id') + ->whereDate('r.created_at', $date) + ->where('r.billplz_id', 'NOT LIKE', '%CDM%') + ->select('r.*', 'p.id as payment_id', 'p.total_payment') + ->distinct() + ->get(); + + if ($pendingResits->isEmpty()) { + $this->info('Tiada Resit belum dibayar ditemui.'); + return 0; + } + + if ($preview) { + $this->info("Preview mode aktif. Senarai Resit:"); + foreach ($pendingResits as $resit) { + $this->line("Resit ID: {$resit->id} | Billplz ID: {$resit->billplz_id} | Jumlah: RM{$resit->total_payment}"); + } + return 0; + } + + foreach ($pendingResits as $resit) { + $this->info("Semak Resit ID: {$resit->id}"); + + $billData = $this->billplzService->testGetBill($resit->billplz_id); + + if (isset($billData['error'])) { + $this->error("❌ Gagal semak Billplz untuk Resit {$resit->id}: {$billData['details']}"); + continue; + } + + if (isset($billData['paid']) && $billData['paid'] === true) { + // Simulate Request object + $data = [ + 'billplz' => [ + 'id' => $resit->billplz_id, + 'paid' => 'true', + 'paid_at' => $billData['paid_at'] ?? now()->toDateTimeString(), + ] + ]; + // dd($data); + + $apiController = App::make(ApiController::class); + $apiController->processPaidResit($data); + // dd($fakeRequest); + + + $this->info("✅ Resit ID {$resit->id} handled via processPaidResit."); + }else { + $this->info("ℹ️ Resit ID {$resit->id} masih belum dibayar."); + } + } + + $this->info('Semakan semula Billplz selesai.'); +} + + +} diff --git a/app/Http/Controllers/ApiController.php b/app/Http/Controllers/ApiController.php index e0da97b0..c3a641dc 100644 --- a/app/Http/Controllers/ApiController.php +++ b/app/Http/Controllers/ApiController.php @@ -39,6 +39,7 @@ use Billplz\Client; use Http\Client\Common\HttpMethodsClient; use Http\Adapter\Guzzle7\Client as GuzzleHttpClient; use Http\Message\MessageFactory\GuzzleMessageFactory; +use App\Models\LogUser; class ApiController extends Controller { @@ -644,5 +645,66 @@ class ApiController extends Controller return view('pembida.feePaymentResult', compact('data', 'result', 'success')); } } + + + // BillplzService.php atau ApiController.php +public function processPaidResit(array $data) +{ + $currentdate = Carbon::now(); + $currentdate = $currentdate->toDateTimeString(); + $loguser = new LogUser; + $loguser->user_id = User::where('name','System')->first()->id; + $loguser->date = $currentdate; + $loguser->username = 'System'; + $loguser->nama_user = 'System'; + $loguser->norujukan = $data['billplz']['id']; + $loguser->nota = 'Callback Resit Billplz Telah Bayar : Billplz_id -'.$data['billplz']['id']; + $loguser->save(); + + $resit = Resit::where('billplz_id', '=', $data['billplz']['id'])->first(); + + if (!$resit) { + return false; + } + + $resit->status = 'PAID'; + $resit->paid_at = $data['billplz']['paid_at']; + $resit->save(); + + $resitoriginal = Resit::where('user_id', $resit->user_id) + ->where('lelong_id', $resit->lelong_id) + ->where('resit_type', 2) + ->first(); + + $payment = Payment::where('lelong_id', $resit->lelong_id) + ->where('user_id', $resit->user_id) + ->where('resit_id', $resit->id) + ->get(); + + foreach ($payment as $index) { + $index->status = 2; + $index->remark = "BERJAYA BAYARAN ONLINE"; + $index->update(); + + $updateTagBid = LelongBid::where('norujukan', $index->norujukan)->first(); + if ($updateTagBid) { + $updateTagBid->status_payment = 2; + $updateTagBid->save(); + + $updateTagBidder = LelongBidder::where('lelongbid_id', $updateTagBid->id) + ->where('user_id', $resit->user_id)->first(); + if ($updateTagBidder) { + $updateTagBidder->status_payment = 2; + $updateTagBidder->payment_id = $index->id; + $updateTagBidder->save(); + } + } + } + + event(new PaymentNotify('PAID', $resit->user_id, $payment)); + + return true; +} + } \ No newline at end of file diff --git a/app/Http/Controllers/PembidaController.php b/app/Http/Controllers/PembidaController.php index 597b7b45..57fab8a2 100644 --- a/app/Http/Controllers/PembidaController.php +++ b/app/Http/Controllers/PembidaController.php @@ -24,19 +24,26 @@ use App\Models\Invoice; use App\Models\ScanJual; use App\Services\ResitPhotoService; use Symfony\Component\Console\Descriptor\Descriptor; +use Illuminate\Support\Facades\DB; +use App\Services\BillplzService; +use App\Http\Controllers\ApiController; + class PembidaController extends Controller { private $resitPhotoService; + protected $billplzService; + protected $ResitSemakanService; /** * Create a new controller instance. * * @return void */ - public function __construct(ResitPhotoService $resitPhotoService) + public function __construct(ResitPhotoService $resitPhotoService,BillplzService $billplzService) { $this->middleware('auth'); $this->resitPhotoService = $resitPhotoService; + $this->billplzService = $billplzService; } /** @@ -1371,5 +1378,166 @@ class PembidaController extends Controller //return view('pembida.statusserahanbypembida',compact('listwinner','lelong','arrayinfo','cawanganbidder')); return view('pembida.statusserahanbypembida',compact('arraysms','arraysls','listwinner','lelong','cawanganbidder')); } + + public function semakanBillplz(Request $request, BillplzService $billplzService) + { + $date = $request->input('date') ?? now()->toDateString(); + + try { + $dateObj = Carbon::createFromFormat('Y-m-d', $date); + } catch (\Exception $e) { + return back()->with('error', 'Tarikh tidak sah. Guna format YYYY-MM-DD.'); + } + + $date = $dateObj->toDateString(); + + $allPendingResits = DB::table('resit as r') + ->join('payment as p', 'p.resit_id', '=', 'r.id') + ->join(DB::raw('( + SELECT user_id, total_payment, MIN(id) AS min_id + FROM payment + GROUP BY user_id, total_payment + ) as unique_payment'), 'p.id', '=', 'unique_payment.min_id') + ->join('users as u', 'p.user_id', '=', 'u.id') + ->join('cawangan as c', 'p.kodcaw', '=', 'c.caw_code') // JOIN ikut kodcaw + // Elakkan resit yang ada CDM payment pada hari yang sama + ->whereNotExists(function ($query) use ($date) { + $query->select(DB::raw(1)) + ->from('resit as r2') + ->join('payment as p2', 'p2.resit_id', '=', 'r2.id') + ->whereRaw('p2.user_id = p.user_id') + ->whereRaw('p2.total_payment = p.total_payment') + ->whereDate('r2.created_at', $date) + ->where('r2.billplz_id', 'LIKE', '%CDM%'); + }) + ->whereNull('r.status') + ->where('p.status', 0) + ->whereNotNull('r.billplz_id') + ->where('r.billplz_id', 'NOT LIKE', '%CDM%') + ->whereDate('r.created_at', $date) + ->select( + 'r.*', + 'p.id as payment_id', + 'p.total_payment', + 'p.user_id', + 'u.name as payer_name', + 'c.caw_name as cawangan_nama' // Tambah nama cawangan + ) + ->distinct() + ->get(); + + // Kumpul mengikut billplz_id dan user_id + $groupedResitsUnpaid = $allPendingResits + ->groupBy('billplz_id') // Group mengikut billplz_id + ->map(function ($items) { + return (object) [ + 'billplz_id' => $items->first()->billplz_id, + 'jumlah_bayaran' => $items->sum('total_payment'), + 'payer_name' => $items->first()->payer_name, + 'cawangan_nama' => $items->first()->cawangan_nama, + 'resit_id' => $items->first()->id, + 'created_at' => $items->first()->created_at, + 'user_id' => $items->first()->user_id, + ]; + }) + ->values(); // Reset index + + + $billplzWithCdmResits = DB::table('resit as r') + ->join('payment as p', 'p.resit_id', '=', 'r.id') + ->join(DB::raw('( + SELECT user_id, total_payment, MIN(id) AS min_id + FROM payment + GROUP BY user_id, total_payment + ) as unique_payment'), 'p.id', '=', 'unique_payment.min_id') + ->join('users as u', 'p.user_id', '=', 'u.id') + ->join('cawangan as c', 'p.kodcaw', '=', 'c.caw_code') + ->whereNull('r.status') + ->where('p.status', 0) + ->whereNotNull('r.billplz_id') + ->where('r.billplz_id', 'NOT LIKE', '%CDM%') + ->whereDate('r.created_at', $date) + ->whereExists(function ($query) use ($date) { + $query->select(DB::raw(1)) + ->from('resit as r2') + ->join('payment as p2', 'p2.resit_id', '=', 'r2.id') + ->join('users as u2', 'p2.user_id', '=', 'u2.id') + ->whereRaw('u2.name = u.name') // banding nama + ->whereRaw('p2.total_payment = p.total_payment') // dan jumlah + ->whereDate('r2.created_at', $date) + ->where('r2.billplz_id', 'LIKE', '%CDM%'); + }) + ->select('r.*', 'p.id as payment_id', 'p.total_payment', 'u.name as payer_name', 'c.caw_name as cawangan_nama') + ->distinct() + ->get(); + + $groupedResits = $billplzWithCdmResits + ->groupBy('billplz_id') + ->map(function ($items) { + return (object) [ + 'billplz_id' => $items->first()->billplz_id, + 'jumlah_bayaran' => $items->sum('total_payment'), + 'payer_name' => $items->first()->payer_name, + 'cawangan_nama' => $items->first()->cawangan_nama, + 'resit_id' => $items->first()->id, + 'created_at' => $items->first()->created_at, + 'user_id' => $items->first()->user_id, + ]; + })->values(); + + + + + + // Semak status Billplz untuk setiap resit + $paidResits = collect(); + $unpaidResits = collect(); + $processingResits = collect(); + + foreach ($allPendingResits as $resit) { + $billData = $billplzService->testGetBill($resit->billplz_id); + + $isPaid = isset($billData['paid']) && $billData['paid'] === true; + $isProcessing = isset($billData['state']) && in_array($billData['state'], ['pending', 'processing']) && isset($billData['paid_at']); + + if ($isPaid) { + $resit->paid_at = $billData['paid_at'] ?? now(); + $paidResits->push($resit); + } elseif ($isProcessing) { + $resit->paid_at = $billData['paid_at']; + $resit->state = $billData['state']; + $processingResits->push($resit); + } else { + $unpaidResits->push($resit); + } + } + + return view('pembida.semakanbillplz', compact( + 'paidResits', + 'unpaidResits', + 'billplzWithCdmResits', + 'processingResits', + 'groupedResits', + 'groupedResitsUnpaid', + 'date' + )); + } + + public function prosesBillplz(Request $request) + { + $data = [ + 'billplz' => [ + 'id' => $request->billplz_id, + 'paid' => 'true', + 'paid_at' => $request->paid_at ?? now()->toDateTimeString(), + ] + ]; + + $apiController = app(ApiController::class); + $apiController->processPaidResit($data); + + return redirect()->back()->with('success', 'Resit telah diproses.'); + } + } \ No newline at end of file diff --git a/app/Services/BillplzService.php b/app/Services/BillplzService.php new file mode 100644 index 00000000..f279144f --- /dev/null +++ b/app/Services/BillplzService.php @@ -0,0 +1,49 @@ +client = new BillplzClient($http, $apiKey, $baseUrl); + } + + public function testGetBill(string $billId) + { + try { + // Minta details bill berdasarkan ID + $response = $this->client->bill()->get($billId); + + // Pastikan respons berjaya + if ($response->isSuccessful()) { + return $response->toArray(); // Kembalikan hasil dalam bentuk array + } + + return ['error' => 'Request failed', 'details' => $response->toArray()]; + } catch (NotFoundException $e) { + return ['error' => 'Bill not found', 'details' => $e->getMessage()]; + } catch (\Throwable $e) { + return ['error' => 'Unexpected error', 'details' => $e->getMessage()]; + } + } +} diff --git a/config/services.php b/config/services.php index 2a1d616c..91dedce4 100644 --- a/config/services.php +++ b/config/services.php @@ -30,4 +30,9 @@ return [ 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), ], + 'billplz' => [ + 'key' => env('BILLPLZ_API_KEY'), + 'sandbox' => env('BILLPLZ_SANDBOX', false), + ], + ]; diff --git a/resources/views/pembida/semakanbillplz.blade.php b/resources/views/pembida/semakanbillplz.blade.php new file mode 100644 index 00000000..daa85675 --- /dev/null +++ b/resources/views/pembida/semakanbillplz.blade.php @@ -0,0 +1,178 @@ +@extends('layouts.app') +@section('content') +
+
+
+
+
+ + + +
+ Semakan Billplz FPX +
+
+
+ @if ($message = Session::get('success')) +
+

{{ $message }}

+
+ @endif + +
+
+ + + +
+
+ + +
+ +

✅ Resit Telah Dibayar (PAID)

+@if($paidResits->isEmpty()) +

Tiada resit yang telah dibayar.

+@else + + + + + + + + + + + + + + + @foreach($paidResits as $resit) + + + + + + + + + + + @endforeach + +
Nama PembayarID PembayarCawanganResit IDBillplz IDJumlah (RM)Dibayar PadaTindakan
{{ $resit->payer_name ?? '-' }}{{ $resit->user_id }}{{ $resit->cawangan_nama ?? '-' }}{{ $resit->id }}{{ $resit->billplz_id }}{{ number_format($resit->total_payment, 2) }}{{ \Carbon\Carbon::parse($resit->paid_at)->format('d/m/Y H:i') }} +
+ @csrf + + + +
+
+@endif + +
+ +

❌ Resit Belum Dibayar (UNPAID)

+@if($unpaidResits->isEmpty()) +

Semua resit telah dibayar.

+@else + + + + + + + + + + + + + + @foreach ($groupedResitsUnpaid as $resit) + + + + + + + + + + @endforeach + +
Nama PembayarID PembayarCawanganResit IDBillplz IDJumlah (RM)Tarikh Dicipta
{{ $resit->payer_name ?? '-' }}{{ $resit->user_id }}{{ $resit->cawangan_nama ?? '-' }}{{ $resit->resit_id }}{{ $resit->billplz_id }}{{ number_format($resit->jumlah_bayaran, 2) }}{{ \Carbon\Carbon::parse($resit->created_at)->format('d/m/Y H:i') }}
+@endif + +
+

🔄 Resit Billplz yang Telah Dibayar Semula Melalui CDM

+@if($billplzWithCdmResits->isEmpty()) +

Tiada bayaran semula melalui CDM ditemui.

+@else + + + + + + + + + + + + + + @foreach ($groupedResits as $resit) + + + + + + + + + + @endforeach + +
Nama PembayarID PembayarCawanganJumlah (RM)Resit IDBillplz IDTarikh Dicipta
{{ $resit->payer_name }}{{ $resit->user_id }}{{ $resit->cawangan_nama }}{{ number_format($resit->jumlah_bayaran, 2) }}{{ $resit->resit_id }}{{ $resit->billplz_id }}{{ \Carbon\Carbon::parse($resit->created_at)->format('Y-m-d') }}
+@endif + +
+

🕒 Resit Telah Dibayar Tetapi Dalam Proses (PENDING)

+@if($processingResits->isEmpty()) +

Tiada resit yang belum direlease.

+@else + + + + + + + + + + + + + + @foreach($processingResits as $resit) + + + + + + + + + + @endforeach + +
Nama PembayarID PembayarCawanganResit IDBillplz IDJumlah (RM)Tarikh Dibayar
{{ $resit->payer_name }}{{ $resit->user_id }}{{ $resit->cawangan_nama ?? '-' }}{{ $resit->id }}{{ $resit->billplz_id }}{{ number_format($resit->total_payment, 2) }}{{ \Carbon\Carbon::parse($resit->paid_at)->format('d/m/Y H:i') }}
+@endif + + + + + +@endsection \ No newline at end of file diff --git a/resources/views/pembida/statuspembayaran2.blade.php b/resources/views/pembida/statuspembayaran2.blade.php index 6343a8e4..b5086619 100644 --- a/resources/views/pembida/statuspembayaran2.blade.php +++ b/resources/views/pembida/statuspembayaran2.blade.php @@ -13,6 +13,8 @@
@hasrole('operasi') Status Pembayaran Mengikut Nama Pembida + Semakan BILLPLZ + @endhasrole @hasrole('account') Status Pembayaran Mengikut Nama Pembida diff --git a/routes/web.php b/routes/web.php index ada42e86..1e4b41ff 100644 --- a/routes/web.php +++ b/routes/web.php @@ -364,6 +364,9 @@ Route::group(['middleware' => ['permission:pengesahanpembida'],'prefix'=>'pembid Route::get('/statuspembayaranbypembida',['as'=>'pembida.statuspembayaranbypembida','uses'=>'PembidaController@statuspembayaranbypembida']); Route::get('/statusserahanbidaan',['as'=>'pembida.statusserahanbidaan','uses'=>'PembidaController@statusserahanbidaan']); Route::get('/statusserahanbypembida',['as'=>'pembida.statusserahanbypembida','uses'=>'PembidaController@statusserahanbypembida']); + Route::get('/semakanbillplz',['as'=>'pembida.semakanbillplz','uses'=>'PembidaController@semakanbillplz']); + Route::post('/semakanbillplz/proses',['as'=>'pembida.prosesBillplz','uses'=>'PembidaController@prosesBillplz']); + // Route::get('/pemenangkedua',['as'=>'pembida.pemenangkedua','uses'=>'PembidaController@pemenangkedua']); // Route::get('/senaraipemenangkedua/{lelong_id}/{kodcaw}',['as'=>'pembida.senaraipemenangkedua','uses'=>'PembidaController@senaraipemenangkedua']);