pembayaran

This commit is contained in:
Baghiz Zuhdi Adzin 2026-01-21 15:34:54 +07:00
parent e4ee4b3f72
commit 1f8efdccc4
41 changed files with 2922 additions and 561 deletions

View File

@ -0,0 +1,19 @@
<?php
namespace App\Events;
use App\Models\VAPayment;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class PaymentCompleted
{
use Dispatchable, SerializesModels;
public VAPayment $payment;
public function __construct(VAPayment $payment)
{
$this->payment = $payment;
}
}

View File

@ -0,0 +1,218 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Bootcamp;
use App\Models\BootcampPurchase;
use App\Models\CartItem;
use App\Models\Coupon;
use App\Models\Course;
use App\Models\Enrollment;
use App\Models\OfflinePayment;
use App\Models\Payment_history;
use App\Models\TeamPackagePurchase;
use App\Models\TeamTrainingPackage;
use App\Models\TutorBooking;
use App\Models\TutorSchedule;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class OfflinePaymentController extends Controller
{
public function index(Request $request)
{
$payments = OfflinePayment::orderBY('id', 'DESC');
if ($request->status == 'approved') {
$payments->where('status', 1);
} elseif ($request->status == 'suspended') {
$payments->where('status', 2);
} elseif ($request->status == 'pending') {
$payments->where('status', 0)->orWhere('status', null);
}
$page_data['payments'] = $payments->paginate(10);
return view('admin.offline_payments.index', $page_data);
}
public function download_doc($id)
{
// validate id
if (empty($id)) {
Session::flash('error', get_phrase('Data not found.'));
return redirect()->back();
}
// payment details
$payment_details = OfflinePayment::where('id', $id)->first();
$filePath = public_path($payment_details->doc);
if (! file_exists($filePath)) {
Session::flash('error', get_phrase('Data not found.'));
return redirect()->back();
}
// download file
return Response::download($filePath);
}
public function accept_payment($id)
{
// validate id
if (empty($id)) {
Session::flash('error', get_phrase('Id can not be empty.'));
return redirect()->back();
}
// payment details
$query = OfflinePayment::where('id', $id)->where('status', 0);
if ($query->doesntExist()) {
Session::flash('error', get_phrase('Data not found.'));
return redirect()->back();
}
$payment_details = $query->first();
$payment['invoice'] = Str::random(20);
$payment['user_id'] = $payment_details['user_id'];
$payment['payment_type'] = 'offline';
$payment['coupon'] = $payment_details->coupon;
if ($payment_details->item_type == 'course') {
$items = json_decode($payment_details->items);
foreach ($items as $item) {
$course = Course::where('id', $item)->first();
$payment['course_id'] = $course->id;
// Calculate base amount
$amount = $course->discount_flag == 1 ? $course->discounted_price : $course->price;
// Apply coupon discount if applicable
$discounted_price = 0;
if ($payment_details->coupon) {
$coupon = Coupon::where('code', $payment_details->coupon)->first();
if ($coupon) {
$discounted_price = $amount * ($coupon->discount / 100);
}
}
// Final payment amount and tax
$final_amount = $amount - $discounted_price;
$payment['amount'] = $final_amount;
$payment['tax'] = (get_settings('course_selling_tax') / 100) * $final_amount;
// Calculate admin and instructor revenue
if (get_course_creator_id($course->id)->role == 'admin') {
$payment['admin_revenue'] = $final_amount;
} else {
$payment['instructor_revenue'] = $final_amount * (get_settings('instructor_revenue') / 100);
$payment['admin_revenue'] = $final_amount - $payment['instructor_revenue'];
}
// Insert payment record
$accept_payment = Payment_history::insert($payment);
// Enroll user in course if payment succeeds
if ($accept_payment) {
$enroll['user_id'] = $payment_details['user_id'];
$enroll['course_id'] = $course->id;
$enroll['enrollment_type'] = "paid";
$enroll['entry_date'] = time();
$enroll['created_at'] = date('Y-m-d H:i:s');
$enroll['updated_at'] = date('Y-m-d H:i:s');
if ($course->expiry_period > 0) {
$days = $course->expiry_period * 30;
$enroll['expiry_date'] = strtotime("+" . $days . " days");
} else {
$enroll['expiry_date'] = null;
}
Enrollment::insert($enroll);
}
}
} elseif ($payment_details->item_type == 'bootcamp') {
$bootcamps = Bootcamp::whereIn('id', json_decode($payment_details->items, true))->get();
foreach($bootcamps as $bootcamp){
$bootcamp_payment['invoice'] = '#' . Str::random(20);
$bootcamp_payment['user_id'] = $payment_details['user_id'];
$bootcamp_payment['bootcamp_id'] = $bootcamp->id;
$bootcamp_payment['price'] = $bootcamp->discount_flag == 1 ? $bootcamp->price - $bootcamp->discounted_price : $bootcamp->price;
$bootcamp_payment['tax'] = 0;
$bootcamp_payment['payment_method'] = 'offline';
$bootcamp_payment['status'] = 1;
// insert bootcamp purchase
BootcampPurchase::insert($bootcamp_payment);
}
} elseif ($payment_details->item_type == 'package') {
$packages = TeamTrainingPackage::whereIn('id', json_decode($payment_details->items, true))->get();
foreach($packages as $package){
$package_payment['invoice'] = '#' . Str::random(20);
$package_payment['user_id'] = $payment_details['user_id'];
$package_payment['package_id'] = $package->id;
$package_payment['price'] = $package->price;
$package_payment['tax'] = 0;
$package_payment['payment_method'] = 'offline';
$package_payment['status'] = 1;
// insert package purchase
TeamPackagePurchase::insert($package_payment);
}
} elseif ($payment_details->item_type == 'tutor_booking') {
$schedules = TutorSchedule::whereIn('id', json_decode($payment_details->items, true))->get();
foreach($schedules as $schedule){
$schedule_payment['invoice'] = '#' . Str::random(20);
$schedule_payment['student_id'] = $payment_details['user_id'];
$schedule_payment['schedule_id'] = $schedule->id;
$schedule_payment['price'] = $payment_details['total_amount'];
$schedule_payment['tax'] = $payment_details['tax'];
$schedule_payment['payment_method'] = 'offline';
$schedule = TutorSchedule::find($schedule->id);
if (get_user_info($schedule->tutor_id)->role == 'admin') {
$schedule_payment['admin_revenue'] = $payment_details['payable_amount'];
} else {
$schedule_payment['instructor_revenue'] = $payment_details['total_amount'] * (get_settings('instructor_revenue') / 100);
$schedule_payment['admin_revenue'] = $payment_details['total_amount'] - $schedule_payment['instructor_revenue'];
}
$schedule_payment['tutor_id'] = $schedule->tutor_id;
$schedule_payment['start_time'] = $schedule->start_time;
$schedule_payment['end_time'] = $schedule->end_time;
// insert tutor bookings
TutorBooking::insert($schedule_payment);
}
}
// remove items from offline payment
OfflinePayment::where('id', $id)->update(['status' => 1]);
// go back
Session::flash('success', 'Payment has been accepted.');
return redirect()->route('admin.offline.payments');
}
public function decline_payment($id)
{
// remove items from offline payment
OfflinePayment::where('id', $id)->update(['status' => 2]);
// go back
Session::flash('success', 'Payment has been suspended');
return redirect()->route('admin.offline.payments');
}
public function delete_payment($id)
{
OfflinePayment::where('id', $id)->delete();
Session::flash('success', get_phrase('Admin revenue delete successfully'));
return redirect()->route('admin.offline.payments');
}
}

View File

@ -2,9 +2,12 @@
namespace App\Http\Controllers;
use App\Models\FileUploader;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
class PaymentController extends Controller
{
@ -69,17 +72,6 @@ class PaymentController extends Controller
return redirect()->to($created_payment_link);
}
public function payment_razorpay($identifier)
{
$payment_details = session('payment_details');
$payment_gateway = DB::table('payment_gateways')->where('identifier', $identifier)->first();
$model_name = $payment_gateway->model_name;
$model_full_path = str_replace(' ', '', 'App\Models\payment_gateway\ ' . $model_name);
$data = $model_full_path::payment_create($identifier);
return view('payment.razorpay.payment', compact('data'));
}
public function webRedirectToPayFee(Request $request)
{
// Check if the 'auth' query parameter is present

View File

@ -238,25 +238,16 @@ class SettingController extends Controller
$paypal = json_encode($paypal);
$data = array_splice($data, 0, 4);
$data['keys'] = $paypal;
} elseif ($request->identifier == 'stripe') {
$stripe = [
'public_key' => $data['public_key'],
'secret_key' => $data['secret_key'],
'public_live_key' => $data['public_live_key'],
'secret_live_key' => $data['secret_live_key'],
];
$stripe = json_encode($stripe);
$data = array_splice($data, 0, 4);
$data['keys'] = $stripe;
} elseif ($request->identifier == 'razorpay') {
$razorpay = [
'public_key' => $data['public_key'],
'secret_key' => $data['secret_key'],
} elseif ($request->identifier == 'offline' || $request->identifier == 'virtualaccount') {
$offline = [
'bank_information' => $data['bank_information'],
];
$razorpay = json_encode($razorpay);
$offline = json_encode($offline);
$data = array_splice($data, 0, 4);
$data['keys'] = $razorpay;
$data['keys'] = $offline;
unset($data['bank_information']);
}
Payment_gateway::where('identifier', $request->identifier)->update($data);
}

View File

@ -4,7 +4,9 @@ namespace App\Http\Controllers\student;
use App\Http\Controllers\Controller;
use App\Models\Bootcamp;
use App\Models\OfflinePayment;
use App\Models\BootcampPurchase;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
@ -49,6 +51,20 @@ class BootcampPurchaseController extends Controller
return redirect()->route('my.bootcamps');
}
// check any offline processing data
$processing_payments = OfflinePayment::where([
'user_id' => auth()->user()->id,
'items' => $bootcamp->id,
'item_type' => 'bootcamp',
'status' => 0,
])->first();
if ($processing_payments) {
Session::flash('warning', get_phrase('Your request is in process.'));
return redirect()->back();
}
// prepare bootcamp payment data
$price = $bootcamp->discount_flag ? $bootcamp->price - $bootcamp->discounted_price : $bootcamp->price;
$payment_details = [

View File

@ -4,6 +4,7 @@ namespace App\Http\Controllers\student;
use App\Http\Controllers\Controller;
use App\Models\Enrollment;
use App\Models\OfflinePayment;
use App\Models\TeamPackageMember;
use App\Models\TeamPackagePurchase;
use App\Models\TeamTrainingPackage;
@ -184,6 +185,20 @@ class MyTeamPackageController extends Controller
return redirect()->back();
}
// check any offline processing data
$processing_payments = OfflinePayment::where([
'user_id' => auth()->user()->id,
'items' => $package->id,
'item_type' => 'team_package',
'status' => 0,
])
->first();
if ($processing_payments) {
Session::flash('warning', get_phrase('Your request is in process.'));
return redirect()->back();
}
// prepare team package payment data
$payment_details = [
'items' => [

View File

@ -0,0 +1,68 @@
<?php
namespace App\Http\Controllers\student;
use App\Http\Controllers\Controller;
use App\Models\CartItem;
use App\Models\FileUploader;
use App\Models\OfflinePayment;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
class OfflinePaymentController extends Controller
{
public function store(Request $request)
{
// check amount
$payment_details = Session::get('payment_details');
// dd($payment_details);
$item_id_arr = [];
foreach($payment_details['items'] as $item){
$item_id_arr[] = $item['id'];
}
$rules = [
'doc' => 'required|mimes:jpeg,jpg,pdf,txt,png,docx,doc|max:3072',
];
$validator = Validator::make($request->all(), $rules);
if ($validator->fails()) {
return redirect()->back()->withErrors($validator)->withInput();
}
$file = $request->doc;
$file_name = Str::random(20) . '.' . $file->extension();
$path = 'uploads/offline_payment/' . slugify(auth()->user()->name) . '/' . $file_name;
FileUploader::upload($file, $path, null, null, 300);
$offline_payment['user_id'] = auth()->user()->id;
$offline_payment['item_type'] = $request->item_type;
$offline_payment['items'] = json_encode($item_id_arr);
$offline_payment['tax'] = $payment_details['tax'];
$offline_payment['total_amount'] = $payment_details['payable_amount'];
$offline_payment['doc'] = $path;
$offline_payment['coupon'] = $payment_details['coupon'] ?? null;
// insert offline payment history
OfflinePayment::insert($offline_payment);
// remove from cart
if ($request->item_type == 'course') {
$url = 'purchase.history';
CartItem::whereIn('course_id', $item_id_arr)->where('user_id', auth()->user()->id)->delete();
} elseif ($request->item_type == 'bootcamp') {
$url = 'bootcamps';
} elseif ($request->item_type == 'package') {
$url = 'team.packages';
} elseif ($request->item_type == 'tutor_booking') {
$url = 'tutor_list';
}
// return to courses
Session::flash('success', get_phrase('The payment will be completed once the admin reviews and approves it.'));
return redirect()->route($url);
}
}

View File

@ -7,21 +7,76 @@ use App\Models\CartItem;
use App\Models\Course;
use App\Models\Enrollment;
use App\Models\Payment_history;
use App\Models\VAPayment;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class PurchaseController extends Controller
{
public function purchase_history()
{
$page_data['payments'] = Payment_history::join('courses', 'payment_histories.course_id', 'courses.id')
->join('users', 'payment_histories.user_id', 'users.id')
->where('payment_histories.user_id', auth()->user()->id)
->select('payment_histories.*', 'courses.title as course_title', 'users.name as user_name')
->latest('id')->paginate(10);
$userId = auth()->id();
// 1. Query untuk Paid menggunakan Model Payment_history
$paid = Payment_history::query()
->join('courses', 'payment_histories.course_id', '=', 'courses.id')
->where('payment_histories.user_id', $userId)
->select(
'payment_histories.id',
'courses.title as title',
'payment_histories.amount',
'payment_histories.payment_type',
'payment_histories.created_at',
DB::raw('NULL as expired_at'),
DB::raw('"paid" as payment_status'),
DB::raw('payment_histories.id as invoice_id')
);
// 2. Query untuk Unpaid menggunakan Model VAPayment
$unpaid = VAPayment::query()
->where('user_id', $userId)
->where('status', 0)
->where('expired_at', '>=', now())
->select(
'id',
DB::raw('"Virtual Account BTN" as title'),
'total_amount as amount',
DB::raw('"VA BTN" as payment_type'),
'created_at',
'expired_at',
DB::raw('"unpaid" as payment_status'),
DB::raw('NULL as invoice_id')
);
// 3. Query untuk Failed menggunakan Model VAPayment
$failed = VAPayment::query()
->where('user_id', $userId)
->where('status', 0)
->where('expired_at', '<', now())
->select(
'id',
DB::raw('"Virtual Account BTN" as title'),
'total_amount as amount',
DB::raw('"VA BTN" as payment_type'),
'created_at',
'expired_at',
DB::raw('"failed" as payment_status'),
DB::raw('NULL as invoice_id')
);
// 4. Gabungkan (Union) dan Eksekusi
$payments = $paid
->unionAll($unpaid)
->unionAll($failed)
->orderBy('created_at', 'desc')
->get();
$view_path = 'frontend.' . get_frontend_settings('theme') . '.student.purchase_history.index';
return view($view_path, $page_data);
return view($view_path, compact('payments'));
}
public function invoice($id)

View File

@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
use App\Models\TutorBooking;
use App\Models\TutorSchedule;
use App\Models\TutorReview;
use App\Models\OfflinePayment;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Auth;
@ -54,6 +55,20 @@ class TutorBookingController extends Controller
return redirect()->back();
}
// check any offline processing data
$processing_payments = OfflinePayment::where([
'user_id' => auth()->user()->id,
'items' => $schedule->id,
'item_type' => 'tutor_booking',
'status' => 0,
])
->first();
if ($processing_payments) {
Session::flash('warning', get_phrase('Your request is in process.'));
return redirect()->back();
}
// prepare team package payment data
$payment_details = [
'items' => [

View File

@ -0,0 +1,328 @@
<?php
namespace App\Http\Controllers\student;
use App\Events\PaymentCompleted;
use App\Http\Controllers\Controller;
use App\Models\CartItem;
use App\Models\Payment_history;
use App\Models\VAPayment;
use App\Notifications\CoursePurchaseCreated;
use App\Models\Course;
use App\Models\Enrollment;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Carbon\Carbon;
class VAPaymentController extends Controller
{
public function store(Request $request)
{
$request->validate([
'item_type' => 'required|string',
]);
$payment_details = Session::get('payment_details');
if (!$payment_details) {
return back()->with('error', 'Payment session not found.');
}
$item_id_arr = collect($payment_details['items'])->pluck('id')->toArray();
// BTN REQUIREMENT
$noTest = str_pad(random_int(0, 99999), 5, '0', STR_PAD_LEFT);
$nama = auth()->user()->name;
$tagihan = $payment_details['payable_amount'];
$expiredAt = Carbon::now()->addHour();
DB::beginTransaction();
try {
$response = $this->generateDummyVA(
$noTest,
$tagihan,
$nama,
$expiredAt->format('Y-m-d H:i:s')
);
$btnResponse = json_decode($response, true);
Log::info('BTN VA Response', $btnResponse ?? []);
$vaNumber = $btnResponse['BTNVirtualAccount'];
if (!$vaNumber) {
throw new \Exception('BTN VA failed: ' . $response);
}
$vaPayment = VAPayment::create([
'user_id' => auth()->id(),
'item_type' => $request->item_type,
'items' => json_encode($item_id_arr),
'tax' => $payment_details['tax'],
'total_amount' => $tagihan,
'coupon' => $payment_details['coupon'] ?? null,
'va_number' => $vaNumber,
'status' => 0,
'expired_at' => $expiredAt
]);
if ($request->item_type === 'course') {
// Hapus dari cart
CartItem::whereIn('course_id', $item_id_arr)
->where('user_id', auth()->id())
->delete();
$courses = Course::whereIn('id', $item_id_arr)->get();
auth()->user()->notify(
new CoursePurchaseCreated($vaPayment, $courses)
);
}
DB::commit();
return redirect()
->route('payment.va.show', $vaPayment->id)
->with('success', 'Virtual Account berhasil dibuat.');
} catch (\Throwable $e) {
DB::rollBack();
Log::error('VA Payment Error', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return back()->with('error', 'Gagal membuat Virtual Account: ' . $e->getMessage());
}
}
private function generateVAFast($notest, $tagihan, $nama, $tgl_terakhirbayar)
{
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => 'https://neosidata.unesa.ac.id/btn_v2/create',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => [
'credential' => '$1$bCE4xLfB$U7t8ux0g4iflFaCpcLqaB.',
'noid' => $notest,
'nama' => $nama,
'tagihan' => $tagihan,
'flag' => 'F',
'expired_date' => $tgl_terakhirbayar,
'deskripsi' => 'Pembayaran Fast Track',
],
]);
$response = curl_exec($curl);
curl_close($curl);
return $response;
}
//untuk testing
private function generateDummyVA($notest, $tagihan, $nama, $tgl_terakhirbayar)
{
// Prefix BTN: 9422 + 5 digit random + 8 digit noTest (total 17 digit)
// Format: 9422 + XXXXX + 00000 + noTest (5 digit)
// Buat angka random 5 digit untuk middle part
$middle = str_pad(random_int(0, 99999), 5, '0', STR_PAD_LEFT);
// Pad noTest dengan leading zeros jika kurang dari 5 digit
$paddedNoTest = str_pad($notest, 5, '0', STR_PAD_LEFT);
// Generate VA number (17 digit)
$vaNumber = '9422' . $middle . '0' . $paddedNoTest; // 4 + 5 + 1 + 5 = 15 digit
// Tambah 2 digit random untuk genapin 17 digit
$vaNumber .= str_pad(random_int(0, 99), 2, '0', STR_PAD_LEFT);
// Format response seperti aslinya
$dummyResponse = [
'BTNVirtualAccount' => $vaNumber,
'status' => '00',
'message' => 'Success',
'data' => [
'va' => $vaNumber,
'nama' => $nama,
'tagihan' => $tagihan,
'expired_date' => $tgl_terakhirbayar,
'bank' => 'BTN',
'deskripsi' => 'Pembayaran Fast Track'
],
'timestamp' => now()->toISOString()
];
Log::info('DUMMY VA GENERATED', [
'va_number' => $vaNumber,
'notest' => $notest,
'padded_notest' => $paddedNoTest,
'total_digits' => strlen($vaNumber)
]);
return json_encode($dummyResponse);
}
public function show($id)
{
$vaPayment = VAPayment::where('id', $id)
->where('user_id', auth()->user()->id)
->firstOrFail();
return view('payment.va_show', compact('vaPayment'));
}
public function checkPaymentApi($id)
{
Log::info('CHECK PAYMENT STARTED');
$payment = VAPayment::where('id', $id)
->where('user_id', auth()->id())
->firstOrFail();
if ($payment->status == 1) {
return response()->json([
'status' => 1,
'message' => 'Already paid'
]);
}
if (now()->greaterThan($payment->expired_at)) {
$payment->update(['status' => 2]);
return response()->json([
'status' => 2,
'message' => 'Expired'
]);
}
try {
$fullVa = $payment->va_number;
$noidcek = substr($fullVa, 5);
$url = "https://neosidata.unesa.ac.id/api-va-narkoba/{$noidcek}/{$noidcek}";
$response = Http::timeout(15)
->withOptions(['verify' => false])
->get($url);
if (!$response->successful()) {
return response()->json([
'status' => 0,
'message' => 'BTN API error'
]);
}
$data = $response->json();
$isPaid = is_numeric($data['terbayar'] ?? 0)
&& floatval($data['terbayar']) >= floatval($data['tagihan'] ?? 0);
if (!$isPaid) {
return response()->json([
'status' => 0,
'message' => 'Unpaid'
]);
}
DB::beginTransaction();
// 1⃣ Update VA Payment
$payment->update([
'status' => 1,
'paid_at' => now()
]);
// 2⃣ Insert ke payment_history
if ($payment->item_type === 'course') {
$items = json_decode($payment->items);
foreach ($items as $courseId) {
$course = Course::findOrFail($courseId);
$amount = $course->discount_flag
? $course->discounted_price
: $course->price;
// Coupon
$discount = 0;
if ($payment->coupon) {
$coupon = Coupon::where('code', $payment->coupon)->first();
if ($coupon) {
$discount = $amount * ($coupon->discount / 100);
}
}
$finalAmount = $amount - $discount;
$tax = (get_settings('course_selling_tax') / 100) * $finalAmount;
$history = [
'invoice' => Str::random(20),
'user_id' => $payment->user_id,
'payment_type' => 'virtual_account',
'course_id' => $course->id,
'amount' => $finalAmount,
'tax' => $tax,
'coupon' => $payment->coupon,
];
// Revenue split
if (get_course_creator_id($course->id)->role === 'admin') {
$history['admin_revenue'] = $finalAmount;
} else {
$history['instructor_revenue'] =
$finalAmount * (get_settings('instructor_revenue') / 100);
$history['admin_revenue'] =
$finalAmount - $history['instructor_revenue'];
}
// Payment_history::insert($history);
// // 3⃣ Enrollment
// Enrollment::insert([
// 'user_id' => $payment->user_id,
// 'course_id' => $course->id,
// 'enrollment_type' => 'paid',
// 'entry_date' => time(),
// 'expiry_date' => $course->expiry_period > 0
// ? strtotime('+' . ($course->expiry_period * 30) . ' days')
// : null,
// 'created_at' => now(),
// 'updated_at' => now(),
// ]);
}
}
DB::commit();
// 4⃣ Event & Notification
event(new PaymentCompleted($payment));
return response()->json([
'status' => 1,
'message' => 'Payment successful'
]);
} catch (\Throwable $e) {
DB::rollBack();
Log::error('VA CHECK ERROR', [
'payment_id' => $payment->id,
'error' => $e->getMessage()
]);
return response()->json([
'status' => 0,
'message' => 'Check failed'
], 500);
}
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Listeners;
use App\Events\PaymentCompleted;
use App\Models\Course;
use App\Notifications\CoursePaid;
class SendVirtualAccountPaidNotification
{
public function handle(PaymentCompleted $event): void
{
$payment = $event->payment;
$user = $payment->user;
$courseIds = json_decode($payment->items, true);
$courses = Course::whereIn('id', $courseIds)->get();
$user->notify(
new CoursePaid($payment, $courses)
);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class OfflinePayment extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'item_type',
'items',
'tax',
'total_amount',
'coupon',
'phone_no',
'bank_no',
'doc',
'status',
];
}

37
app/Models/VAPayment.php Normal file
View File

@ -0,0 +1,37 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class VAPayment extends Model
{
use HasFactory;
protected $table = 'va_payments';
protected $fillable = [
'user_id',
'item_type',
'items',
'tax',
'total_amount',
'coupon',
'va_number',
'status',
'expired_at',
'paid_at',
];
protected $casts = [
'expired_at' => 'datetime',
'paid_at' => 'datetime',
];
public function user()
{
return $this->belongsTo(User::class);
}
}

View File

@ -1,110 +0,0 @@
<?php
namespace App\Models\payment_gateway;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Razorpay\Api\Api;
class Razorpay extends Model
{
use HasFactory;
public static function payment_status($identifier, $transaction_keys = [])
{
$payment_gateway = DB::table('payment_gateways')->where('identifier', $identifier)->first();
$keys = json_decode($payment_gateway->keys, true);
$public_key = $keys['public_key'];
$secret_key = $keys['secret_key'];
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => 'https://api.razorpay.com/v1/payments/' . $transaction_keys['razorpay_payment_id'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 0,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => array(
'Authorization: Basic ' . base64_encode($public_key . ':' . $keys['secret_key'])
),
));
$response = curl_exec($curl);
curl_close($curl);
$response = json_decode($response);
if ($response->status == 'captured' || $response->status == 'success') {
return true;
}
}
public static function payment_create($identifier)
{
$payment_details = session('payment_details');
$user = DB::table('users')->where('id', auth()->user()->id)->first();
$model = $payment_details['success_method']['model_name'];
$payment_gateway = DB::table('payment_gateways')
->where('identifier', $identifier)
->first();
if ($model == 'InstructorPayment') {
$settings = DB::table('users')->where('id', $payment_details['items'][0]['id'])
->value('paymentkeys');
$keys = isset($settings) ? json_decode($settings) : null;
$public_key = $secret_key = '';
if ($keys) {
$public_key = $keys->razorpay->public_key;
$secret_key = $keys->razorpay->secret_key;
}
} else {
$keys = json_decode($payment_gateway->keys, true);
$public_key = $keys['public_key'];
$secret_key = $keys['secret_key'];
$color = '';
}
$receipt_id = Str::random(20);
$api = new Api($public_key, $secret_key);
$order = $api->order->create(array(
'receipt' => $receipt_id,
'amount' => round($payment_details['payable_amount'] * 100, 2),
'currency' => $payment_gateway->currency,
));
$page_data = [
'order_id' => $order['id'],
'razorpay_id' => $public_key,
'amount' => round($payment_details['payable_amount'] * 100, 2),
'name' => $user->name,
'currency' => $payment_gateway->currency,
'email' => $user->email,
'phone' => $user->phone,
'address' => $user->address,
'description' => isset($payment_details['custom_field']['description']) ? $payment_details['custom_field']['description'] : '',
];
$data = [
'page_data' => $page_data,
'color' => null,
'payment_details' => $payment_details,
];
return $data;
}
}

View File

@ -1,119 +0,0 @@
<?php
namespace App\Models\payment_gateway;
use App\Http\Requests;
use DB;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
//for stripe
use Session;
use Stripe;
class StripePay extends Model
{
use HasFactory;
public static function payment_status($identifier, $transaction_keys = [])
{
$payment_gateway = DB::table('payment_gateways')->where('identifier', $identifier)->first();
$keys = json_decode($payment_gateway->keys, true);
if ($payment_gateway->test_mode == 1) :
$stripeSecretKey = $keys['secret_key'];
else :
$stripeSecretKey = $keys['secret_live_key'];
endif;
// Check whether stripe checkout session is not empty
$session_id = $transaction_keys['session_id'];
if ($session_id != "") {
// Set API key
\Stripe\Stripe::setApiKey($stripeSecretKey);
// Fetch the Checkout Session to display the JSON result on the success page
try {
$checkout_session = \Stripe\Checkout\Session::retrieve($session_id);
} catch (Exception $e) {
$api_error = $e->getMessage();
}
if (empty($api_error) && $checkout_session) {
// Retrieve the details of a PaymentIntent
try {
$intent = \Stripe\PaymentIntent::retrieve($checkout_session->payment_intent);
} catch (\Stripe\Exception\ApiErrorException $e) {
$api_error = $e->getMessage();
}
if ($intent) {
// Check whether the charge is successful
if ($intent->status == 'succeeded') {
Session::put(['session_id' => $transaction_keys['session_id']]);
return true;
} else {
return false;
}
} else {
return false;
}
} else {
return false;
}
} else {
return false;
}
return false;
}
public static function payment_create($identifier)
{
$payment_gateway = DB::table('payment_gateways')->where('identifier', $identifier)->first();
$payment_details = session('payment_details');
$keys = json_decode($payment_gateway->keys, true);
$products_name = '';
foreach ($payment_details['items'] as $key => $value) :
if ($key == 0) {
$products_name .= $value['title'];
} else {
$products_name .= ', ' . $value['title'];
}
endforeach;
if ($payment_gateway->test_mode == 1) :
$stripeSecretKey = $keys['secret_key'];
else :
$stripeSecretKey = $keys['secret_live_key'];
endif;
\Stripe\Stripe::setApiKey($stripeSecretKey);
header('Content-Type: application/json');
$YOUR_DOMAIN = 'http://localhost:4242';
$checkout_session = \Stripe\Checkout\Session::create([
'line_items' => [
[
'price_data' => [
'product_data' => [
'name' => get_phrase('Purchasing') . ' ' . $products_name,
],
'unit_amount' => round($payment_details['payable_amount'] * 100, 2),
'currency' => $payment_gateway->currency,
],
'quantity' => 1,
],
],
'mode' => 'payment', //Checkout has three modes: payment, subscription, or setup. Use payment mode for one-time purchases. Learn more about subscription and setup modes in the docs.
'success_url' => $payment_details['success_url'] . '/' . $identifier . '?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => $payment_details['cancel_url'],
]);
return $checkout_session->url;
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Notifications;
use App\Models\VAPayment;
use App\Models\Course;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class CoursePaid extends Notification implements ShouldQueue
{
use Queueable;
public function __construct(
public $payment,
public $courses
) {}
public function via(object $notifiable): array
{
return ['mail'];
}
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject(get_phrase('Virtual Account Payment Successful') . ' - ' . config('app.name'))
->view('vendor.notifications.course_paid', [
'user' => $notifiable,
'payment' => $this->payment,
'courses' => $this->courses,
]);
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class CoursePurchaseCreated extends Notification
{
use Queueable;
protected $payment;
protected $courses;
public function __construct($payment, $courses)
{
$this->payment = $payment;
$this->courses = $courses;
}
public function via(object $notifiable): array
{
return ['mail'];
}
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject(get_phrase('Course Purchase Created') . ' - ' . config('app.name'))
->view('vendor.notifications.course_purchased_created', [
'user' => $notifiable,
'payment' => $this->payment,
'courses' => $this->courses,
]);
}
}

View File

@ -2,35 +2,21 @@
namespace App\Providers;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;
class EventServiceProvider extends ServiceProvider
{
/**
* The event to listener mappings for the application.
*
* @var array<class-string, array<int, class-string>>
*/
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
\App\Events\PaymentCompleted::class => [
\App\Listeners\SendVirtualAccountPaidNotification::class,
],
];
/**
* Register any events for your application.
*/
public function boot(): void
{
//
}
/**
* Determine if events and listeners should be automatically discovered.
*/
public function shouldDiscoverEvents(): bool
{
return false;

View File

@ -4035,8 +4035,8 @@ html .ui-button.ui-state-disabled:active {
}
.ps-sidebar .learn-btn:hover {
background: linear-gradient( to right, #2f57ef 0%, #c664ff 51%, #c664ff 100%);
color: var(--color-white);
background: linear-gradient(45deg, rgba(2, 25, 110, 1) 0%, rgba(2, 25, 110, 1) 13%, rgba(217, 217, 217, 1) 65%, rgba(255, 255, 255, 1) 98%);
color: white !important;
border-color: transparent;
transition: 0.5s;
background-size: 200% auto;

View File

@ -210,10 +210,6 @@ Common styles END
background-color: #797c8b !important;
}
.bg-light {
background-color: #dedede !important;
}
.color-success {
color: #50cd89 !important;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -37,6 +37,7 @@
'admin.newsletter' => get_phrase('Newsletter'),
'admin.subscribed_user' => get_phrase('Newsletter Subscriber'),
'admin.contacts' => get_phrase('Contact User'),
'admin.offline.payments' => get_phrase('Offline Payment'),
'admin.coupons' => get_phrase('Coupon'),
'admin.blogs' => get_phrase('Blog'),
'admin.pending.blog' => get_phrase('Pending Blog List'),

View File

@ -171,11 +171,12 @@
@endif
@if ( has_permission('admin.revenue') ||
@if (has_permission('admin.offline.payments') ||
has_permission('admin.revenue') ||
has_permission('admin.instructor.revenue') ||
has_permission('admin.purchase.history'))
<li
class="sidebar-first-li first-li-have-sub {{ $current_route == 'admin.revenue' || $current_route == 'admin.instructor.revenue' || $current_route == 'admin.purchase.history' || $current_route == 'admin.purchase.history.invoice' ? 'active' : '' }}">
class="sidebar-first-li first-li-have-sub {{ $current_route == 'admin.offline.payments' || $current_route == 'admin.revenue' || $current_route == 'admin.instructor.revenue' || $current_route == 'admin.purchase.history' || $current_route == 'admin.purchase.history.invoice' ? 'active' : '' }}">
<a href="javascript:void(0);">
<span class="icon fi-rr-comment-dollar"></span>
<div class="text">
@ -185,6 +186,14 @@
<ul class="first-sub-menu">
<li class="first-sub-menu-title fs-14px mb-18px">{{ get_phrase('Payment Report') }}</li>
@if (has_permission('admin.offline.payments'))
<li
class="sidebar-second-li {{ $current_route == 'admin.offline.payments' ? 'active' : '' }}">
<a
href="{{ route('admin.offline.payments') }}">{{ get_phrase('Offline payments') }}</a>
</li>
@endif
@if (has_permission('admin.revenue'))
<li class="sidebar-second-li {{ $current_route == 'admin.revenue' ? 'active' : '' }}"><a
href="{{ route('admin.revenue') }}">{{ get_phrase('Admin Revenue') }}</a></li>

View File

@ -0,0 +1,236 @@
@extends('layouts.admin')
@push('title', get_phrase('Offline payments'))
@push('meta')@endpush
@push('css')@endpush
@section('content')
<!-- Mani section header and breadcrumb -->
<div class="ol-card radius-8px print-d-none">
<div class="ol-card-body px-20px my-3 py-4">
<div class="d-flex align-items-center justify-content-between flex-md-nowrap flex-wrap gap-3">
<h4 class="title fs-16px">
<i class="fi-rr-settings-sliders me-2"></i>
<span>{{ get_phrase('Offline payments') }}</span>
</h4>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="ol-card">
<div class="ol-card-body p-3">
<div class="row print-d-none row-gap-3 mb-3 mt-3">
<div class="col-md-6 d-flex align-items-center gap-3">
<div class="custom-dropdown">
<button class="dropdown-header btn ol-btn-light">
{{ get_phrase('Export') }}
<i class="fi-rr-file-export ms-2"></i>
</button>
<ul class="dropdown-list">
<li>
<a class="dropdown-item" href="#" onclick="downloadPDF('.print-table', 'offline-payments')"><i class="fi-rr-file-pdf"></i>
{{ get_phrase('PDF') }}</a>
</li>
<li>
<a class="dropdown-item" href="#" onclick="window.print();"><i class="fi-rr-print"></i> {{ get_phrase('Print') }}</a>
</li>
</ul>
</div>
<div class="custom-dropdown dropdown-filter @if (!isset($_GET) || (isset($_GET) && count($_GET) == 0)) @endif">
<button class="dropdown-header btn ol-btn-light">
<i class="fi-rr-filter me-2"></i>
{{ get_phrase('Filter') }}
@if (isset($_GET) && count($_GET))
<span class="text-12px">
({{ count($_GET) }})
</span>
@endif
</button>
<ul class="dropdown-list w-250px">
<li>
<form id="filter-dropdown" action="{{ route('admin.offline.payments') }}" method="get">
<div class="filter-option d-flex flex-column gap-3">
<div>
<label for="eDataList" class="form-label ol-form-label">{{ get_phrase('Category') }}</label>
<select class="form-control ol-form-control ol-select2" data-toggle="select2" name="status" data-placeholder="Type to search...">
<option value="all">{{ get_phrase('All') }}</option>
<option value="pending" @if (isset($_GET['status']) && $_GET['status'] == 'pending') selected @endif>{{ get_phrase('Pending') }}</option>
<option value="approved" @if (isset($_GET['status']) && $_GET['status'] == 'approved') selected @endif>{{ get_phrase('Approved') }}</option>
<option value="suspended" @if (isset($_GET['status']) && $_GET['status'] == 'suspended') selected @endif>{{ get_phrase('Suspended') }}</option>
</select>
</div>
</div>
<div class="filter-button d-flex justify-content-end align-items-center mt-3">
<button type="submit" class="ol-btn-primary">{{ get_phrase('Apply') }}</button>
</div>
</form>
</li>
</ul>
</div>
@if (isset($_GET) && count($_GET) > 0)
<a href="{{ route('admin.offline.payments') }}" class="me-2" data-bs-toggle="tooltip" title="{{ get_phrase('Clear') }}"><iclass="fi-rr-cross-circle"></iclass=></a>
@endif
</div>
<div class="col-md-6">
</div>
</div>
<!-- Table -->
@if (count($payments) > 0)
<div class="admin-tInfo-pagi d-flex justify-content-between justify-content-center align-items-center gr-15 flex-wrap">
<p class="admin-tInfo">
{{ get_phrase('Showing') . ' ' . count($payments) . ' ' . get_phrase('of') . ' ' . $payments->total() . ' ' . get_phrase('data') }}
</p>
</div>
<div class="table-responsive course_list" id="course_list">
<table class="eTable eTable-2 print-table table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">{{ get_phrase('User') }}</th>
<th scope="col">{{ get_phrase('Items') }}</th>
<th scope="col">{{ get_phrase('Type') }}</th>
<th scope="col">{{ get_phrase('Total') }}</th>
<th scope="col">{{ get_phrase('Issue date') }}</th>
<th scope="col">{{ get_phrase('Payment info') }}</th>
<th scope="col">{{ get_phrase('Status') }}</th>
<th scope="col" class="print-d-none">{{ get_phrase('Options') }}</th>
</tr>
</thead>
<tbody>
@foreach ($payments as $key => $payment)
<tr>
<th scope="row">
<p class="row-number">{{ $key + 1 }}</p>
</th>
<td>
<div class="dAdmin_profile d-flex align-items-center min-w-200px">
<div class="dAdmin_profile_name">
<h4 class="title fs-14px">
{{ get_user_info($payment->user_id)->name }}
</h4>
<p class="sub-title text-12px">{{ get_user_info($payment->user_id)->email }}</p>
<p class="sub-title text-12px">{{get_phrase('Phone')}}: {{ get_user_info($payment->user_id)->phone }}</p>
</div>
</div>
</td>
<td>
<div class="dAdmin_profile d-flex align-items-center min-w-200px">
<div class="dAdmin_profile_name">
@if ($payment->item_type == 'course')
@foreach (App\Models\Course::whereIn('id', json_decode($payment->items, true))->get() as $course)
<p class="sub-title text-12px">
<a href="{{ route('course.details', slugify($course->title)) }}" class="text-muted me-3">{{ $course->title }} </a>
</p>
@endforeach
@endif
@if ($payment->item_type == 'bootcamp')
@foreach (App\Models\Bootcamp::whereIn('id', json_decode($payment->items, true))->get() as $bootcamp)
<p class="sub-title text-12px">
<a href="{{ route('bootcamp.details', ['slug' => slugify($bootcamp->title)]) }}" class="text-muted me-3">{{ $bootcamp->title }} </a>
</p>
@endforeach
@endif
@if ($payment->item_type == 'package')
@foreach (App\Models\TeamTrainingPackage::whereIn('id', json_decode($payment->items, true))->get() as $package)
<p class="sub-title text-12px">
<a href="{{ route('team.package.details', ['slug' => $package->slug]) }}" class="text-muted me-3">{{ $package->title }} </a>
</p>
@endforeach
@endif
@if ($payment->item_type == 'tutor_booking')
@foreach (App\Models\TutorSchedule::whereIn('id', json_decode($payment->items, true))->get() as $tutor_schedule)
<p class="sub-title text-12px">
{{ $tutor_schedule->schedule_to_tutorSubjects->name }}
</p>
<small><a href="{{ route('tutor_schedule', [$tutor_schedule->tutor_id, slugify($tutor_schedule->schedule_to_tutor->name)]) }}" target="_blank" class="text-muted me-3">{{ $tutor_schedule->schedule_to_tutor->name }} </a></small>
@endforeach
@endif
</div>
</div>
</td>
<td>
<span class="badge bg-info">{{ ucfirst($payment->item_type) }}</span>
</td>
<td>
<div class="sub-title2 text-12px">
{{ currency($payment->total_amount) }}
</div>
</td>
<td>
<div class="sub-title2 text-12px">
<p>{{ date('d-M-y', strtotime($payment->created_at)) }}</p>
</div>
</td>
<td>
<a class="dropdown-item btn ol-btn-primary px-2 py-1" href="{{ route('admin.offline.payment.doc', $payment->id) }}"><i class="fi-rr-cloud-download"></i> {{ get_phrase('Download') }}</a>
</td>
<td>
@if ($payment->status == 1)
<span class="badge bg-success">{{ get_phrase('Accepted') }}</span>
@elseif($payment->status == 2)
<span class="badge bg-danger">{{ get_phrase('Suspended') }}</span>
@else
<span class="badge bg-warning">{{ get_phrase('Pending') }}</span>
@endif
</td>
<td class="print-d-none">
<div class="dropdown ol-icon-dropdown ol-icon-dropdown-transparent">
<button class="btn ol-btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<span class="fi-rr-menu-dots-vertical"></span>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ route('admin.offline.payment.doc', $payment->id) }}">{{ get_phrase('Download') }}</a>
</li>
@if ($payment->status != 1)
<li><a class="dropdown-item" href="{{ route('admin.offline.payment.accept', $payment->id) }}">{{ get_phrase('Accept') }}</a>
</li>
@endif
@if ($payment->status != 2 && $payment->status != 1)
<li><a class="dropdown-item" href="#" onclick="confirmModal('{{ route('admin.offline.payment.decline', $payment->id) }}')">{{ get_phrase('Decline') }}</a>
</li>
@endif
<li><a class="dropdown-item" href="#" onclick="confirmModal('{{ route('admin.offline.payment.delete', $payment->id) }}')">{{ get_phrase('Delete') }}</a>
</li>
</ul>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@else
@include('admin.no_data')
@endif
<!-- Data info and Pagination -->
@if (count($payments) > 0)
<div class="admin-tInfo-pagi d-flex justify-content-between justify-content-center align-items-center gr-15 flex-wrap">
<p class="admin-tInfo">
{{ get_phrase('Showing') . ' ' . count($payments) . ' ' . get_phrase('of') . ' ' . $payments->total() . ' ' . get_phrase('data') }}
</p>
{{ $payments->links() }}
</div>
@endif
</div>
</div>
</div>
</div>
@endsection()

View File

@ -97,7 +97,7 @@
@csrf
<input type="hidden" name="identifier" value="{{ $payment_gateway->identifier }}">
@if ($payment_gateway->identifier != 'offline')
@if ($payment_gateway->identifier != 'offline' && $payment_gateway->identifier != 'virtualaccount')
<div class="fpb-7 mb-3">
<label class="mb-2 text-capitalize">{{ get_phrase('Active') }}</label>
<select class="form-control ol-form-control ol-select2" data-toggle="select2" name="status">

View File

@ -99,6 +99,17 @@
$btn['title'] = get_phrase('In Collection');
$btn['url'] = route('my.bootcamp.details', $bootcamp->slug);
}
$pending_payment = DB::table('offline_payments')
->where('user_id', auth()->user()->id)
->where('item_type', 'bootcamp')
->where('items', $bootcamp->id)
->where('status', 0)
->first();
if ($pending_payment) {
$btn['title'] = get_phrase('Processing');
$btn['url'] = 'javascript:void(0);';
}
}
@endphp
<a href="{{ $btn['url'] }}"

View File

@ -21,6 +21,12 @@
->where('status', 1)
->exists();
}
$pending_bootcamp_payment = DB::table('offline_payments')
->where('user_id', auth()->user()->id)
->where('item_type', 'bootcamp')
->where('items', $bootcamp_details->id)
->where('status', 0)
->first();
@endphp
@if (isset(auth()->user()->id))

View File

@ -85,26 +85,67 @@
</div>
@php
$watch_history = App\Models\Watch_history::where('course_id', $course->course_id)
->where('student_id', auth()->user()->id)
$student_id = auth()->user()->id;
$course_id = $course->course_id;
$url = null;
$watch_history = App\Models\Watch_history::where('course_id', $course_id)
->where('student_id', $student_id)
->first();
$lesson = App\Models\Lesson::where('course_id', $course->course_id)
$lesson = App\Models\Lesson::where('course_id', $course_id)
->orderBy('section_id', 'asc')
->orderBy('sort', 'asc')
->first();
$total_lesson = App\Models\Lesson::where('course_id', $course_id)
->pluck('id')
->toArray();
if (!$watch_history && !$lesson) {
$url = route('course.player', ['slug' => $course->slug]);
} else {
if ($watch_history) {
$lesson_id = $watch_history->watching_lesson_id;
} elseif ($lesson) {
$lesson_id = $lesson->id;
}
$url = route('course.player', ['slug' => $course->slug, 'id' => $lesson_id]);
if ($watch_history && $watch_history->id) {
// UPDATE (sebenarnya jarang masuk sini, tapi disamakan dgn referensi)
$lessons = (array) json_decode($watch_history->completed_lesson);
if (!in_array($lesson_id, $lessons)) {
$lessons[] = $lesson_id;
}
App\Models\Watch_history::where('course_id', $course_id)
->where('student_id', $student_id)
->update([
'completed_lesson' => json_encode($lessons),
'watching_lesson_id' => $lesson_id,
'completed_date' => count($total_lesson) == count($lessons) ? time() : null,
]);
} else {
App\Models\Watch_history::insert([
'course_id' => $course_id,
'student_id' => $student_id,
'completed_lesson' => json_encode([$lesson_id]),
'watching_lesson_id' => $lesson_id,
'completed_date' => count($total_lesson) == 1 ? time() : null,
]);
}
}
$url = route('course.player', [
'slug' => $course->slug,
'id' => $lesson_id
]);
}
@endphp
@if($course->expiry_date > 0 && $course->expiry_date < time())

View File

@ -1,43 +1,96 @@
@extends('layouts.default')
@push('title', get_phrase('Purchase History'))
@push('meta')@endpush
@push('css')@endpush
@section('content')
<section class="wishlist-content">
<div class="profile-banner-area"></div>
<div class="container profile-banner-area-container">
<div class="row">
@include('frontend.default.student.left_sidebar')
<div class="col-lg-9">
<h4 class="g-title mb-5">{{ get_phrase('Payment History') }}</h4>
<h4 class="g-title mb-4">{{ get_phrase('Payment History') }}</h4>
<div class="mb-4 d-flex gap-2 flex-wrap">
<button class="btn btn-outline-secondary filter-btn" data-status="all">
{{ get_phrase('All') }}
</button>
<button class="btn btn-outline-warning filter-btn" data-status="unpaid">
{{ get_phrase('Unpaid') }}
</button>
<button class="btn btn-outline-success filter-btn" data-status="paid">
{{ get_phrase('Paid') }}
</button>
<button class="btn btn-outline-danger filter-btn" data-status="failed">
{{ get_phrase('Failed') }}
</button>
</div>
<div class="my-panel purchase-history-panel">
@if ($payments->count() > 0)
<div class="table-responsive">
<table class="table eTable">
<thead>
<tr>
<th>{{ get_phrase('Course Name') }}</th>
<th>{{ get_phrase('Item') }}</th>
<th>{{ get_phrase('Date') }}</th>
<th>{{ get_phrase('Payment Method') }}</th>
<th>{{ get_phrase('Price') }}</th>
<th>{{ get_phrase('Invoice') }}</th>
<th>{{ get_phrase('Status') }}</th>
<th>{{ get_phrase('Action') }}</th>
</tr>
</thead>
<tbody>
<tbody id="paymentTable">
@foreach ($payments as $payment)
<tr>
<td>{{ $payment->course_title }}</td>
<td>{{ date('Y-m-d', strtotime($payment->created_at)) }}</td>
<td>{{ ucfirst($payment->payment_type) }}</td>
<td>{{ currency($payment->amount) }}</td>
<tr data-status="{{ $payment->payment_status }}">
<td>{{ $payment->title }}</td>
<td>
<a href="{{ route('invoice', $payment->id) }}"
class="d-flex align-items-center justify-content-center btn btn-primary text-18 text-white py-3" data-bs-toggle="tooltip" data-bs-title="{{get_phrase('Print Invoice')}}">
<i class="fi fi-rr-print d-inline-flex"></i>
{{ date('Y-m-d', strtotime($payment->created_at)) }}
</td>
<td>
{{ ucfirst($payment->payment_type) }}
</td>
<td>
{{ currency($payment->amount) }}
</td>
<td>
@if ($payment->payment_status === 'paid')
<span class="badge bg-success">PAID</span>
@elseif ($payment->payment_status === 'unpaid')
<span class="badge bg-warning">UNPAID</span>
@else
<span class="badge bg-danger">FAILED</span>
@endif
</td>
<td>
@if ($payment->payment_status === 'paid')
<a href="{{ route('student.invoice', $payment->invoice_id) }}"
class="btn btn-sm btn-primary d-flex align-items-center justify-content-center"
style="width: 42px; height: 42px;"
data-bs-toggle="tooltip"
title="{{ get_phrase('Print Invoice') }}">
<i class="fi fi-rr-print fs-4"></i>
</a>
@elseif ($payment->payment_status === 'unpaid')
<a href="{{ route('payment.va.show', $payment->id) }}"
class="btn btn-sm btn-warning d-flex align-items-center justify-content-center"
style="width: 42px; height: 42px;"
data-bs-toggle="tooltip"
title="{{ get_phrase('Pay') }}">
<i class="fi fi-rr-credit-card fs-4"></i>
</a>
@else
<span class="text-muted">-</span>
@endif
</td>
</tr>
@endforeach
@ -46,7 +99,7 @@
</div>
@else
<div class="row bg-white radius-10 mx-2">
<div class="com-md-12">
<div class="col-md-12">
@include('frontend.default.empty')
</div>
</div>
@ -54,18 +107,24 @@
</div>
</div>
</div>
<!-- Pagination -->
@if (count($payments) > 0)
<div class="entry-pagination">
<nav aria-label="Page navigation example">
{{ $payments->links() }}
</nav>
</div>
@endif
<!-- Pagination -->
</div>
</section>
<!------------ purchase history area End ------------>
@endsection
@push('js')@endpush
@push('js')
<script>
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', function () {
const status = this.dataset.status;
document.querySelectorAll('#paymentTable tr').forEach(row => {
if (status === 'all' || row.dataset.status === status) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
});
});
</script>
@endpush

Binary file not shown.

View File

@ -0,0 +1,56 @@
@php $amount = $payment_details['payable_amount']; @endphp
@php
$model = $payment_details['success_method']['model_name'];
if ($model == 'InstructorPayment') {
$settings = DB::table('users')
->where('id', $payment_details['items'][0]['id'])
->value('paymentkeys');
$keys = isset($settings) ? json_decode($settings) : null;
if ($keys) {
$bank_information = $keys->offline->bank_information;
}
if ($bank_information == '') {
$msg = "This payment gateway isn't configured.";
}
} else {
$payment_keys = json_decode($payment_gateway->keys, true);
$bank_information = '';
if ($payment_keys != '') {
if ($payment_gateway->status == 1) {
$bank_information = $payment_keys['bank_information'];
if ($bank_information == '') {
$msg = get_phrase("This payment gateway isn't configured.");
}
} else {
$msg = get_phrase('Admin denied transaction through this gateway.');
}
} else {
$msg = get_phrase("This payment gateway isn't configured.");
}
}
@endphp
<div class="row my-5">
<div class="col-md-12 text-start">
{!! removeScripts($bank_information) !!}
</div>
</div>
<form action="{{ route('payment.offline.store') }}" method="post" enctype="multipart/form-data">@csrf
<div class="mb-3">
<label for="" class="form-label d-flex justify-content-between">
<span>{{ get_phrase('Payment Document') }}</span>
<span>{{ get_phrase('(jpg, pdf, txt, png, docx)') }}</span>
</label>
<input type="hidden" name="item_type" value="{{ $payment_details['custom_field']['item_type'] ?? 'course' }}" required>
<input type="file" name="doc" class="form-control" required>
</div>
<input type="submit" class="btn btn-primary" value="{{ get_phrase('Pay offline') }}">
</form>

View File

@ -1,64 +0,0 @@
@php
$model = $payment_details['success_method']['model_name'];
if ($model == 'InstructorPayment') {
$payment_keys = DB::table('users')
->where('id', $payment_details['items'][0]['id'])
->value('paymentkeys');
$keys = isset($payment_keys) ? json_decode($payment_keys) : null;
$public_key = $secret_key = '';
if ($keys) {
$public_key = $keys->razorpay->public_key;
$secret_key = $keys->razorpay->secret_key;
}
if ($public_key == '' || $secret_key == '') {
$msg = get_phrase('This payment gateway is not configured.');
}
} else {
$payment_gateway = DB::table('payment_gateways')->where('identifier', 'razorpay')->first();
$public_key = $secret_key = '';
if ($payment_gateway->keys != '') {
if ($payment_gateway->status == 1) {
$keys = json_decode($payment_gateway->keys, true);
$public_key = $keys['public_key'];
$secret_key = $keys['secret_key'];
if ($public_key == '' || $secret_key == '') {
$msg = get_phrase('This payment gateway is not configured.');
}
} else {
$msg = get_phrase('Admin denied transaction through this gateway.');
}
} else {
$msg = get_phrase('This payment gateway is not configured.');
}
}
@endphp
@if ($public_key != '' && $secret_key != '')
<form action="{{ route('razorpay.order', $payment_gateway->identifier) }}" method="post">
@csrf
<input type="hidden" name="price" value="{{ $payment_details['payable_amount'] }}">
<button type="submit" class="btn btn-primary">{{ get_phrase('Pay by Razorpay') }}</button>
</form>
@else
<svg xmlns="http://www.w3.org/2000/svg" class="d-none;">
<symbol id="exclamation-triangle-fill" fill="currentColor" viewBox="0 0 16 16">
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z" />
</symbol>
</svg>
<div class="alert alert-danger d-flex align-items-center" role="alert">
<svg class="bi me-2 flex-shrink-0" width="24" height="24" role="img" aria-label="Danger:">
<use xlink:href="#exclamation-triangle-fill" />
</svg>
<div class="payment_err_msg">
<b>{{ get_phrase('Opps!') }}</b> {{ $msg }}<br>
{{ get_phrase('Try another gateway.') }}
</div>
</div>
@endif

View File

@ -1,68 +0,0 @@
@php
$page_data = $data['page_data'];
$payment_details = $data['payment_details'];
$color = $data['color'];
$payment_gateway = DB::table('payment_gateways')->where('identifier', 'razorpay')->first();
@endphp
<button id="rzp-button1" hidden>{{ get_phrase('Pay') }}</button>
<form action="{{ route('payment.success', ['identifier' => 'razorpay']) }}" hidden>
@csrf
<input type="text" name="razorpay_payment_id" id="razorpay_payment_id">
<input type="text" name="razorpay_order_id" id="razorpay_order_id">
<input type="text" name="razorpay_signature" id="razorpay_signature">
<input type="submit" id="payment_done">
</form>
<script src="{{ asset('assets/frontend/default/js/jquery-3.7.1.min.js') }}"></script>
<script src="https://checkout.razorpay.com/v1/checkout.js"></script>
<script>
"use strict";
var color = "{{ $color }}";
var options = {
"key": "{{ $page_data['razorpay_id'] }}",
"amount": "{{ $page_data['amount'] }}",
"currency": "{{ $payment_gateway->currency }}",
"name": "{{ $page_data['name'] }}",
"description": "{{ $page_data['description'] }}",
"image": "{{ get_image(auth()->user()->photo) }}",
"order_id": "{{ $page_data['order_id'] }}",
"handler": function(response) {
var razorpay_payment_id = response.razorpay_payment_id;
var razorpay_order_id = response.razorpay_order_id;
var razorpay_signature = response.razorpay_signature;
window.location.href = "{{$payment_details['success_url'] . '/' . $payment_gateway->identifier}}?razorpay_payment_id=" + response.razorpay_payment_id;
},
"prefill": {
"name": "{{ $page_data['name'] }}",
"email": "{{ $page_data['email'] }}",
"contact": "{{ $page_data['phone'] }}"
},
"notes": {
"address": "{{ $page_data['name'] }}"
},
"theme": {
"color": color
}
};
var rzp1 = new Razorpay(options);
rzp1.on('payment.failed', function(response) {
alert(response.error.description);
alert(response.error.reason);
});
document.getElementById('rzp-button1').onclick = function(e) {
rzp1.open();
e.preventDefault();
}
$(document).ready(function() {
$('#rzp-button1').trigger('click');
});
</script>

View File

@ -1,71 +0,0 @@
@php
$model = $payment_details['success_method']['model_name'];
if ($model == 'InstructorPayment') {
$payment_keys = DB::table('users')
->where('id', $payment_details['items'][0]['id'])
->value('paymentkeys');
$keys = isset($payment_keys) ? json_decode($payment_keys) : null;
if ($payment_gateway->test_mode == 1) {
$public_key = $public_live_key = '';
if ($keys) {
$key = $keys->stripe->public_key;
$key = $keys->stripe->public_live_key;
}
} else {
$secret_key = $secret_live_key = '';
if ($keys) {
$key = $keys->stripe->secret_live_key;
$key = $keys->stripe->secret_key;
}
}
if ($key == '') {
$msg = get_phrase('This payment gateway is not configured.');
}
} else {
$payment_keys = json_decode($payment_gateway->keys, true);
$key = '';
if ($payment_keys != '') {
if ($payment_gateway->status == 1) {
if ($payment_gateway->test_mode == 1) {
$key = $payment_keys['secret_key'];
} else {
$key = $payment_keys['secret_live_key'];
}
if ($key == '') {
$msg = get_phrase('This payment gateway is not configured.');
}
} else {
$msg = get_phrase('Admin denied transaction through this gateway.');
}
} else {
$msg = get_phrase('This payment gateway is not configured.');
}
}
@endphp
@if ($key != '')
<a href="{{ route('payment.create', $payment_gateway->identifier) }}" class="btn btn-primary py-2 px-3">{{ get_phrase('Pay by Stripe') }}</a>
@else
<svg xmlns="http://www.w3.org/2000/svg" class="d-none;">
<symbol id="exclamation-triangle-fill" fill="currentColor" viewBox="0 0 16 16">
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z" />
</symbol>
</svg>
<div class="alert alert-danger d-flex align-items-center" role="alert">
<svg class="bi flex-shrink-0 me-2" width="24" height="24" role="img" aria-label="Danger:">
<use xlink:href="#exclamation-triangle-fill" />
</svg>
<div class="payment_err_msg">
<b>{{ get_phrase('Opps!') }}</b> {{ $msg }}<br>
{{ get_phrase('Try another gateway.') }}
</div>
</div>
@endif

View File

@ -0,0 +1,403 @@
<!DOCTYPE html>
<html lang="en">
<head>
@php
$system_name = \App\Models\Setting::where('type', 'system_name')->value('description');
$system_favicon = \App\Models\Setting::where('type', 'system_fav_icon')->value('description');
@endphp
<title>Virtual Account - {{ $system_name }}</title>
<!-- all the meta tags -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta content="" name="description" />
<meta content="" name="author" />
<!-- CSRF Token for ajax for submission -->
<meta name="csrf_token" content="{{ csrf_token() }}" />
<!-- all the css files -->
<!-- fav icon -->
<link rel="shortcut icon" href="{{ asset(get_frontend_settings('favicon')) }}" />
<!-- Bootstrap CSS -->
<link rel="stylesheet" type="text/css" href="{{ asset('assets/payment/style/vendors/bootstrap-5.1.3/css/bootstrap.min.css') }}" />
<link rel="stylesheet" type="text/css" href="{{ asset('assets/payment/style/css/swiper-bundle.min.css') }}" />
<link rel="stylesheet" type="text/css" href="{{ asset('assets/payment/style/css/custom.css') }}" />
<link rel="stylesheet" type="text/css" href="{{ asset('assets/payment/style/css/style.css') }}" />
<link rel="stylesheet" type="text/css" href="{{ asset('assets/payment/style/vendors/bootstrap-icons-1.8.1/bootstrap-icons.css') }}" />
<link rel="stylesheet" type="text/css" href="{{ asset('assets/global/icons/uicons-regular-rounded/css/uicons-regular-rounded.css') }}" />
<link rel="stylesheet" type="text/css" href="{{ asset('assets/payment/style/css/own.css') }}" />
<!--Main Jquery-->
<script src="{{ asset('assets/payment/style/vendors/jquery/jquery-3.7.1.min.js') }}"></script>
<style>
.main_content {
min-height: calc(100% - 50px);
margin-top: 0px !important;
}
.va-card {
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
border: none;
}
.va-card .card-body {
padding: 2rem;
}
.va-number {
font-size: 1.5rem;
letter-spacing: 2px;
background: #f8f9fa;
padding: 10px 15px;
border-radius: 5px;
border: 1px dashed #dee2e6;
}
.payment-instruction {
background: #fff8e1;
border-left: 4px solid #ffc107;
border-radius: 5px;
}
.badge {
font-size: 0.9rem;
padding: 0.5rem 1rem;
}
</style>
</head>
<body class="pt-lg-5 pb-4">
@if (session('app_url'))
@include('payment.go_back_to_mobile_app')
@endif
<div class="main_content paymentContent">
<div class="paymentHeader d-flex justify-content-between align-items-center px-4 px-sm-5 mb-4">
<h5 class="title text-capitalize">{{ get_phrase('Payment Details') }}</h5>
<a href="{{ route('purchase.history') }}" class="btn btn-light text-sm">
<i class="fi-rr-arrow-left"></i>
<span class="d-none d-sm-inline-block">{{ get_phrase('Back to History') }}</span>
</a>
</div>
<div class="px-4 px-sm-5 pt-2 pb-5">
<div class="card va-card">
<div class="card-body">
<h4 class="mb-4 pb-2 border-bottom">{{ get_phrase('Virtual Account Payment') }}</h4>
<table class="table table-bordered mb-4">
<tbody>
<tr>
<th class="bg-light" style="width: 30%">{{ get_phrase('Bank') }}</th>
<td>
<div class="d-flex align-items-center">
<span class="me-2">BTN</span>
<span class="badge bg-primary">{{ get_phrase('Virtual Account') }}</span>
</div>
</td>
</tr>
<tr>
<th class="bg-light">{{ get_phrase('VA Number') }}</th>
<td>
<div class="va-number d-inline-block">
<strong>{{ chunk_split($vaPayment->va_number, 4, ' ') }}</strong>
</div>
<button class="btn btn-sm btn-outline-secondary ms-3" onclick="copyToClipboard('{{ $vaPayment->va_number }}')">
<i class="bi bi-clipboard"></i> {{ get_phrase('Copy') }}
</button>
</td>
</tr>
<tr>
<th class="bg-light">{{ get_phrase('Total Amount') }}</th>
<td class="h5 text-primary">
Rp {{ number_format($vaPayment->total_amount, 0, ',', '.') }}
</td>
</tr>
<tr>
<th class="bg-light">{{ get_phrase('Expired At') }}</th>
<td class="{{ \Carbon\Carbon::now()->gt($vaPayment->expired_at) ? 'text-danger' : '' }}">
<i class="bi bi-clock me-1"></i>
{{ \Carbon\Carbon::parse($vaPayment->expired_at)->translatedFormat('l, d F Y H:i') }}
@if(\Carbon\Carbon::now()->gt($vaPayment->expired_at))
<span class="badge bg-danger ms-2">{{ get_phrase('Expired') }}</span>
@endif
</td>
</tr>
<tr>
<th class="bg-light">{{ get_phrase('Status') }}</th>
<td>
@if ($vaPayment->status == 0)
<span class="badge bg-warning">
<i class="bi bi-clock-history me-1"></i> {{ get_phrase('UNPAID') }}
</span>
@elseif ($vaPayment->status == 1)
<span class="badge bg-success">
<i class="bi bi-check-circle me-1"></i> {{ get_phrase('PAID') }}
</span>
@else
<span class="badge bg-danger">
<i class="bi bi-x-circle me-1"></i> {{ get_phrase('FAILED') }}
</span>
@endif
</td>
</tr>
</tbody>
</table>
<div class="alert payment-instruction mt-4">
<h6 class="alert-heading mb-2">
<i class="bi bi-info-circle me-2"></i> {{ get_phrase('Payment Instructions') }}
</h6>
<ol class="mb-0 ps-3">
<li>{{ get_phrase('Go to ATM or Mobile Banking BTN') }}</li>
<li>{{ get_phrase('Select menu "Transfer" or "Payment"') }}</li>
<li>{{ get_phrase('Select "Virtual Account" or "VA"') }}</li>
<li>{{ get_phrase('Enter the VA number above') }}</li>
<li>{{ get_phrase('Confirm the amount and complete the payment') }}</li>
<li>{{ get_phrase('Payment confirmation will be processed automatically') }}</li>
</ol>
</div>
<div class="d-flex justify-content-between align-items-center mt-4 pt-3 border-top">
<div>
<small class="text-muted">
<i class="bi bi-shield-check me-1"></i>
{{ get_phrase('Secure payment via BTN Virtual Account') }}
</small>
</div>
<div>
@if($vaPayment->status == 0 && \Carbon\Carbon::now()->lt($vaPayment->expired_at))
<button class="btn btn-primary" onclick="checkPaymentStatus()">
<i class="bi bi-arrow-clockwise me-1"></i> {{ get_phrase('Check Status') }}
</button>
@endif
</div>
</div>
</div>
</div>
</div>
</div>
<!--Bootstrap bundle with popper-->
<script src="{{ asset('assets/payment/style/vendors/bootstrap-5.1.3/js/bootstrap.bundle.min.js') }}"></script>
<script src="{{ asset('assets/payment/style/js/swiper-bundle.min.js') }}"></script>
<!-- Datepicker js -->
<script src="{{ asset('assets/payment/style/js/moment.min.js') }}"></script>
<script src="{{ asset('assets/payment/style/js/sweetalert2@11.js') }}"></script>
<!-- toster file -->
@include('frontend.default.toaster')
<script>
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(function() {
Swal.fire({
icon: 'success',
title: 'Copied!',
text: 'VA number copied to clipboard',
timer: 2000,
showConfirmButton: false
});
});
}
function checkPaymentStatus() {
Swal.fire({
title: 'Checking Payment Status',
text: 'Please wait...',
allowOutsideClick: false,
didOpen: () => {
Swal.showLoading();
// Simulate API call - replace with actual API endpoint
setTimeout(() => {
window.location.reload();
}, 1500);
}
});
}
// Auto refresh page every 60 seconds if payment is still unpaid
@if($vaPayment->status == 0 && \Carbon\Carbon::now()->lt($vaPayment->expired_at))
setTimeout(function() {
window.location.reload();
}, 60000); // 60 seconds
@endif
</script>
</body>
</html>
<<script>
document.addEventListener('DOMContentLoaded', function () {
const paymentId = {{ $vaPayment->id }};
const CHECK_DELAY = 15000; // 15 detik (sesuai permintaan)
const MAX_CHECKS = 240; // 1 jam (15s x 240)
let checkCount = 0;
let intervalId = null;
let isChecking = false;
function startAutoCheck() {
if (intervalId) return;
intervalId = setInterval(() => {
checkPaymentStatus(false);
}, CHECK_DELAY);
}
function stopAutoCheck() {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
}
async function checkPaymentStatus(manual = true) {
if (isChecking) return;
if (checkCount >= MAX_CHECKS) {
stopAutoCheck();
console.log('Stop checking: time limit reached (1 hour)');
showExpiredMessage();
return;
}
isChecking = true;
checkCount++;
console.log(`Check #${checkCount} initiated`);
if (manual) {
Swal.fire({
title: 'Checking payment status...',
allowOutsideClick: false,
didOpen: () => Swal.showLoading()
});
}
try {
const response = await fetch(`/payment/va/${paymentId}/status`, {
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
}
});
// Cek jika response OK
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('API Response:', data);
if (data.status === 1) {
// Payment success
stopAutoCheck();
if (manual) Swal.close();
Swal.fire({
icon: 'success',
title: 'Payment Success',
text: 'Pembayaran berhasil diterima',
timer: 3000,
showConfirmButton: false
}).then(() => {
window.location.reload();
});
return;
}
if (data.status === 2) {
// Expired
stopAutoCheck();
if (manual) Swal.close();
Swal.fire({
icon: 'error',
title: 'Payment Expired',
text: 'Virtual Account sudah tidak valid',
confirmButtonText: 'OK'
}).then(() => {
window.location.reload();
});
return;
}
// status === 0 → lanjut auto-check
console.log('Payment still pending');
} catch (error) {
console.error('Check status error:', error);
if (manual) {
Swal.fire({
icon: 'error',
title: 'Error',
text: 'Gagal memeriksa status pembayaran',
confirmButtonText: 'OK'
});
}
} finally {
if (manual && Swal.isVisible()) Swal.close();
isChecking = false;
}
}
function showExpiredMessage() {
// Update UI untuk expired
const statusCell = document.querySelector('tr:nth-child(5) td');
if (statusCell) {
statusCell.innerHTML = `
<span class="badge bg-secondary">
<i class="bi bi-clock me-1"></i> EXPIRED
</span>
`;
}
// Disable check button
const checkBtn = document.querySelector('button[onclick="checkPaymentStatus()"]');
if (checkBtn) {
checkBtn.disabled = true;
checkBtn.innerHTML = '<i class="bi bi-clock me-1"></i> Expired';
checkBtn.className = 'btn btn-secondary';
checkBtn.onclick = null;
}
// Show alert
Swal.fire({
icon: 'info',
title: 'Auto-check Stopped',
text: 'Pengecekan otomatis dihentikan setelah 1 jam',
confirmButtonText: 'OK'
});
}
// ▶️ START AUTO CHECK SAAT PAGE DIBUKA
@if($vaPayment->status == 0 )
startAutoCheck();
// Cek pertama setelah 2 detik
setTimeout(() => checkPaymentStatus(false), 2000);
@endif
// 🔘 EXPOSE KE BUTTON
window.checkPaymentStatus = function () {
checkPaymentStatus(true);
};
// Override fungsi lama jika ada
window.originalCheckPaymentStatus = window.checkPaymentStatus;
// ⛔ STOP CHECK JIKA TAB DITUTUP / PINDAH
document.addEventListener('visibilitychange', function () {
if (document.hidden) {
stopAutoCheck();
console.log('Auto-check paused (tab hidden)');
} else {
startAutoCheck();
console.log('Auto-check resumed');
}
});
});
</script>

View File

@ -0,0 +1,52 @@
@php $amount = $payment_details['payable_amount']; @endphp
@php
$model = $payment_details['success_method']['model_name'];
if ($model == 'InstructorPayment') {
$settings = DB::table('users')
->where('id', $payment_details['items'][0]['id'])
->value('paymentkeys');
$keys = isset($settings) ? json_decode($settings) : null;
if ($keys) {
$bank_information = $keys->offline->bank_information;
}
if ($bank_information == '') {
$msg = "This payment gateway isn't configured.";
}
} else {
$payment_keys = json_decode($payment_gateway->keys, true);
$bank_information = '';
if ($payment_keys != '') {
if ($payment_gateway->status == 1) {
$bank_information = $payment_keys['bank_information'];
if ($bank_information == '') {
$msg = get_phrase("This payment gateway isn't configured.");
}
} else {
$msg = get_phrase('Admin denied transaction through this gateway.');
}
} else {
$msg = get_phrase("This payment gateway isn't configured.");
}
}
@endphp
<div class="row my-1">
<div class="col-md-12 text-start">
{!! removeScripts($bank_information) !!}
</div>
</div>
<form action="{{ route('payment.va.store') }}" method="post">
@csrf
<input type="hidden" name="item_type" value="course">
<input type="submit" class="btn btn-primary"
value="{{ get_phrase('Pay Virtual Account') }}">
</form>

View File

@ -0,0 +1,502 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{{ get_phrase('Payment Successful') }} | {{ config('app.name') }}</title>
<style>
/* Reset & Base */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: #f9fafb;
margin: 0;
padding: 20px;
line-height: 1.5;
color: #374151;
}
.email-container {
max-width: 600px;
margin: 0 auto;
background: #ffffff;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
border: 1px solid #e5e7eb;
}
/* Header */
.email-header {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
padding: 40px 30px;
text-align: center;
position: relative;
}
.header-icon {
font-size: 48px;
margin-bottom: 16px;
display: block;
}
.header-title {
font-size: 28px;
font-weight: 700;
margin: 0 0 8px 0;
letter-spacing: -0.025em;
}
.header-subtitle {
font-size: 16px;
opacity: 0.9;
font-weight: 400;
margin: 0;
}
/* Content */
.email-body {
padding: 40px 30px;
}
.greeting {
font-size: 18px;
color: #111827;
margin-bottom: 24px;
font-weight: 500;
}
.greeting strong {
color: #059669;
}
.intro-text {
font-size: 16px;
color: #4b5563;
margin-bottom: 32px;
line-height: 1.6;
}
/* Info Box */
.info-box {
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border-radius: 12px;
padding: 28px;
margin: 32px 0;
border: 1px solid #e2e8f0;
}
.info-box-title {
font-size: 18px;
font-weight: 700;
color: #1e293b;
margin-bottom: 24px;
display: flex;
align-items: center;
gap: 10px;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 0;
border-bottom: 1px solid #e2e8f0;
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
font-weight: 500;
color: #64748b;
font-size: 15px;
min-width: 120px;
}
.info-value {
font-weight: 600;
color: #1e293b;
font-size: 16px;
text-align: right;
}
.va-number {
background: white;
padding: 12px 16px;
border-radius: 8px;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
font-size: 18px;
letter-spacing: 1.5px;
color: #1e293b;
border: 2px solid #cbd5e1;
display: inline-block;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.amount {
color: #059669;
font-size: 22px;
font-weight: 700;
}
/* Courses Section */
.courses-section {
margin: 32px 0;
}
.section-title {
font-size: 18px;
font-weight: 700;
color: #1e293b;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 2px solid #e2e8f0;
display: flex;
align-items: center;
gap: 10px;
}
.course-item {
background: white;
border-radius: 10px;
padding: 18px;
margin-bottom: 12px;
border: 1px solid #e2e8f0;
transition: all 0.2s ease;
}
.course-item:hover {
border-color: #059669;
box-shadow: 0 4px 12px rgba(5, 150, 105, 0.1);
}
.course-title {
font-size: 16px;
font-weight: 600;
color: #374151;
margin: 0;
}
/* Next Steps */
.next-steps {
background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
border-radius: 12px;
padding: 28px;
margin: 32px 0;
border: 1px solid #93c5fd;
}
.next-steps-title {
font-size: 18px;
font-weight: 700;
color: #1e40af;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.steps-list {
list-style: none;
padding: 0;
margin: 0;
}
.steps-list li {
padding: 10px 0 10px 32px;
position: relative;
font-size: 15px;
color: #374151;
line-height: 1.5;
}
.steps-list li:before {
content: '→';
position: absolute;
left: 0;
color: #3b82f6;
font-weight: bold;
font-size: 14px;
width: 24px;
height: 24px;
background: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* CTA Buttons */
.cta-buttons {
text-align: center;
margin: 40px 0 32px;
}
.cta-button {
display: inline-block;
background: linear-gradient(135deg, #059669 0%, #047857 100%);
color: white !important;
text-decoration: none;
padding: 16px 32px;
border-radius: 12px;
font-weight: 600;
font-size: 16px;
margin: 0 8px;
transition: all 0.3s ease;
box-shadow: 0 4px 16px rgba(5, 150, 105, 0.25);
border: none;
cursor: pointer;
}
.cta-button:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(5, 150, 105, 0.35);
}
.cta-button-secondary {
display: inline-block;
background: white !important;
color: #059669 !important;
text-decoration: none;
padding: 14px 28px;
border-radius: 12px;
font-weight: 600;
font-size: 15px;
margin: 0 8px;
border: 2px solid #059669;
transition: all 0.3s ease;
}
.cta-button-secondary:hover {
background: #f0fdf4;
}
/* Final Message */
.final-message {
text-align: center;
margin: 32px 0;
padding: 24px;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border-radius: 12px;
border: 1px solid #e2e8f0;
}
.final-text {
font-size: 15px;
color: #4b5563;
margin-bottom: 12px;
}
.final-subtext {
font-size: 18px;
color: #1e293b;
font-weight: 600;
margin: 0;
}
/* Footer */
.email-footer {
background: #1e293b;
color: #cbd5e1;
padding: 32px;
text-align: center;
font-size: 14px;
}
.footer-brand {
font-size: 20px;
font-weight: 700;
color: white;
margin-bottom: 16px;
letter-spacing: 0.5px;
}
.footer-disclaimer {
font-size: 13px;
color: #94a3b8;
margin-top: 20px;
line-height: 1.5;
}
/* Responsive */
@media (max-width: 640px) {
body {
padding: 16px;
}
.email-body {
padding: 30px 24px;
}
.email-header {
padding: 32px 24px;
}
.header-title {
font-size: 24px;
}
.info-row {
flex-direction: column;
align-items: flex-start;
gap: 6px;
padding: 16px 0;
}
.info-label {
min-width: auto;
}
.info-value {
text-align: left;
width: 100%;
}
.va-number {
font-size: 16px;
padding: 10px 14px;
letter-spacing: 1px;
display: block;
text-align: center;
}
.cta-button,
.cta-button-secondary {
display: block;
width: 100%;
margin: 8px 0;
text-align: center;
}
.cta-buttons {
margin: 32px 0 24px;
}
}
@media (max-width: 480px) {
.email-body {
padding: 24px 20px;
}
.email-header {
padding: 28px 20px;
}
.info-box,
.next-steps {
padding: 20px;
}
}
</style>
</head>
<body>
<div class="email-container">
<div class="email-header">
<div class="header-icon">🎉</div>
<h1 class="header-title">{{ get_phrase('Payment Successful') }}</h1>
<p class="header-subtitle">{{ get_phrase('Your courses are now available!') }}</p>
</div>
<div class="email-body">
<div class="greeting">
{{ get_phrase('Congratulations') }}, <strong>{{ $user->name }}</strong>! 🎊
</div>
<p class="intro-text">
{{ get_phrase('We have successfully processed your payment. Your courses are now available for immediate access. Thank you for your purchase!') }}
</p>
<!-- Payment Details Box -->
<div class="info-box">
<div class="info-box-title">
<span>💳</span>
{{ get_phrase('Payment Details') }}
</div>
<div class="info-row">
<span class="info-label">{{ get_phrase('Virtual Account') }}</span>
<div class="info-value">
<div class="va-number">{{ chunk_split($payment->va_number, 4, ' ') }}</div>
</div>
</div>
<div class="info-row">
<span class="info-label">{{ get_phrase('Amount Paid') }}</span>
<span class="info-value amount">Rp {{ number_format($payment->total_amount, 0, ',', '.') }}</span>
</div>
<div class="info-row">
<span class="info-label">{{ get_phrase('Payment Date') }}</span>
<span class="info-value">
📅 {{ optional($payment->paid_at)->format('l, F d, Y \a\t H:i') }}
</span>
</div>
<div class="info-row">
<span class="info-label">{{ get_phrase('Transaction ID') }}</span>
<span class="info-value">
#{{ $payment->id }}
</span>
</div>
</div>
<!-- Courses Section -->
@if($courses->count() > 0)
<div class="courses-section">
<div class="section-title">📚 {{ get_phrase('Your Courses Are Now Available') }}</div>
@foreach($courses as $course)
<div class="course-item">
<div class="course-title">{{ $course->title }}</div>
</div>
@endforeach
</div>
@endif
<!-- Next Steps -->
<div class="next-steps">
<div class="next-steps-title">🚀 {{ get_phrase('What You Can Do Now') }}</div>
<ul class="steps-list">
<li>{{ get_phrase('Access all course materials immediately') }}</li>
<li>{{ get_phrase('Join course discussions and forums') }}</li>
<li>{{ get_phrase('Complete assignments and quizzes') }}</li>
<li>{{ get_phrase('Track your learning progress') }}</li>
</ul>
</div>
<!-- CTA Buttons -->
<div class="cta-buttons">
<a href="{{ url('/my-courses') }}" class="cta-button">
{{ get_phrase('Start Learning Now') }}
</a>
<a href="{{ url('/') }}" class="cta-button-secondary">
{{ get_phrase('Browse More Courses') }}
</a>
</div>
<!-- Final Message -->
<div class="final-message">
<p class="final-text">
{{ get_phrase('If you need any assistance with your courses, our support team is always ready to help.') }}
</p>
<p class="final-subtext">
{{ get_phrase('Happy Learning!') }} 📚✨
</p>
</div>
</div>
<!-- Footer -->
<div class="email-footer">
<div class="footer-brand">{{ config('app.name') }}</div>
<div class="footer-disclaimer">
{{ get_phrase('This is an automated email. Please do not reply to this message.') }}<br>
{{ get_phrase('If you have any questions about your purchase, please contact our support team.') }}
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,538 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{{ get_phrase('Course Purchase Confirmation') }} | {{ config('app.name') }}</title>
<style>
/* Reset & Base */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: #f9fafb;
margin: 0;
padding: 20px;
line-height: 1.5;
color: #374151;
}
.email-container {
max-width: 600px;
margin: 0 auto;
background: #ffffff;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
border: 1px solid #e5e7eb;
}
/* Header */
.email-header {
background: linear-gradient(135deg, #212e91 0%, #4f46e5 100%);
color: white;
padding: 40px 30px;
text-align: center;
position: relative;
}
.header-icon {
font-size: 48px;
margin-bottom: 16px;
display: block;
}
.header-title {
font-size: 28px;
font-weight: 700;
margin: 0 0 8px 0;
letter-spacing: -0.025em;
}
.header-subtitle {
font-size: 16px;
opacity: 0.9;
font-weight: 400;
margin: 0;
}
/* Content */
.email-body {
padding: 40px 30px;
}
.greeting {
font-size: 18px;
color: #111827;
margin-bottom: 24px;
font-weight: 500;
}
.greeting strong {
color: #4f46e5;
}
.intro-text {
font-size: 16px;
color: #4b5563;
margin-bottom: 32px;
line-height: 1.6;
}
/* Info Box */
.info-box {
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border-radius: 12px;
padding: 28px;
margin: 32px 0;
border: 1px solid #e2e8f0;
}
.info-box-title {
font-size: 18px;
font-weight: 700;
color: #1e293b;
margin-bottom: 24px;
display: flex;
align-items: center;
gap: 10px;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 0;
border-bottom: 1px solid #e2e8f0;
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
font-weight: 500;
color: #64748b;
font-size: 15px;
min-width: 120px;
}
.info-value {
font-weight: 600;
color: #1e293b;
font-size: 16px;
text-align: right;
}
.va-number {
background: white;
padding: 12px 16px;
border-radius: 8px;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
font-size: 18px;
letter-spacing: 1.5px;
color: #1e293b;
border: 2px solid #cbd5e1;
display: inline-block;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.amount {
color: #059669;
font-size: 22px;
font-weight: 700;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border-radius: 20px;
font-size: 14px;
font-weight: 600;
}
.status-pending {
background: #fef3c7;
color: #92400e;
}
.status-success {
background: #d1fae5;
color: #065f46;
}
.status-expired {
background: #fee2e2;
color: #991b1b;
}
/* Courses Section */
.courses-section {
margin: 32px 0;
}
.section-title {
font-size: 18px;
font-weight: 700;
color: #1e293b;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 2px solid #e2e8f0;
display: flex;
align-items: center;
gap: 10px;
}
.course-item {
background: white;
border-radius: 10px;
padding: 18px;
margin-bottom: 12px;
border: 1px solid #e2e8f0;
transition: all 0.2s ease;
}
.course-item:hover {
border-color: #4f46e5;
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.1);
}
.course-title {
font-size: 16px;
font-weight: 600;
color: #374151;
margin: 0;
}
/* Instructions */
.instructions {
background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
border-radius: 12px;
padding: 28px;
margin: 32px 0;
border: 1px solid #bbf7d0;
}
.instructions-title {
font-size: 18px;
font-weight: 700;
color: #166534;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.instructions-list {
list-style: none;
padding: 0;
margin: 0;
}
.instructions-list li {
padding: 10px 0 10px 32px;
position: relative;
font-size: 15px;
color: #374151;
line-height: 1.5;
}
.instructions-list li:before {
content: '✓';
position: absolute;
left: 0;
color: #059669;
font-weight: bold;
font-size: 14px;
width: 24px;
height: 24px;
background: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* CTA Buttons */
.cta-buttons {
text-align: center;
margin: 40px 0 32px;
}
.ii a[href] {
color: white !important;
}
.cta-button {
display: inline-block;
background: linear-gradient(135deg, #212e91 0%, #4f46e5 100%);
color: white !important;
text-decoration: none;
padding: 16px 32px;
border-radius: 12px;
font-weight: 600;
font-size: 16px;
margin: 0 8px;
transition: all 0.3s ease;
box-shadow: 0 4px 16px rgba(79, 70, 229, 0.25);
border: none;
cursor: pointer;
}
.cta-button:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(79, 70, 229, 0.35);
}
.cta-button-secondary {
display: inline-block;
background: white;
color: #4f46e5;
text-decoration: none;
padding: 14px 28px;
border-radius: 12px;
font-weight: 600;
font-size: 15px;
margin: 0 8px;
border: 2px solid #4f46e5;
transition: all 0.3s ease;
}
.cta-button-secondary:hover {
background: #f8fafc;
}
/* Final Message */
.final-message {
text-align: center;
margin: 32px 0;
padding: 24px;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border-radius: 12px;
border: 1px solid #e2e8f0;
}
.final-text {
font-size: 15px;
color: #4b5563;
margin-bottom: 12px;
}
.final-subtext {
font-size: 18px;
color: #1e293b;
font-weight: 600;
margin: 0;
}
/* Footer */
.email-footer {
background: #1e293b;
color: #cbd5e1;
padding: 32px;
text-align: center;
font-size: 14px;
}
.footer-brand {
font-size: 20px;
font-weight: 700;
color: white;
margin-bottom: 16px;
letter-spacing: 0.5px;
}
.footer-disclaimer {
font-size: 13px;
color: #94a3b8;
margin-top: 20px;
line-height: 1.5;
}
/* Responsive */
@media (max-width: 640px) {
body {
padding: 16px;
}
.email-body {
padding: 30px 24px;
}
.email-header {
padding: 32px 24px;
}
.header-title {
font-size: 24px;
}
.info-row {
flex-direction: column;
align-items: flex-start;
gap: 6px;
padding: 16px 0;
}
.info-label {
min-width: auto;
}
.info-value {
text-align: left;
width: 100%;
}
.va-number {
font-size: 16px;
padding: 10px 14px;
letter-spacing: 1px;
display: block;
text-align: center;
}
.cta-button,
.cta-button-secondary {
display: block;
width: 100%;
margin: 8px 0;
text-align: center;
}
.cta-buttons {
margin: 32px 0 24px;
}
}
@media (max-width: 480px) {
.email-body {
padding: 24px 20px;
}
.email-header {
padding: 28px 20px;
}
.info-box,
.instructions {
padding: 20px;
}
}
</style>
</head>
<body>
<div class="email-container">
<div class="email-header">
<div class="header-icon">🎓</div>
<h1 class="header-title">{{ get_phrase('Course Purchase Confirmation') }}</h1>
<p class="header-subtitle">{{ get_phrase('Your learning journey begins here!') }}</p>
</div>
<div class="email-body">
<div class="greeting">
{{ get_phrase('Hello') }}, <strong>{{ $user->name }}</strong>! 👋
</div>
<p class="intro-text">
{{ get_phrase('Thank you for choosing to learn with us! Your course purchase has been successfully created. Please complete your payment using the Virtual Account details below before it expires to secure your access.') }}
</p>
<!-- Payment Details Box -->
<div class="info-box">
<div class="info-box-title">
<span>💳</span>
{{ get_phrase('Payment Details') }}
</div>
<div class="info-row">
<span class="info-label">{{ get_phrase('Virtual Account') }}</span>
<div class="info-value">
<div class="va-number">{{ $payment->va_number }}</div>
</div>
</div>
<div class="info-row">
<span class="info-label">{{ get_phrase('Total Amount') }}</span>
<span class="info-value amount">Rp {{ number_format($payment->total_amount, 0, ',', '.') }}</span>
</div>
<div class="info-row">
<span class="info-label">{{ get_phrase('Payment Status') }}</span>
<span class="info-value">
@if($payment->status == 0)
<span class="status-badge status-pending"> {{ get_phrase('Waiting for Payment') }}</span>
@elseif($payment->status == 1)
<span class="status-badge status-success"> {{ get_phrase('Payment Successful') }}</span>
@else
<span class="status-badge status-expired"> {{ get_phrase('Payment Expired') }}</span>
@endif
</span>
</div>
<div class="info-row">
<span class="info-label">{{ get_phrase('Valid Until') }}</span>
<span class="info-value">
🗓️ {{ \Carbon\Carbon::parse($payment->expired_at)->locale('id')->translatedFormat('l, d F Y \p\u\k\u\l H:i') }}
</span>
</div>
</div>
<!-- Courses Section -->
@if($courses->count() > 0)
<div class="courses-section">
<div class="section-title">📚 {{ get_phrase('Your Selected Courses') }}</div>
@foreach($courses as $course)
<div class="course-item">
<div class="course-title">{{ $course->title }}</div>
</div>
@endforeach
</div>
@endif
<!-- Instructions -->
<div class="instructions">
<div class="instructions-title">📋 {{ get_phrase('Payment Instructions') }}</div>
<ul class="instructions-list">
<li>{{ get_phrase('Go to ATM or Mobile Banking BTN') }}</li>
<li>{{ get_phrase('Select menu "Transfer" or "Payment"') }}</li>
<li>{{ get_phrase('Choose "Virtual Account" or "VA" option') }}</li>
<li>{{ get_phrase('Enter your Virtual Account number exactly as shown above') }}</li>
<li>{{ get_phrase('Confirm the amount matches exactly') }}</li>
<li>{{ get_phrase('Complete the payment before expiry time') }}</li>
</ul>
</div>
<!-- CTA Buttons -->
<div class="cta-buttons">
<a href="{{ url('/payment/va/' . $payment->id) }}" class="cta-button">
{{ get_phrase('View Payment Details') }}
</a>
<a href="{{ url('/') }}" class="cta-button-secondary">
{{ get_phrase('Back to Home') }}
</a>
</div>
<!-- Final Message -->
<div class="final-message">
<p class="final-text">
{{ get_phrase('Once your payment is completed, you will receive another email with access details.') }}
</p>
<p class="final-subtext">
{{ get_phrase('Happy Learning!') }} 📚✨
</p>
</div>
</div>
<!-- Footer -->
<div class="email-footer">
<div class="footer-brand">{{ config('app.name') }}</div>
<div class="footer-disclaimer">
{{ get_phrase('This is an automated email. Please do not reply to this message.') }}<br>
{{ get_phrase('If you have any questions about your purchase, please contact our support team.') }}
</div>
</div>
</div>
</body>
</html>

View File

@ -7,6 +7,7 @@ use App\Http\Controllers\Admin\BootcampModuleController;
use App\Http\Controllers\Admin\BootcampResourceController;
use App\Http\Controllers\Admin\CategoryController;
use App\Http\Controllers\Admin\MessageController;
use App\Http\Controllers\Admin\OfflinePaymentController;
use App\Http\Controllers\Admin\OpenAiController;
use App\Http\Controllers\Admin\PageBuilderController;
use App\Http\Controllers\Admin\QuestionController;
@ -326,6 +327,15 @@ Route::name('admin.')->prefix('admin')->middleware('admin')->group(function () {
Route::get('api/configurations', [SettingController::class, 'api_configurations'])->name('api.configurations');
Route::post('api/configuration/update/{type}', [SettingController::class, 'api_configuration_update'])->name('api.configuration.update');
// offline payment
Route::controller(OfflinePaymentController::class)->group(function () {
Route::get('offline-payments', 'index')->name('offline.payments');
Route::get('offline-payment/doc/{id}', 'download_doc')->name('offline.payment.doc');
Route::get('offline-payment/accept/{id}', 'accept_payment')->name('offline.payment.accept');
Route::get('offline-payment/decline/{id}', 'decline_payment')->name('offline.payment.decline');
Route::get('offline-payment/delete/{id}', 'delete_payment')->name('offline.payment.delete');
});
// coupon
Route::controller(CouponController::class)->group(function () {
Route::get('coupons', 'index')->name('coupons');

View File

@ -1,5 +1,6 @@
<?php
use App\Http\Controllers\OfflinePaymentController;
use App\Http\Controllers\PaymentController;
use Illuminate\Support\Facades\Route;
@ -8,9 +9,6 @@ Route::controller(PaymentController::class)->middleware('auth')->group(function
Route::get('payment/show_payment_gateway_by_ajax/{identifier}', 'show_payment_gateway_by_ajax')->name('payment.show_payment_gateway_by_ajax');
Route::any('payment/success/{identifier?}', 'payment_success')->name('payment.success');
Route::get('payment/create/{identifier}', 'payment_create')->name('payment.create');
// razor pay
Route::post('payment/{identifier}/order', 'payment_razorpay')->name('razorpay.order');
});
Route::any('payment-notification/{identifier?}', [PaymentController::class, 'payment_notification'])->name('payment.notification');

View File

@ -6,12 +6,15 @@ use App\Http\Controllers\student\BlogCommentController;
use App\Http\Controllers\student\BlogController;
use App\Http\Controllers\student\BootcampPurchaseController;
use App\Http\Controllers\student\CartController;
use App\Http\Controllers\student\InvoiceController;
use App\Http\Controllers\student\LiveClassController;
use App\Http\Controllers\student\MessageController;
use App\Http\Controllers\student\MyBootcampsController;
use App\Http\Controllers\student\MyCoursesController;
use App\Http\Controllers\student\MyProfileController;
use App\Http\Controllers\student\MyTeamPackageController;
use App\Http\Controllers\student\OfflinePaymentController;
use App\Http\Controllers\student\VAPaymentController;
use App\Http\Controllers\student\PurchaseController;
use App\Http\Controllers\student\QuizController;
use App\Http\Controllers\student\ReviewController;
@ -144,5 +147,16 @@ Route::middleware(['auth'])->group(function () {
});
Route::middleware('auth')->group(function () {
//Certificate download
Route::get('certificate/{identifier}', [HomeController::class, 'download_certificate'])->name('certificate');
// offline payment
Route::post('payment/offline/store', [OfflinePaymentController::class, 'store'])->name('payment.offline.store');
// offline payment
Route::post('payment/va/store', [VAPaymentController::class, 'store'])->name('payment.va.store');
Route::get('payment/va/{id}', [VAPaymentController::class, 'show'])->name('payment.va.show');
Route::get('/payment/va/{id}/status', [VAPaymentController::class, 'checkPaymentApi'])->name('payment.va.status');
});