Add all files
This commit is contained in:
72
app/Livewire/Auth/Login.php
Normal file
72
app/Livewire/Auth/Login.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Auth;
|
||||
|
||||
use App\Livewire\Component;
|
||||
use App\Models\User;
|
||||
use App\Traits\Captchable;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Livewire\Attributes\Validate;
|
||||
|
||||
class Login extends Component
|
||||
{
|
||||
use Captchable;
|
||||
|
||||
#[Validate('required|email')]
|
||||
public string $email = '';
|
||||
|
||||
#[Validate('required')]
|
||||
public string $password = '';
|
||||
|
||||
public $remember = false;
|
||||
|
||||
public function submit()
|
||||
{
|
||||
$this->captcha();
|
||||
$this->validate();
|
||||
|
||||
if (RateLimiter::tooManyAttempts('login:' . $this->email, 5)) {
|
||||
$this->addError('email', 'Too many login attempts. Please try again in 60 seconds.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
RateLimiter::increment('login:' . $this->email);
|
||||
|
||||
if (!Auth::attempt($this->only('email', 'password'), $this->remember)) {
|
||||
$this->addError('email', 'These credentials do not match our records.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Check 2FA
|
||||
if (Auth::user()->tfa_secret) {
|
||||
Session::put('2fa', [
|
||||
'user_id' => Auth::id(),
|
||||
'remember' => $this->remember,
|
||||
'expires' => now()->addMinutes(5),
|
||||
]);
|
||||
|
||||
Auth::logout();
|
||||
|
||||
return $this->redirect(route('2fa'), true);
|
||||
}
|
||||
|
||||
RateLimiter::clear('login:' . $this->email);
|
||||
|
||||
event(new \App\Events\Auth\Login(User::find(Auth::id())));
|
||||
|
||||
$intendedUrl = session()->pull('url.intended', default: route('dashboard'));
|
||||
$isAdminRoute = str_starts_with($intendedUrl, url('/admin'));
|
||||
|
||||
// Redirect normally if it is an admin route, otherwise navigate using livewire
|
||||
return $this->redirect($intendedUrl, navigate: !$isAdminRoute);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('auth.login');
|
||||
}
|
||||
}
|
||||
21
app/Livewire/Auth/Logout.php
Normal file
21
app/Livewire/Auth/Logout.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Auth;
|
||||
|
||||
use App\Livewire\Component;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class Logout extends Component
|
||||
{
|
||||
public function logout()
|
||||
{
|
||||
Auth::logout();
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('auth.logout');
|
||||
}
|
||||
}
|
||||
53
app/Livewire/Auth/Password/Request.php
Normal file
53
app/Livewire/Auth/Password/Request.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Auth\Password;
|
||||
|
||||
use App\Helpers\NotificationHelper;
|
||||
use App\Livewire\Component;
|
||||
use App\Models\User;
|
||||
use App\Traits\Captchable;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
|
||||
class Request extends Component
|
||||
{
|
||||
use Captchable;
|
||||
|
||||
public string $email = '';
|
||||
|
||||
public function submit()
|
||||
{
|
||||
$this->captcha();
|
||||
|
||||
$this->validate([
|
||||
'email' => 'required|email',
|
||||
]);
|
||||
|
||||
$rateLimitKey = 'password-reset:' . request()->ip();
|
||||
|
||||
if (RateLimiter::tooManyAttempts($rateLimitKey, 3)) {
|
||||
$this->addError('email', 'Too many password reset attempts. Please try again later.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
RateLimiter::hit($rateLimitKey, 60);
|
||||
|
||||
// Find the user
|
||||
$user = User::where('email', $this->email)->first();
|
||||
|
||||
if ($user && !$user?->role) {
|
||||
NotificationHelper::passwordResetNotification($user, ['url' => url(route('password.reset', [
|
||||
'token' => Password::createToken($user),
|
||||
'email' => $user->email,
|
||||
], false))]);
|
||||
}
|
||||
|
||||
$this->notify('If the email address is associated with an account, you will receive an email with instructions on how to reset your password.', 'success');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('auth.password.request');
|
||||
}
|
||||
}
|
||||
69
app/Livewire/Auth/Password/Reset.php
Normal file
69
app/Livewire/Auth/Password/Reset.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Auth\Password;
|
||||
|
||||
use App\Livewire\Component;
|
||||
use App\Models\User;
|
||||
use App\Traits\Captchable;
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Support\Facades\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\Url;
|
||||
use Livewire\Attributes\Validate;
|
||||
|
||||
class Reset extends Component
|
||||
{
|
||||
use Captchable;
|
||||
|
||||
public string $token;
|
||||
|
||||
#[Url, Locked]
|
||||
public string $email = '';
|
||||
|
||||
#[Validate('required|string|min:8|confirmed')]
|
||||
public string $password = '';
|
||||
|
||||
#[Validate('required')]
|
||||
public string $password_confirmation = '';
|
||||
|
||||
public function mount()
|
||||
{
|
||||
if (!$this->token || !Request::has('email')) {
|
||||
return abort(404);
|
||||
}
|
||||
// Validate token
|
||||
if (!Password::tokenExists(User::where('email', $this->email)->firstOrFail(), $this->token)) {
|
||||
return abort(404);
|
||||
}
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
$this->captcha();
|
||||
|
||||
$status = Password::reset(
|
||||
['email' => $this->email, 'password' => $this->password, 'password_confirmation' => $this->password_confirmation, 'token' => $this->token],
|
||||
function ($user) {
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($this->password),
|
||||
])->setRememberToken(Str::random(60));
|
||||
|
||||
$user->save();
|
||||
|
||||
event(new PasswordReset($user));
|
||||
}
|
||||
);
|
||||
|
||||
return $status === Password::PASSWORD_RESET ? $this->redirect(route('login')) : $this->notify($status, 'error');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('auth.password.reset');
|
||||
}
|
||||
}
|
||||
66
app/Livewire/Auth/Register.php
Normal file
66
app/Livewire/Auth/Register.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Auth;
|
||||
|
||||
use App\Attributes\DisabledIf;
|
||||
use App\Livewire\ComponentWithProperties;
|
||||
use App\Models\User;
|
||||
use App\Traits\Captchable;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
#[DisabledIf('registration_disabled')]
|
||||
class Register extends ComponentWithProperties
|
||||
{
|
||||
use Captchable;
|
||||
|
||||
public string $first_name = '';
|
||||
|
||||
public string $last_name = '';
|
||||
|
||||
public string $email = '';
|
||||
|
||||
public string $password = '';
|
||||
|
||||
public string $password_confirmation = '';
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->initializeProperties(null, User::class);
|
||||
}
|
||||
|
||||
public function rules()
|
||||
{
|
||||
return array_merge([
|
||||
'first_name' => 'required|string|max:255',
|
||||
'last_name' => 'required|string|max:255',
|
||||
'email' => 'required|email|max:255|unique:users',
|
||||
'password' => 'required|string|min:8|confirmed',
|
||||
], $this->getRulesForProperties());
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
$validatedData = $this->validate();
|
||||
|
||||
$user = User::create([
|
||||
'first_name' => $this->first_name,
|
||||
'last_name' => $this->last_name,
|
||||
'email' => $this->email,
|
||||
'password' => Hash::make($this->password),
|
||||
]);
|
||||
|
||||
if (array_key_exists('properties', $validatedData)) {
|
||||
$this->updateProperties($user, $validatedData['properties']);
|
||||
}
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
return $this->redirectIntended(route('dashboard'), true);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('auth.register');
|
||||
}
|
||||
}
|
||||
74
app/Livewire/Auth/Tfa.php
Normal file
74
app/Livewire/Auth/Tfa.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Auth;
|
||||
|
||||
use App\Livewire\Component;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use RobThree\Auth\Providers\Qr\EndroidQrCodeProvider;
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
|
||||
class Tfa extends Component
|
||||
{
|
||||
public $code;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$session = Session::get('2fa');
|
||||
if (!$session || $session['expires'] < now()) {
|
||||
return $this->redirect(route('login'), true);
|
||||
}
|
||||
}
|
||||
|
||||
public function verify()
|
||||
{
|
||||
$this->validate([
|
||||
'code' => 'required|numeric',
|
||||
]);
|
||||
|
||||
if (RateLimiter::tooManyAttempts('2fa', 5)) {
|
||||
$this->addError('code', 'Too many attempts.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
RateLimiter::increment('2fa');
|
||||
|
||||
$session = Session::get('2fa');
|
||||
$user = User::findOrfail($session['user_id']);
|
||||
|
||||
if (!$user->tfa_secret) {
|
||||
return $this->redirect(route('login'), true);
|
||||
}
|
||||
|
||||
$tfa = new TwoFactorAuth(new EndroidQrCodeProvider, config('app.name'));
|
||||
if (!$tfa->verifyCode($user->tfa_secret, $this->code)) {
|
||||
return $this->addError('code', 'Invalid code.');
|
||||
}
|
||||
|
||||
// Check if code has been used before, preventing replay attacks
|
||||
if (Cache::has('tfa_used_code_' . $user->id . '_' . $this->code)) {
|
||||
return $this->addError('code', 'Invalid code.');
|
||||
}
|
||||
// Mark code as used for 60 seconds just in case
|
||||
Cache::put('tfa_used_code_' . $user->id . '_' . $this->code, true, 60);
|
||||
|
||||
Auth::loginUsingId($user->id, $session['remember']);
|
||||
|
||||
event(new \App\Events\Auth\Login(User::find(Auth::id())));
|
||||
|
||||
Session::forget('2fa');
|
||||
|
||||
RateLimiter::clear('2fa');
|
||||
|
||||
return $this->redirect(route('dashboard'), true);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('auth.2fa');
|
||||
}
|
||||
}
|
||||
43
app/Livewire/Auth/VerifyEmail.php
Normal file
43
app/Livewire/Auth/VerifyEmail.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Auth;
|
||||
|
||||
use App\Helpers\NotificationHelper;
|
||||
use App\Livewire\Component;
|
||||
use App\Traits\Captchable;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
|
||||
class VerifyEmail extends Component
|
||||
{
|
||||
use Captchable;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
if (Auth::user()->hasVerifiedEmail()) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
$this->captcha();
|
||||
|
||||
if (RateLimiter::tooManyAttempts('email-verification', 1)) {
|
||||
$this->addError('code', 'Too many attempts. Try again later.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
NotificationHelper::emailVerificationNotification(Auth::user());
|
||||
|
||||
RateLimiter::hit('email-verification', 120);
|
||||
|
||||
$this->notify('Verification email sent.');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('auth.verify-email');
|
||||
}
|
||||
}
|
||||
299
app/Livewire/Cart.php
Normal file
299
app/Livewire/Cart.php
Normal file
@@ -0,0 +1,299 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Classes\Cart as ClassesCart;
|
||||
use App\Classes\Price;
|
||||
use App\Exceptions\DisplayException;
|
||||
use App\Helpers\ExtensionHelper;
|
||||
use App\Jobs\Server\CreateJob;
|
||||
use App\Models\Coupon;
|
||||
use App\Models\Gateway;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Order;
|
||||
use App\Models\Service;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Livewire\Attributes\Locked;
|
||||
|
||||
class Cart extends Component
|
||||
{
|
||||
#[Locked]
|
||||
public $total;
|
||||
|
||||
#[Locked]
|
||||
public array $gateways;
|
||||
|
||||
public $gateway;
|
||||
|
||||
public $coupon;
|
||||
|
||||
public $use_credits = true;
|
||||
|
||||
public $tos;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
if (Session::has('coupon')) {
|
||||
$this->coupon = Session::get('coupon');
|
||||
}
|
||||
$this->updateTotal();
|
||||
}
|
||||
|
||||
private function updateTotal()
|
||||
{
|
||||
if (ClassesCart::get()->isEmpty()) {
|
||||
$this->total = null;
|
||||
|
||||
return;
|
||||
}
|
||||
$this->total = new Price(['price' => ClassesCart::get()->sum(fn ($item) => $item->price->price * $item->quantity), 'currency' => ClassesCart::get()->first()->price->currency]);
|
||||
$this->gateways = ExtensionHelper::getCheckoutGateways(ClassesCart::get(), 'cart');
|
||||
if (count($this->gateways) > 0 && !array_search($this->gateway, array_column($this->gateways, 'id')) !== false) {
|
||||
$this->gateway = $this->gateways[0]->id;
|
||||
}
|
||||
}
|
||||
|
||||
public function applyCoupon()
|
||||
{
|
||||
if ($this->coupon && Session::has('coupon')) {
|
||||
return $this->notify('Coupon code already applied', 'error');
|
||||
}
|
||||
try {
|
||||
ClassesCart::applyCoupon($this->coupon);
|
||||
} catch (DisplayException $e) {
|
||||
$this->notify($e->getMessage(), 'error');
|
||||
$this->coupon = null;
|
||||
|
||||
return;
|
||||
}
|
||||
$this->coupon = Session::get('coupon');
|
||||
$this->updateTotal();
|
||||
$this->notify('Coupon code applied successfully', 'success');
|
||||
}
|
||||
|
||||
public function removeCoupon()
|
||||
{
|
||||
if (!$this->coupon || !Session::has('coupon')) {
|
||||
return $this->notify('No coupon code applied', 'error');
|
||||
}
|
||||
ClassesCart::removeCoupon();
|
||||
$this->coupon = null;
|
||||
$this->updateTotal();
|
||||
$this->notify('Coupon code removed successfully', 'success');
|
||||
}
|
||||
|
||||
public function removeProduct($index)
|
||||
{
|
||||
ClassesCart::remove($index);
|
||||
$this->updateTotal();
|
||||
}
|
||||
|
||||
public function updateQuantity($index, $quantity)
|
||||
{
|
||||
if (ClassesCart::get()->get($index)->product->allow_quantity !== 'combined') {
|
||||
return;
|
||||
}
|
||||
if ($quantity < 1) {
|
||||
$this->removeProduct($index);
|
||||
|
||||
return;
|
||||
}
|
||||
ClassesCart::get()->get($index)->quantity = $quantity;
|
||||
session(['cart' => ClassesCart::get()->toArray()]);
|
||||
$this->updateTotal();
|
||||
}
|
||||
|
||||
// Checkout
|
||||
public function checkout()
|
||||
{
|
||||
if (ClassesCart::get()->isEmpty() || Session::has('cart') === false) {
|
||||
return $this->notify('Your cart is empty', 'error');
|
||||
}
|
||||
if (!Auth::check()) {
|
||||
return redirect()->guest('login');
|
||||
}
|
||||
if (config('settings.mail_must_verify') && !Auth::user()->hasVerifiedEmail()) {
|
||||
return redirect()->route('verification.notice');
|
||||
}
|
||||
if (config('settings.tos') && !$this->tos) {
|
||||
return $this->notify('You must accept the terms of service', 'error');
|
||||
}
|
||||
|
||||
// Re-validate coupon if one exists
|
||||
if (Session::has('coupon') && !ClassesCart::validateAndRefreshCoupon()) {
|
||||
$this->coupon = null;
|
||||
$this->updateTotal();
|
||||
|
||||
return $this->notify('This coupon can no longer be used', 'error');
|
||||
}
|
||||
|
||||
// Start database transaction
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$user = User::where('id', Auth::id())->lockForUpdate()->first();
|
||||
// Lock the orderproducts
|
||||
foreach (ClassesCart::get() as $item) {
|
||||
if (
|
||||
$item->product->per_user_limit > 0 && ($user->services->where('product_id', $item->product->id)->count() >= $item->product->per_user_limit ||
|
||||
ClassesCart::get()->filter(fn ($it) => $it->product->id == $item->product->id)->sum(fn ($it) => $it->quantity) + $user->services->where('product_id', $item->product->id)->count() > $item->product->per_user_limit
|
||||
)
|
||||
) {
|
||||
throw new DisplayException(__('product.user_limit', ['product' => $item->product->name]));
|
||||
}
|
||||
if ($item->product->stock !== null) {
|
||||
if ($item->product->stock < $item->quantity) {
|
||||
throw new DisplayException(__('product.out_of_stock', ['product' => $item->product->name]));
|
||||
}
|
||||
|
||||
$item->product->stock -= $item->quantity;
|
||||
$item->product->save();
|
||||
}
|
||||
}
|
||||
// Create the order
|
||||
$order = new Order([
|
||||
'user_id' => $user->id,
|
||||
'currency_code' => $this->total->currency->code,
|
||||
]);
|
||||
$order->save();
|
||||
|
||||
// Create the invoice
|
||||
if ($this->total->price > 0) {
|
||||
$invoice = new Invoice([
|
||||
'user_id' => $user->id,
|
||||
'due_at' => now()->addDays(7),
|
||||
'currency_code' => $this->total->currency->code,
|
||||
]);
|
||||
$invoice->save();
|
||||
}
|
||||
|
||||
// Create the services
|
||||
foreach (ClassesCart::get() as $item) {
|
||||
// Is it a lifetime coupon, then we can adjust the price of the service
|
||||
if ($this->coupon && $this->coupon->recurring != 1) {
|
||||
$price = $item->price->price - $item->price->setup_fee;
|
||||
} else {
|
||||
$price = $item->price->original_price - $item->price->original_setup_fee;
|
||||
}
|
||||
// Create the service
|
||||
$service = $order->services()->create([
|
||||
'user_id' => $user->id,
|
||||
'currency_code' => $this->total->currency->code,
|
||||
'product_id' => $item->product->id,
|
||||
'plan_id' => $item->plan->id,
|
||||
'price' => $price,
|
||||
'quantity' => $item->quantity,
|
||||
'coupon_id' => Session::has('coupon') ? Session::get('coupon')->id : null,
|
||||
]);
|
||||
|
||||
foreach ($item->checkoutConfig as $key => $value) {
|
||||
$service->properties()->updateOrCreate([
|
||||
'key' => $key,
|
||||
], [
|
||||
'value' => $value,
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ($item->configOptions as $configOption) {
|
||||
if (in_array($configOption->option_type, ['text', 'number', 'checkbox'])) {
|
||||
if (!isset($configOption->value)) {
|
||||
continue;
|
||||
}
|
||||
$service->properties()->updateOrCreate([
|
||||
'key' => $configOption->option_env_variable ? $configOption->option_env_variable : $configOption->option_name,
|
||||
], [
|
||||
'name' => $configOption->option_name,
|
||||
'value' => $configOption->value,
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$service->configs()->create([
|
||||
'config_option_id' => $configOption->option_id,
|
||||
'config_value_id' => $configOption->value,
|
||||
]);
|
||||
}
|
||||
|
||||
// Create the invoice items
|
||||
if ($item->price->price > 0) {
|
||||
$invoice->items()->create([
|
||||
'reference_id' => $service->id,
|
||||
'reference_type' => Service::class,
|
||||
'price' => $item->price->price,
|
||||
'quantity' => $item->quantity,
|
||||
'description' => $service->description,
|
||||
]);
|
||||
} else {
|
||||
// We'll make the service active immediately
|
||||
if ($service->product->server) {
|
||||
CreateJob::dispatch($service);
|
||||
}
|
||||
$service->status = Service::STATUS_ACTIVE;
|
||||
$service->expires_at = $service->calculateNextDueDate();
|
||||
$service->save();
|
||||
}
|
||||
}
|
||||
|
||||
// We don't wanna use credits if the total price is 0, duh
|
||||
if ($this->use_credits && $this->total->price > 0) {
|
||||
$credit = Auth::user()->credits()->where('currency_code', $this->total->currency->code)->first();
|
||||
if ($credit && $credit->amount > 0) {
|
||||
// Is it more credits or less credits than the total price?
|
||||
if ($credit->amount >= $this->total->price) {
|
||||
$credit->amount -= $this->total->price;
|
||||
$credit->save();
|
||||
ExtensionHelper::addPayment($invoice->id, null, amount: $this->total->price);
|
||||
} else {
|
||||
$this->total->price -= $credit->amount;
|
||||
ExtensionHelper::addPayment($invoice->id, null, amount: $credit->amount);
|
||||
$credit->amount = 0;
|
||||
$credit->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Commit the transaction
|
||||
DB::commit();
|
||||
|
||||
// Clear the cart
|
||||
Session::forget('cart');
|
||||
Session::forget('coupon');
|
||||
|
||||
// Pass the gateway to the payment page
|
||||
Session::put(['gateway' => $this->gateway]);
|
||||
|
||||
if ($this->total->price == 0) {
|
||||
// Is it only one item? Then redirect to the service page
|
||||
if ($order->services->count() == 1) {
|
||||
return $this->redirect(route('services.show', $order->services->first()), true);
|
||||
}
|
||||
|
||||
return $this->redirect(route('services'), true);
|
||||
} else {
|
||||
return $this->redirect(route('invoices.show', $invoice) . '?pay');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Rollback the transaction
|
||||
DB::rollBack();
|
||||
// Return error message
|
||||
// Is it a real error or a validation error?
|
||||
// If it's a validation error, you can use the $this->addError() method to display the error message to the user.
|
||||
if ($e instanceof DisplayException) {
|
||||
return $this->notify($e->getMessage(), 'error');
|
||||
} else {
|
||||
Log::error($e);
|
||||
$this->notify('An error occurred while processing your order. Please try again later.');
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('cart');
|
||||
}
|
||||
}
|
||||
62
app/Livewire/Client/Account.php
Normal file
62
app/Livewire/Client/Account.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Client;
|
||||
|
||||
use App\Livewire\ComponentWithProperties;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class Account extends ComponentWithProperties
|
||||
{
|
||||
public string $first_name = '';
|
||||
|
||||
public string $last_name = '';
|
||||
|
||||
public string $email = '';
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$this->first_name = $user->first_name;
|
||||
$this->last_name = $user->last_name;
|
||||
$this->email = $user->email;
|
||||
|
||||
$this->initializeProperties($user, $user::class);
|
||||
}
|
||||
|
||||
public function rules()
|
||||
{
|
||||
return array_merge([
|
||||
'first_name' => 'required|string|max:255',
|
||||
'last_name' => 'required|string|max:255',
|
||||
'email' => 'required|email|max:255|unique:users,email,' . Auth::id(),
|
||||
], $this->getRulesForProperties());
|
||||
}
|
||||
|
||||
public function validationAttributes()
|
||||
{
|
||||
return $this->getAttributesForProperties();
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
$validatedData = $this->validate();
|
||||
|
||||
/** @var \App\Models\User $user */
|
||||
$user = Auth::user();
|
||||
$user->update($validatedData);
|
||||
|
||||
if (array_key_exists('properties', $validatedData)) {
|
||||
$this->updateProperties($user, $validatedData['properties']);
|
||||
}
|
||||
|
||||
$this->notify(__('Account updated successfully.'));
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('client.account.index')->layoutData([
|
||||
'sidebar' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
113
app/Livewire/Client/Credits.php
Normal file
113
app/Livewire/Client/Credits.php
Normal file
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Client;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
use App\Helpers\ExtensionHelper;
|
||||
use App\Livewire\Component;
|
||||
use App\Models\Credit;
|
||||
use App\Models\Gateway;
|
||||
use App\Models\Invoice;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\Validate;
|
||||
|
||||
class Credits extends Component
|
||||
{
|
||||
#[Validate('required|exists:currencies,code')]
|
||||
public $currency;
|
||||
|
||||
public $amount;
|
||||
|
||||
#[Locked]
|
||||
public $gateways;
|
||||
|
||||
public $gateway;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
if (!config('settings.credits_enabled')) {
|
||||
return redirect()->route('account');
|
||||
}
|
||||
|
||||
$this->amount = config('settings.credits_minimum_deposit');
|
||||
$this->currency = session('currency', config('settings.default_currency'));
|
||||
$this->gateways = ExtensionHelper::getCheckoutGateways([], 'credits');
|
||||
if (count($this->gateways) > 0 && !array_search($this->gateway, array_column($this->gateways, 'id')) !== false) {
|
||||
$this->gateway = $this->gateways[0]->id;
|
||||
}
|
||||
}
|
||||
|
||||
public function addCredit()
|
||||
{
|
||||
$this->validate([
|
||||
'currency' => 'required|exists:currencies,code',
|
||||
'amount' => 'required|numeric|min:' . config('settings.credits_minimum_deposit') . '|max:' . config('settings.credits_maximum_deposit'),
|
||||
'gateway' => 'required|in:' . implode(',', array_column($this->gateways, 'id')),
|
||||
]);
|
||||
|
||||
if (Auth::user()->credits()->where('currency_code', $this->currency)->exists()) {
|
||||
// Check if the current credits + the new credits exceed the maximum credits allowed
|
||||
if (Auth::user()->credits()->where('currency_code', $this->currency)->sum('amount') + $this->amount > config('settings.credits_maximum_credit')) {
|
||||
$this->notify('You cannot exceed the maximum credits allowed.', 'error');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Create invoice
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
$invoice = Invoice::create([
|
||||
'user_id' => Auth::id(),
|
||||
'currency_code' => $this->currency,
|
||||
'due_at' => now(),
|
||||
]);
|
||||
|
||||
$invoice->items()->create([
|
||||
'description' => __('account.credit_deposit', ['currency' => $this->currency]),
|
||||
'quantity' => 1,
|
||||
'price' => $this->amount,
|
||||
'reference_type' => Credit::class,
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
Session::put(['gateway' => $this->gateway]);
|
||||
|
||||
// Redirect to the invoices page and pay the invoice
|
||||
if ($this->gateway) {
|
||||
$pay = ExtensionHelper::pay(Gateway::where('id', $this->gateway)->first(), $invoice->fresh());
|
||||
if (is_string($pay)) {
|
||||
return $this->redirect($pay);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->redirect(route('invoices.show', $invoice) . '?gateway=' . $this->gateway . '&pay', true);
|
||||
} catch (\Exception $e) {
|
||||
// Rollback the transaction
|
||||
DB::rollBack();
|
||||
// Return error message
|
||||
// Is it a real error or a validation error?
|
||||
// If it's a validation error, you can use the $this->addError() method to display the error message to the user.
|
||||
if ($e instanceof DisplayException) {
|
||||
$this->notify($e->getMessage(), 'error');
|
||||
} else {
|
||||
Log::error($e);
|
||||
$this->notify('An error occurred while processing your order. Please try again later.');
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('client.account.credits')->layoutData([
|
||||
'sidebar' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
117
app/Livewire/Client/Security.php
Normal file
117
app/Livewire/Client/Security.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Client;
|
||||
|
||||
use App\Livewire\Component;
|
||||
use App\Models\Session;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Attributes\Locked;
|
||||
use RobThree\Auth\Providers\Qr\EndroidQrCodeProvider;
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
|
||||
class Security extends Component
|
||||
{
|
||||
public string $current_password = '';
|
||||
|
||||
public string $password = '';
|
||||
|
||||
public string $password_confirmation = '';
|
||||
|
||||
public bool $twoFactorEnabled = false;
|
||||
|
||||
#[Locked]
|
||||
public $twoFactorData = [];
|
||||
|
||||
#[Locked]
|
||||
public $showEnableTwoFactor = false;
|
||||
|
||||
public string $twoFactorCode = '';
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->twoFactorEnabled = Auth::user()->tfa_secret ? true : false;
|
||||
}
|
||||
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'current_password' => 'required|string',
|
||||
'password' => 'required|string|min:8|confirmed',
|
||||
];
|
||||
}
|
||||
|
||||
public function changePassword()
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
if (!Hash::check($this->current_password, Auth::user()->password)) {
|
||||
return $this->notify(__('account.notifications.password_incorrect'), 'error');
|
||||
}
|
||||
|
||||
Auth::user()->update([
|
||||
'password' => Hash::make($this->password),
|
||||
]);
|
||||
|
||||
$this->notify(__('account.notifications.password_changed'));
|
||||
|
||||
$this->reset('current_password', 'password', 'password_confirmation');
|
||||
}
|
||||
|
||||
public function enableTwoFactor()
|
||||
{
|
||||
if ($this->showEnableTwoFactor) {
|
||||
$this->validate([
|
||||
'twoFactorCode' => 'required|string',
|
||||
]);
|
||||
|
||||
$tfa = new TwoFactorAuth(new EndroidQrCodeProvider, config('app.name'));
|
||||
if ($tfa->verifyCode($this->twoFactorData['secret'], $this->twoFactorCode)) {
|
||||
Auth::user()->update([
|
||||
'tfa_secret' => $this->twoFactorData['secret'],
|
||||
]);
|
||||
|
||||
$this->notify(__('account.notifications.two_factor_enabled'));
|
||||
|
||||
$this->twoFactorEnabled = true;
|
||||
$this->showEnableTwoFactor = false;
|
||||
} else {
|
||||
$this->notify(__('account.notifications.two_factor_code_incorrect'), 'error');
|
||||
}
|
||||
} else {
|
||||
$tfa = new TwoFactorAuth(new EndroidQrCodeProvider, config('app.name'));
|
||||
$secret = $tfa->createSecret();
|
||||
$this->twoFactorData = [
|
||||
'secret' => $secret,
|
||||
'image' => $tfa->getQRCodeImageAsDataUri(Auth::user()->email, $secret),
|
||||
];
|
||||
|
||||
$this->showEnableTwoFactor = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function disableTwoFactor()
|
||||
{
|
||||
Auth::user()->update([
|
||||
'tfa_secret' => null,
|
||||
]);
|
||||
|
||||
$this->notify(__('account.notifications.two_factor_disabled'));
|
||||
|
||||
$this->twoFactorEnabled = false;
|
||||
}
|
||||
|
||||
public function logoutSession(Session $session)
|
||||
{
|
||||
$session->delete();
|
||||
|
||||
$this->notify(__('account.notifications.session_logged_out'));
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('client.account.security')->layoutData([
|
||||
'sidebar' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
23
app/Livewire/Component.php
Normal file
23
app/Livewire/Component.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class Component extends \Livewire\Component
|
||||
{
|
||||
use Traits\Disabled, Traits\HasNotifications;
|
||||
|
||||
public function paginationView()
|
||||
{
|
||||
return 'components.pagination';
|
||||
}
|
||||
|
||||
public function exception($e, $stopPropagation)
|
||||
{
|
||||
if ($e instanceof DisplayException) {
|
||||
$this->notify($e->getMessage(), 'error');
|
||||
$stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
86
app/Livewire/ComponentWithProperties.php
Normal file
86
app/Livewire/ComponentWithProperties.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Events\Properties\Updated as PropertiesUpdated;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
class ComponentWithProperties extends Component
|
||||
{
|
||||
// Custom Properties which will be used to render the Inputs
|
||||
public Collection $custom_properties;
|
||||
|
||||
// Values of the Custom Properties
|
||||
public array $properties = [];
|
||||
|
||||
/**
|
||||
* Updates the properties of the model
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model|null $model
|
||||
* @param string $morphClass
|
||||
*/
|
||||
public function initializeProperties($model, $morphClass)
|
||||
{
|
||||
$this->custom_properties = \App\Models\CustomProperty::where('model', $morphClass)->get();
|
||||
if ($model) {
|
||||
$this->properties = $model
|
||||
->properties->mapWithKeys(function ($property) {
|
||||
return [$property->key => $property->value];
|
||||
})
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
public function getRulesForProperties(): array
|
||||
{
|
||||
return $this->custom_properties->mapWithKeys(function ($property) {
|
||||
return ["properties.$property->key" => ($property->required ? 'required|' : 'nullable|') . "$property->validation"];
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the attributes of the properties to show in error messages
|
||||
*/
|
||||
public function getAttributesForProperties(): array
|
||||
{
|
||||
return $this->custom_properties->mapWithKeys(function ($property) {
|
||||
return ["properties.$property->key" => $property->name];
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the properties of the model
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param array $properties
|
||||
*/
|
||||
public function updateProperties($model, $properties)
|
||||
{
|
||||
$properties = collect($properties)->map(function ($value, $key) use ($model) {
|
||||
$custom_property = $this->custom_properties->where('key', $key)->first();
|
||||
|
||||
return [
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
'model_id' => $model->id,
|
||||
'model_type' => $model->getMorphClass(),
|
||||
'name' => $custom_property->name,
|
||||
'custom_property_id' => $custom_property->id,
|
||||
];
|
||||
})->toArray();
|
||||
|
||||
$model->properties()->upsert($properties, uniqueBy: [
|
||||
'key',
|
||||
'model_id',
|
||||
'model_type',
|
||||
], update: [
|
||||
'name',
|
||||
'value',
|
||||
'model_id',
|
||||
'model_type',
|
||||
'custom_property_id',
|
||||
]);
|
||||
|
||||
event(new PropertiesUpdated($model, $properties));
|
||||
}
|
||||
}
|
||||
31
app/Livewire/Components/Cart.php
Normal file
31
app/Livewire/Components/Cart.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Components;
|
||||
|
||||
use App\Classes\Cart as ClassesCart;
|
||||
use App\Livewire\Component;
|
||||
use Livewire\Attributes\On;
|
||||
|
||||
class Cart extends Component
|
||||
{
|
||||
public $cartCount;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->cartCount = ClassesCart::get()->count();
|
||||
if (ClassesCart::get()->isEmpty()) {
|
||||
$this->skipRender();
|
||||
}
|
||||
}
|
||||
|
||||
#[On('cartUpdated')]
|
||||
public function onCartUpdated()
|
||||
{
|
||||
$this->cartCount = ClassesCart::get()->count();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('components.cart');
|
||||
}
|
||||
}
|
||||
44
app/Livewire/Components/CurrencySwitch.php
Normal file
44
app/Livewire/Components/CurrencySwitch.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Components;
|
||||
|
||||
use App\Classes\Cart;
|
||||
use App\Livewire\Component;
|
||||
use App\Models\Currency;
|
||||
|
||||
class CurrencySwitch extends Component
|
||||
{
|
||||
public $currentCurrency;
|
||||
|
||||
protected $currencies = [];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->currentCurrency = session('currency', config('settings.default_currency'));
|
||||
$this->currencies = Currency::all()->map(fn ($currency) => [
|
||||
'value' => $currency->code,
|
||||
'label' => $currency->code,
|
||||
])->values()->toArray();
|
||||
if (Cart::get()->isNotEmpty() || count($this->currencies) <= 1) {
|
||||
$this->skipRender();
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedCurrentCurrency($currency)
|
||||
{
|
||||
if (Cart::get()->isNotEmpty()) {
|
||||
$this->notify('You cannot change the currency while there are items in the cart.', 'error');
|
||||
$this->currentCurrency = session('currency', config('settings.default_currency'));
|
||||
|
||||
return;
|
||||
}
|
||||
session(['currency' => $currency]);
|
||||
|
||||
return $this->redirect(request()->header('Referer', '/'), navigate: true);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('components.currency-switch');
|
||||
}
|
||||
}
|
||||
34
app/Livewire/Components/LanguageSwitch.php
Normal file
34
app/Livewire/Components/LanguageSwitch.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Components;
|
||||
|
||||
use App\Livewire\Component;
|
||||
|
||||
class LanguageSwitch extends Component
|
||||
{
|
||||
public $currentLocale;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->currentLocale = session('locale', config('app.locale'));
|
||||
}
|
||||
|
||||
public function updatedCurrentLocale($locale)
|
||||
{
|
||||
if (!array_key_exists($locale, config('app.available_locales', []))) {
|
||||
return;
|
||||
}
|
||||
|
||||
session(['locale' => $locale]);
|
||||
app()->setLocale($locale);
|
||||
|
||||
return $this->redirect(request()->header('Referer', '/'), navigate: true);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$locales = config('app.available_locales');
|
||||
|
||||
return view('components.language-switch', compact('locales'));
|
||||
}
|
||||
}
|
||||
15
app/Livewire/Dashboard.php
Normal file
15
app/Livewire/Dashboard.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
class Dashboard extends Component
|
||||
{
|
||||
public $activeComponent = 'services';
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('dashboard')->layoutData([
|
||||
'sidebar' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
21
app/Livewire/Home.php
Normal file
21
app/Livewire/Home.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Category;
|
||||
|
||||
class Home extends Component
|
||||
{
|
||||
public function render()
|
||||
{
|
||||
return view('home', [
|
||||
// Categories which have NO parent and at least one child
|
||||
'categories' => Category::whereNull('parent_id')->where(function ($query) {
|
||||
$query->whereHas('children')->orWhereHas('products', function ($query) {
|
||||
$query->where('hidden', false);
|
||||
});
|
||||
})->orderBy('sort')->get(),
|
||||
'title' => 'Home',
|
||||
]);
|
||||
}
|
||||
}
|
||||
22
app/Livewire/Invoices/Index.php
Normal file
22
app/Livewire/Invoices/Index.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Invoices;
|
||||
|
||||
use App\Livewire\Component;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
class Index extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('invoices.index', [
|
||||
'invoices' => Auth::user()->invoices()->orderBy('id', 'desc')->paginate(config('settings.pagination')),
|
||||
])->layoutData([
|
||||
'title' => __('invoices.invoices'),
|
||||
'sidebar' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
132
app/Livewire/Invoices/Show.php
Normal file
132
app/Livewire/Invoices/Show.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Invoices;
|
||||
|
||||
use App\Classes\PDF;
|
||||
use App\Helpers\ExtensionHelper;
|
||||
use App\Livewire\Component;
|
||||
use App\Models\Credit;
|
||||
use App\Models\Gateway;
|
||||
use App\Models\Invoice;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Request;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\Url;
|
||||
|
||||
class Show extends Component
|
||||
{
|
||||
#[Locked]
|
||||
public Invoice $invoice;
|
||||
|
||||
#[Url]
|
||||
public $gateway = null;
|
||||
|
||||
#[Locked]
|
||||
public $gateways;
|
||||
|
||||
public $checkPayment = false;
|
||||
|
||||
private $pay = null;
|
||||
|
||||
public $use_credits = false;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$sessionGateway = session('gateway');
|
||||
if ($sessionGateway) {
|
||||
$this->gateway = $sessionGateway;
|
||||
}
|
||||
|
||||
if ($this->invoice->status === 'pending') {
|
||||
$this->gateways = ExtensionHelper::getCheckoutGateways($this->invoice->items, 'invoice');
|
||||
if (count($this->gateways) > 0 && !array_search($this->gateway, array_column($this->gateways, 'id')) !== false) {
|
||||
$this->gateway = $this->gateways[0]->id;
|
||||
}
|
||||
if ($sessionGateway && Request::has('pay')) {
|
||||
$this->pay();
|
||||
}
|
||||
}
|
||||
if (Request::has('checkPayment') && $this->invoice->status === 'pending') {
|
||||
$this->checkPayment = true;
|
||||
}
|
||||
|
||||
// We don't want to toggle use_credits before $this->pay() is called, otherwise it will always paid with credits
|
||||
$this->use_credits = true;
|
||||
$hasCredits = $this->invoice->items()->where('reference_type', Credit::class)->exists();
|
||||
if ($hasCredits) {
|
||||
$this->use_credits = false;
|
||||
}
|
||||
}
|
||||
|
||||
public function pay()
|
||||
{
|
||||
if ($this->use_credits) {
|
||||
$credit = Auth::user()->credits()->where('currency_code', $this->invoice->currency_code)->first();
|
||||
if ($credit && $credit->amount > 0) {
|
||||
// Is it more credits or less credits than the total price?
|
||||
if ($credit->amount >= $this->invoice->remaining) {
|
||||
$credit->amount -= $this->invoice->remaining;
|
||||
$credit->save();
|
||||
ExtensionHelper::addPayment($this->invoice->id, null, amount: $this->invoice->remaining);
|
||||
|
||||
return $this->redirect(route('invoices.show', $this->invoice), true);
|
||||
} else {
|
||||
ExtensionHelper::addPayment($this->invoice->id, null, amount: $credit->amount);
|
||||
$credit->amount = 0;
|
||||
$credit->save();
|
||||
|
||||
$this->invoice = $this->invoice->fresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->invoice->status !== 'pending') {
|
||||
return $this->notify(__('This invoice cannot be paid.'), 'error');
|
||||
}
|
||||
if ($this->checkPayment) {
|
||||
$this->checkPayment = false;
|
||||
}
|
||||
$this->validate([
|
||||
'gateway' => 'required',
|
||||
]);
|
||||
|
||||
$this->pay = ExtensionHelper::pay(Gateway::where('id', $this->gateway)->first(), $this->invoice);
|
||||
|
||||
if (is_string($this->pay)) {
|
||||
$this->redirect($this->pay);
|
||||
}
|
||||
}
|
||||
|
||||
public function exitPay()
|
||||
{
|
||||
$this->pay = null;
|
||||
// Dispatch event so extensions can do their thing
|
||||
$this->dispatch('invoice.payment.cancelled', $this->invoice);
|
||||
// Refresh invoice status
|
||||
$this->redirect(route('invoices.show', $this->invoice), true);
|
||||
}
|
||||
|
||||
public function checkPaymentStatus()
|
||||
{
|
||||
$this->invoice->refresh();
|
||||
if ($this->invoice->status === 'paid') {
|
||||
$this->notify(__('The invoice has been paid.'), 'success');
|
||||
$this->checkPayment = false;
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('invoices.show')->layoutData([
|
||||
'title' => __('invoices.invoice', ['id' => $this->invoice->number]),
|
||||
'sidebar' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function downloadPDF()
|
||||
{
|
||||
return response()->streamDownload(function () {
|
||||
echo PDF::generateInvoice($this->invoice)->stream();
|
||||
}, 'invoice-' . $this->invoice->number . '.pdf');
|
||||
}
|
||||
}
|
||||
21
app/Livewire/Invoices/Widget.php
Normal file
21
app/Livewire/Invoices/Widget.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Invoices;
|
||||
|
||||
use App\Livewire\Component;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
class Widget extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('invoices.widget', [
|
||||
'invoices' => Auth::user()->invoices()->orderBy('id', 'desc')->where('status', '=', 'pending')->paginate(config('settings.pagination')),
|
||||
])->layoutData([
|
||||
'title' => __('invoices.invoices'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
242
app/Livewire/Products/Checkout.php
Normal file
242
app/Livewire/Products/Checkout.php
Normal file
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Products;
|
||||
|
||||
use App\Classes\Cart;
|
||||
use App\Classes\Price;
|
||||
use App\Helpers\ExtensionHelper;
|
||||
use App\Livewire\Component;
|
||||
use App\Models\Category;
|
||||
use App\Models\Plan;
|
||||
use App\Models\Price as ModelsPrice;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\Url;
|
||||
|
||||
class Checkout extends Component
|
||||
{
|
||||
public $product;
|
||||
|
||||
public Category $category;
|
||||
|
||||
public Plan $plan;
|
||||
|
||||
#[Url(keep: true, as: 'plan')]
|
||||
public $plan_id;
|
||||
|
||||
// Don't allow the user to change the total via hacks
|
||||
#[Locked]
|
||||
public $total;
|
||||
|
||||
public $setup_fee;
|
||||
|
||||
#[Url(keep: true, as: 'options')]
|
||||
public $configOptions = [];
|
||||
|
||||
#[Url(keep: true, as: 'config')]
|
||||
public $checkoutConfig = [];
|
||||
|
||||
#[Url(as: 'edit'), Locked]
|
||||
public $cartProductKey = null;
|
||||
|
||||
public function mount($product)
|
||||
{
|
||||
$this->product = $this->category->products()->where('slug', $product)->firstOrFail();
|
||||
if ($this->product->stock === 0) {
|
||||
return $this->redirect(route('products.show', ['category' => $this->category, 'product' => $this->product]), true);
|
||||
}
|
||||
|
||||
// Is there a existing item in the cart?
|
||||
if (Cart::get()->has($this->cartProductKey) && Cart::get()->get($this->cartProductKey)->product->id === $this->product->id) {
|
||||
$item = Cart::get()->get($this->cartProductKey);
|
||||
// Get the item from the cart
|
||||
$this->plan = $item->plan->fresh();
|
||||
$this->plan_id = $this->plan->id;
|
||||
$this->configOptions = $item->configOptions->mapWithKeys(function ($option) {
|
||||
return [$option->option_id => $option->value];
|
||||
});
|
||||
$this->checkoutConfig = (array) $item->checkoutConfig;
|
||||
} else {
|
||||
// Set the first plan as default
|
||||
$this->plan = $this->plan_id ? $this->product->plans->findOrFail($this->plan_id) : $this->product->plans->first();
|
||||
$this->plan_id = $this->plan->id;
|
||||
|
||||
// Prepare the config options
|
||||
$this->configOptions = $this->product->configOptions->mapWithKeys(function ($option) {
|
||||
if (in_array($option->type, ['text', 'number', 'checkbox'])) {
|
||||
return [$option->id => $this->configOptions[$option->id] ?? null];
|
||||
}
|
||||
|
||||
return [$option->id => $this->configOptions[$option->id] ?? $option->children->first()->id];
|
||||
})->toArray();
|
||||
foreach ($this->getCheckoutConfig() as $config) {
|
||||
if (in_array($config['type'], ['select', 'radio'])) {
|
||||
$this->checkoutConfig[$config['name']] = $this->checkoutConfig[$config['name']] ?? $config['default'] ?? array_key_first($config['options']);
|
||||
} else {
|
||||
$this->checkoutConfig[$config['name']] = $this->checkoutConfig[$config['name']] ?? $config['default'] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Update the pricing
|
||||
$this->updatePricing();
|
||||
|
||||
// As there is only one plan, config options and checkout config, we can directly call the checkout method to avoid confusion
|
||||
// This is only done when the user is not editing the cart item
|
||||
if ($this->product->plans->count() === 1 && empty($this->configOptions) && empty($this->checkoutConfig)) {
|
||||
$this->checkout();
|
||||
}
|
||||
}
|
||||
|
||||
public function updatePricing()
|
||||
{
|
||||
$total = $this->plan->price()->price + $this->product->configOptions->sum(function ($option) {
|
||||
if ($option->type === 'checkbox' && (isset($this->configOptions[$option->id]) && $this->configOptions[$option->id])) {
|
||||
return $option->children->first()?->price(billing_period: $this->plan->billing_period, billing_unit: $this->plan->billing_unit)->price;
|
||||
}
|
||||
if (in_array($option->type, ['text', 'number', 'checkbox'])) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $option->children->where('id', $this->configOptions[$option->id])->first()?->price(billing_period: $this->plan->billing_period, billing_unit: $this->plan->billing_unit)->price;
|
||||
});
|
||||
$setup_fee = $this->plan->price()->setup_fee + $this->product->configOptions->sum(function ($option) {
|
||||
if (in_array($option->type, ['text', 'number', 'checkbox'])) {
|
||||
return $option->children->first()?->price(billing_period: $this->plan->billing_period, billing_unit: $this->plan->billing_unit)->setup_fee;
|
||||
}
|
||||
|
||||
return $option->children->where('id', $this->configOptions[$option->id])->first()?->price(billing_period: $this->plan->billing_period, billing_unit: $this->plan->billing_unit)->setup_fee;
|
||||
});
|
||||
$this->total = new Price([
|
||||
'price' => $total + $setup_fee,
|
||||
'currency' => $this->plan->price()->currency,
|
||||
'setup_fee' => $setup_fee,
|
||||
], apply_exclusive_tax: true);
|
||||
}
|
||||
|
||||
// On change of the plan, update the config options
|
||||
public function updatedPlanId($value)
|
||||
{
|
||||
$this->plan = Plan::findOrFail($value);
|
||||
$this->updatePricing();
|
||||
}
|
||||
|
||||
// On change of the config options, update the pricing
|
||||
public function updatedConfigOptions()
|
||||
{
|
||||
$this->updatePricing();
|
||||
}
|
||||
|
||||
public function getCheckoutConfig()
|
||||
{
|
||||
return once(fn () => ExtensionHelper::getCheckoutConfig($this->product, $this->checkoutConfig));
|
||||
}
|
||||
|
||||
public function rules()
|
||||
{
|
||||
$rules = [
|
||||
'plan_id' => [
|
||||
'required',
|
||||
Rule::exists('plans', 'id')->where(function ($query) {
|
||||
$query->where('priceable_id', $this->product->id)->where('priceable_type', get_class($this->product));
|
||||
}),
|
||||
],
|
||||
];
|
||||
foreach ($this->product->configOptions as $option) {
|
||||
if (in_array($option->type, ['text', 'number'])) {
|
||||
$rules["configOptions.{$option->id}"] = ['required'];
|
||||
} elseif ($option->type === 'checkbox') {
|
||||
} else {
|
||||
$rules["configOptions.{$option->id}"] = ['required', 'exists:config_options,id'];
|
||||
}
|
||||
}
|
||||
foreach ($this->getCheckoutConfig() as $key => $config) {
|
||||
$validationRules = [];
|
||||
if ($config['required'] ?? false) {
|
||||
$validationRules[] = 'required';
|
||||
}
|
||||
if (isset($config['type'])) {
|
||||
switch ($config['type']) {
|
||||
case 'text':
|
||||
case 'number':
|
||||
$validationRules[] = 'string';
|
||||
break;
|
||||
case 'select':
|
||||
case 'radio':
|
||||
$validationRules[] = 'in:' . implode(',', array_keys($config['options']));
|
||||
break;
|
||||
case 'checkbox':
|
||||
$validationRules[] = 'nullable';
|
||||
$validationRules[] = 'boolean';
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isset($config['validation'])) {
|
||||
if (is_array($config['validation'])) {
|
||||
$validationRules = array_merge($validationRules, $config['validation']);
|
||||
} else {
|
||||
// Is validation seperated by |?
|
||||
$validationRules = array_merge($validationRules, explode('|', $config['validation']));
|
||||
}
|
||||
}
|
||||
if (count($validationRules) > 0) {
|
||||
$rules["checkoutConfig.{$config['name']}"] = $validationRules;
|
||||
}
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
public function attributes()
|
||||
{
|
||||
$messages = [];
|
||||
foreach ($this->product->configOptions as $option) {
|
||||
$messages["configOptions.{$option->id}"] = $option->name;
|
||||
}
|
||||
foreach ($this->getCheckoutConfig() as $key => $config) {
|
||||
$messages["checkoutConfig.{$config['name']}"] = $config['label'] ?? $config['name'];
|
||||
}
|
||||
|
||||
return $messages;
|
||||
}
|
||||
|
||||
public function checkout()
|
||||
{
|
||||
// Do the checkout
|
||||
// First we validate the plans
|
||||
$this->validate(attributes: $this->attributes());
|
||||
|
||||
// Change configOptions so they also contain the name of the option (resulting in less database calls = faster speeds)
|
||||
$configOptions = $this->product->configOptions->map(function ($option) {
|
||||
if (in_array($option->type, ['text', 'number', 'checkbox'])) {
|
||||
return (object) ['option_id' => $option->id, 'option_name' => $option->name, 'option_type' => $option->type, 'option_env_variable' => $option->env_variable, 'value' => $this->configOptions[$option->id], 'value_name' => $this->configOptions[$option->id]];
|
||||
}
|
||||
|
||||
return (object) ['option_id' => $option->id, 'option_name' => $option->name, 'option_type' => $option->type, 'option_env_variable' => $option->env_variable, 'value' => $this->configOptions[$option->id], 'value_name' => $option->children->where('id', $this->configOptions[$option->id])->first()->name];
|
||||
});
|
||||
|
||||
Cart::add($this->product, $this->plan, $configOptions, $this->checkoutConfig, $this->total, key: $this->cartProductKey);
|
||||
|
||||
$this->dispatch('cartUpdated');
|
||||
|
||||
return $this->redirect(route('cart'), true);
|
||||
}
|
||||
|
||||
private function total()
|
||||
{
|
||||
// A class isn't allowed to be a Livewire property
|
||||
$totalPrice['price'] = new ModelsPrice(['price' => $this->total]);
|
||||
$totalPrice['currency'] = $this->plan->price()->currency;
|
||||
$totalPrice = new Price((object) $totalPrice);
|
||||
|
||||
return $totalPrice;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('products.checkout')->layoutData([
|
||||
'title' => $this->product->name,
|
||||
'image' => $this->product->image ? Storage::url($this->product->image) : null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
44
app/Livewire/Products/Index.php
Normal file
44
app/Livewire/Products/Index.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Products;
|
||||
|
||||
use App\Livewire\Component;
|
||||
use App\Models\Category;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class Index extends Component
|
||||
{
|
||||
public $products;
|
||||
|
||||
public $categories;
|
||||
|
||||
public $childCategories;
|
||||
|
||||
public Category $category;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->products = $this->category->products()->where('hidden', false)->with('category')->orderBy('sort')->get();
|
||||
$this->childCategories = $this->category->children()->where(function ($query) {
|
||||
$query->whereHas('children')->orWhereHas('products', function ($query) {
|
||||
$query->where('hidden', false);
|
||||
});
|
||||
})->orderBy('sort')->get();
|
||||
$this->categories = Category::whereNull('parent_id')->where(function ($query) {
|
||||
$query->whereHas('children')->orWhereHas('products', function ($query) {
|
||||
$query->where('hidden', false);
|
||||
});
|
||||
})->orderBy('sort')->get();
|
||||
if (count($this->products) === 0 && count($this->childCategories) === 0) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('products.index')->layoutData([
|
||||
'title' => $this->category->name,
|
||||
'image' => $this->category->image ? Storage::url($this->category->image) : null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
27
app/Livewire/Products/Show.php
Normal file
27
app/Livewire/Products/Show.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Products;
|
||||
|
||||
use App\Livewire\Component;
|
||||
use App\Models\Category;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class Show extends Component
|
||||
{
|
||||
public $product;
|
||||
|
||||
public Category $category;
|
||||
|
||||
public function mount($product)
|
||||
{
|
||||
$this->product = $this->category->products()->where('slug', $product)->where('hidden', false)->firstOrFail();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('products.show')->layoutData([
|
||||
'title' => $this->product->name,
|
||||
'image' => $this->product->image ? Storage::url($this->product->image) : null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
45
app/Livewire/Services/Cancel.php
Normal file
45
app/Livewire/Services/Cancel.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Services;
|
||||
|
||||
use App\Livewire\Component;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceCancellation;
|
||||
use Livewire\Attributes\Validate;
|
||||
|
||||
class Cancel extends Component
|
||||
{
|
||||
public Service $service;
|
||||
|
||||
#[Validate('required|in:end_of_period,immediate')]
|
||||
public $type = 'end_of_period';
|
||||
|
||||
#[Validate('required')]
|
||||
public $reason = '';
|
||||
|
||||
public function cancelService()
|
||||
{
|
||||
$this->authorize('view', $this->service);
|
||||
|
||||
$this->validate();
|
||||
|
||||
// Event hook will handle the cancellation (if its immediate or end of period)
|
||||
ServiceCancellation::create([
|
||||
'service_id' => $this->service->id,
|
||||
'type' => $this->type,
|
||||
'reason' => $this->reason,
|
||||
]);
|
||||
|
||||
$this->notify(__('services.cancellation_requested'), 'success');
|
||||
|
||||
$this->redirect(route('services.show', $this->service), true);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('services.cancel')->layoutData([
|
||||
'title' => __('services.cancellation', ['service' => $this->service->product->name]),
|
||||
'sidebar' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
37
app/Livewire/Services/Index.php
Normal file
37
app/Livewire/Services/Index.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Services;
|
||||
|
||||
use App\Livewire\Component;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
class Index extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
public $status = null;
|
||||
|
||||
public function render()
|
||||
{
|
||||
$query = Auth::user()->services()
|
||||
->with([
|
||||
'product.category',
|
||||
'product.server',
|
||||
'plan',
|
||||
'proxmoxServer',
|
||||
])
|
||||
->orderBy('created_at', 'desc');
|
||||
|
||||
if ($this->status) {
|
||||
$query->where('status', $this->status);
|
||||
}
|
||||
|
||||
return view('services.index', [
|
||||
'services' => $query->paginate(config('settings.pagination')),
|
||||
])->layoutData([
|
||||
'title' => 'Services',
|
||||
'sidebar' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
117
app/Livewire/Services/Show.php
Normal file
117
app/Livewire/Services/Show.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Services;
|
||||
|
||||
use App\Helpers\ExtensionHelper;
|
||||
use App\Livewire\Component;
|
||||
use App\Models\Service;
|
||||
use Paymenter\Extensions\Servers\Proxmox\Models\Server as ProxmoxServer;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\Url;
|
||||
|
||||
class Show extends Component
|
||||
{
|
||||
public Service $service;
|
||||
|
||||
public ?ProxmoxServer $proxmoxServer = null;
|
||||
|
||||
#[Locked]
|
||||
public $buttons = [];
|
||||
|
||||
#[Locked]
|
||||
public $views = [];
|
||||
|
||||
#[Url('tab', except: false), Locked]
|
||||
public $currentView;
|
||||
|
||||
#[Url('cancel', except: false)]
|
||||
public bool $showCancel = false;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
// Attach Proxmox server (if any) so we can show hostname on the service page
|
||||
$this->proxmoxServer = ProxmoxServer::where('service_id', $this->service->id)->first();
|
||||
|
||||
// Only fetch the actions if the service is active
|
||||
if ($this->service->status == Service::STATUS_ACTIVE) {
|
||||
$actions = [];
|
||||
try {
|
||||
$actions = ExtensionHelper::getActions($this->service);
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
// separate the actions into buttons and views
|
||||
foreach ($actions as $action) {
|
||||
if ($action['type'] == 'button') {
|
||||
$this->buttons[] = $action;
|
||||
} elseif ($action['type'] == 'view') {
|
||||
$this->views[] = $action;
|
||||
}
|
||||
}
|
||||
$this->currentView = $this->currentView ?? ($this->views[0]['name'] ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
public function changeView($view)
|
||||
{
|
||||
if (!$view) {
|
||||
return;
|
||||
}
|
||||
if ($this->currentView === $view || !in_array($view, array_column($this->views, 'name'))) {
|
||||
return $this->skipRender();
|
||||
}
|
||||
$this->currentView = $view;
|
||||
}
|
||||
|
||||
public function updatedShowCancel($value)
|
||||
{
|
||||
if (!$this->service->cancellable) {
|
||||
$this->notify('This service cannot be cancelled', 'error');
|
||||
$this->showCancel = false;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public function goto($function)
|
||||
{
|
||||
// Check if function is allowed
|
||||
if (!in_array($function, array_column($this->buttons, 'function'))) {
|
||||
$this->notify('This action is not allowed', 'error');
|
||||
|
||||
return;
|
||||
}
|
||||
$result = ExtensionHelper::callService($this->service, $function);
|
||||
// If its a response, return it
|
||||
if (!is_string($result)) {
|
||||
return $result;
|
||||
}
|
||||
$this->redirect($result);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$view = null;
|
||||
$previousView = $this->currentView;
|
||||
|
||||
if ($this->currentView) {
|
||||
try {
|
||||
// Search array for the current view
|
||||
$currentViewObj = $this->views[array_search($this->currentView, array_column($this->views, 'name'))] ?? null;
|
||||
if (!$currentViewObj) {
|
||||
throw new \Exception('View not found');
|
||||
}
|
||||
$view = ExtensionHelper::getView($this->service, $currentViewObj);
|
||||
} catch (\Exception $e) {
|
||||
if ($previousView !== $this->views[0]['name'] ?? null) {
|
||||
$this->notify('Got an error while trying to load the view', 'error');
|
||||
}
|
||||
$this->currentView = $this->views[0]['name'] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
return view('services.show', ['extensionView' => $view])->layoutData([
|
||||
'title' => 'Services',
|
||||
'sidebar' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
263
app/Livewire/Services/Upgrade.php
Normal file
263
app/Livewire/Services/Upgrade.php
Normal file
@@ -0,0 +1,263 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Services;
|
||||
|
||||
use App\Classes\Price;
|
||||
use App\Events\Invoice\Created as InvoiceCreated;
|
||||
use App\Jobs\Server\UpgradeJob;
|
||||
use App\Livewire\Component;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Product;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceUpgrade;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Attributes\Computed;
|
||||
|
||||
class Upgrade extends Component
|
||||
{
|
||||
public Service $service;
|
||||
|
||||
public $upgrade;
|
||||
|
||||
public Product $upgradeProduct;
|
||||
|
||||
public int $step = 1;
|
||||
|
||||
public $configOptions = [];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->authorize('view', $this->service);
|
||||
|
||||
if (!$this->service->upgradable) {
|
||||
$this->notify('This service is not upgradable.', 'error');
|
||||
|
||||
return $this->redirect(route('services.show', $this->service), true);
|
||||
}
|
||||
$this->upgradeProduct = $this->service->product;
|
||||
$this->upgrade = $this->service->product->id;
|
||||
$this->totalToday();
|
||||
|
||||
// We only have upgrabble config options if the product has any
|
||||
if ($this->service->productUpgrades()->count() === 0) {
|
||||
$this->nextStep();
|
||||
}
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function totalToday()
|
||||
{
|
||||
$upgrade = new ServiceUpgrade([
|
||||
'service' => $this->service,
|
||||
'product' => $this->upgradeProduct,
|
||||
]);
|
||||
|
||||
$total = $upgrade->calculateProratedAmount(
|
||||
$this->service->product,
|
||||
$this->upgradeProduct
|
||||
)->price;
|
||||
|
||||
// Calculate prices for config options
|
||||
foreach ($this->configOptions as $optionId => $value) {
|
||||
$option = $this->upgradeProduct->upgradableConfigOptions->where('id', $optionId)->first();
|
||||
if (!$option || !$option->children->contains('id', $value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$oldPrice = $this->service->configs->where('config_option_id', $optionId)->first();
|
||||
|
||||
$ctotal = $upgrade->calculateProratedAmount(
|
||||
$oldPrice ? $oldPrice->configValue : null,
|
||||
$option->children->find($value)
|
||||
);
|
||||
$total += $ctotal->price;
|
||||
}
|
||||
|
||||
return new Price([
|
||||
'price' => $total,
|
||||
'currency' => $this->service->currency,
|
||||
]);
|
||||
}
|
||||
|
||||
// When upgrade changes, update the upgradeProduct
|
||||
public function updatedUpgrade($upgrade)
|
||||
{
|
||||
// Check if the upgrade is valid
|
||||
if (!$this->service->productUpgrades()->contains($upgrade) && $upgrade != $this->service->product_id) {
|
||||
$this->notify('Invalid upgrade.', 'error');
|
||||
|
||||
return;
|
||||
}
|
||||
$this->upgradeProduct = Product::findOrFail($upgrade);
|
||||
}
|
||||
|
||||
public function nextStep()
|
||||
{
|
||||
$currentConfigOptions = $this->service->configs->pluck('config_value_id', 'config_option_id')->toArray();
|
||||
$this->configOptions = $this->upgradeProduct->upgradableConfigOptions->mapWithKeys(function ($option) use ($currentConfigOptions) {
|
||||
return [
|
||||
$option->id => $currentConfigOptions[$option->id]
|
||||
?? $this->configOptions[$option->id]
|
||||
?? $option->children->first()->id,
|
||||
];
|
||||
})->toArray();
|
||||
|
||||
$this->step++;
|
||||
}
|
||||
|
||||
public function rules()
|
||||
{
|
||||
$rules = [
|
||||
'upgradeProduct.id' => [
|
||||
'required',
|
||||
function ($attribute, $value, $fail) {
|
||||
$plan = $this->upgradeProduct->availablePlans()
|
||||
->where('billing_period', $this->service->plan->billing_period)
|
||||
->where('billing_unit', $this->service->plan->billing_unit)
|
||||
->first();
|
||||
if (!$plan) {
|
||||
$fail(__('Invalid upgrade.'));
|
||||
}
|
||||
},
|
||||
],
|
||||
|
||||
];
|
||||
foreach ($this->upgradeProduct->upgradableConfigOptions as $option) {
|
||||
if (in_array($option->type, ['text', 'number'])) {
|
||||
$rules["configOptions.{$option->id}"] = ['required'];
|
||||
} elseif ($option->type === 'checkbox') {
|
||||
} else {
|
||||
$rules["configOptions.{$option->id}"] = ['required', 'exists:config_options,id'];
|
||||
}
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
public function doUpgrade()
|
||||
{
|
||||
if (!$this->service->upgradable) {
|
||||
$this->notify('This service is not upgradable.', 'error');
|
||||
|
||||
return $this->redirect(route('services.show', $this->service), true);
|
||||
}
|
||||
|
||||
$this->validate();
|
||||
|
||||
$upgradePlan = $this->upgradeProduct->availablePlans()->where('billing_period', $this->service->plan->billing_period)->where('billing_unit', $this->service->plan->billing_unit)->first();
|
||||
|
||||
// The old config options must be in upgradableConfigOptions
|
||||
$serviceConfigOptions = $this->service->configs->pluck('config_value_id', 'config_option_id')->toArray();
|
||||
$configOptions = collect($serviceConfigOptions)->filter(function ($value, $key) use ($serviceConfigOptions) {
|
||||
return isset($serviceConfigOptions[$key]) && $this->upgradeProduct->upgradableConfigOptions->contains('id', $key);
|
||||
})->toArray();
|
||||
|
||||
// If the product did not change and the config options are the same, present the user with a message
|
||||
if ($this->upgradeProduct->id === $this->service->product_id && $this->configOptions == $configOptions) {
|
||||
$this->notify('You have not changed anything. Please select a different product or change the configuration options.', 'error');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$upgrade = new ServiceUpgrade([
|
||||
'service_id' => $this->service->id,
|
||||
'product_id' => $this->upgradeProduct->id,
|
||||
'plan_id' => $upgradePlan->id,
|
||||
]);
|
||||
$upgrade->save();
|
||||
|
||||
if ($this->configOptions) {
|
||||
foreach ($this->configOptions as $optionId => $value) {
|
||||
$upgrade->configs()->create([
|
||||
'config_option_id' => $optionId,
|
||||
'config_value_id' => $value,
|
||||
]);
|
||||
}
|
||||
}
|
||||
$price = $upgrade->calculatePrice();
|
||||
|
||||
if ($price->price <= 0) {
|
||||
$upgrade->status = ServiceUpgrade::STATUS_COMPLETED;
|
||||
$upgrade->save();
|
||||
|
||||
$upgrade->service()->update([
|
||||
'plan_id' => $upgrade->plan_id,
|
||||
'product_id' => $upgrade->product_id,
|
||||
]);
|
||||
|
||||
// Update the service configs
|
||||
foreach ($upgrade->configs as $config) {
|
||||
$upgrade->service->configs()->updateOrCreate(
|
||||
['config_option_id' => $config->config_option_id],
|
||||
['config_value_id' => $config->config_value_id]
|
||||
);
|
||||
}
|
||||
|
||||
$this->service->refresh();
|
||||
|
||||
$this->service->recalculatePrice();
|
||||
|
||||
if ($this->service->product->server) {
|
||||
// If the service has a server, dispatch the upgrade job
|
||||
UpgradeJob::dispatch($this->service);
|
||||
}
|
||||
|
||||
// Check if user has credits in this currency
|
||||
/** @var \App\Models\User */
|
||||
$user = Auth::user();
|
||||
$credit = $user->credits()->where('currency_code', $price->currency->code)->first();
|
||||
|
||||
if ($credit) {
|
||||
// Increment the credits, `abs()` ensures the amount to add is positive
|
||||
$credit->increment('amount', abs($price->price));
|
||||
} else {
|
||||
$user->credits()->create([
|
||||
'currency_code' => $price->currency->code,
|
||||
'amount' => $price->price,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($price->price < 0) {
|
||||
$this->notify('The upgrade has been completed. We\'ve added the remaining amount to your account balance.', 'success');
|
||||
} else {
|
||||
$this->notify('The upgrade has been completed.', 'success');
|
||||
}
|
||||
|
||||
return $this->redirect(route('services.show', $this->service), true);
|
||||
}
|
||||
|
||||
$invoice = new Invoice([
|
||||
'currency_code' => $this->service->currency_code,
|
||||
'status' => Invoice::STATUS_PENDING,
|
||||
'due_at' => Carbon::now()->addDays(7),
|
||||
'user_id' => $this->service->user_id,
|
||||
]);
|
||||
$invoice->save();
|
||||
|
||||
$upgrade->invoice_id = $invoice->id;
|
||||
$upgrade->save();
|
||||
|
||||
$invoice->items()->create([
|
||||
'description' => 'Upgrade ' . $this->service->product->name . ' to ' . $this->upgradeProduct->name,
|
||||
'price' => $price->price,
|
||||
'quantity' => 1,
|
||||
'reference_id' => $upgrade->id,
|
||||
'reference_type' => ServiceUpgrade::class,
|
||||
]);
|
||||
|
||||
event(new InvoiceCreated($invoice));
|
||||
|
||||
$this->notify('The upgrade has been added to your cart. Please complete the payment to proceed.', 'success');
|
||||
|
||||
return $this->redirect(route('invoices.show', $invoice));
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('services.upgrade')->layoutData([
|
||||
'title' => 'Upgrade Service',
|
||||
'sidebar' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
36
app/Livewire/Services/Widget.php
Normal file
36
app/Livewire/Services/Widget.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Services;
|
||||
|
||||
use App\Livewire\Component;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
class Widget extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
public $status = null;
|
||||
public $limit = null;
|
||||
|
||||
public function render()
|
||||
{
|
||||
$query = Auth::user()->services()->orderByDesc('created_at');
|
||||
|
||||
if ($this->status) {
|
||||
$query->where('status', $this->status);
|
||||
} else {
|
||||
$query->where('status', '!=', 'cancelled');
|
||||
}
|
||||
|
||||
$services = $this->limit
|
||||
? $query->take($this->limit)->get()
|
||||
: $query->paginate(config('settings.pagination'));
|
||||
|
||||
return view('services.widget', [
|
||||
'services' => $services,
|
||||
])->layoutData([
|
||||
'title' => 'Services',
|
||||
]);
|
||||
}
|
||||
}
|
||||
78
app/Livewire/Tickets/Create.php
Normal file
78
app/Livewire/Tickets/Create.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Tickets;
|
||||
|
||||
use App\Attributes\DisabledIf;
|
||||
use App\Livewire\Component;
|
||||
use App\Models\Ticket;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
#[DisabledIf('tickets_disabled')]
|
||||
class Create extends Component
|
||||
{
|
||||
public string $message;
|
||||
|
||||
public string $subject;
|
||||
|
||||
public string $department;
|
||||
|
||||
public ?int $service = null;
|
||||
|
||||
public string $priority;
|
||||
|
||||
public function create()
|
||||
{
|
||||
// Add rules for the department
|
||||
$this->validate([
|
||||
'department' => count((array) config('settings.ticket_departments')) > 0 ? 'required|in:' . implode(',', array_values((array) config('settings.ticket_departments'))) : '',
|
||||
'service' => 'nullable|exists:services,id',
|
||||
'subject' => 'required|string',
|
||||
'message' => 'required|string',
|
||||
'priority' => 'required|in:low,medium,high',
|
||||
]);
|
||||
|
||||
if (RateLimiter::tooManyAttempts('create-ticket', 1)) {
|
||||
$this->notify('Too many ticket creation attempts. Please try again in 60 seconds.', 'error');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
RateLimiter::increment('create-ticket', 30);
|
||||
|
||||
$ticket = Ticket::create([
|
||||
'user_id' => Auth::id(),
|
||||
'department' => $this->department,
|
||||
'service_id' => $this->service,
|
||||
'subject' => $this->subject,
|
||||
'priority' => $this->priority,
|
||||
]);
|
||||
|
||||
$ticket->messages()->create([
|
||||
'user_id' => Auth::id(),
|
||||
'message' => $this->message,
|
||||
]);
|
||||
|
||||
$this->notify('Message sent successfully');
|
||||
|
||||
$this->message = '';
|
||||
$this->dispatch('saved');
|
||||
|
||||
$this->redirect(route('tickets.show', $ticket), true);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
/** @var \App\Models\User */
|
||||
$user = Auth::user();
|
||||
|
||||
return view('tickets.create', [
|
||||
'departments' => (array) config('settings.ticket_departments'),
|
||||
'services' => $user->services()->orderBy('id', 'desc')->get(),
|
||||
])->layoutData([
|
||||
'title' => 'Create Ticket',
|
||||
'sidebar' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
25
app/Livewire/Tickets/Index.php
Normal file
25
app/Livewire/Tickets/Index.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Tickets;
|
||||
|
||||
use App\Attributes\DisabledIf;
|
||||
use App\Livewire\Component;
|
||||
use App\Models\Ticket;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
#[DisabledIf('tickets_disabled')]
|
||||
class Index extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('tickets.index', [
|
||||
'tickets' => Ticket::where('user_id', Auth::id())->latest()->paginate(config('settings.pagination')),
|
||||
])->layoutData([
|
||||
'title' => 'Tickets',
|
||||
'sidebar' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
43
app/Livewire/Tickets/Show.php
Normal file
43
app/Livewire/Tickets/Show.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Tickets;
|
||||
|
||||
use App\Attributes\DisabledIf;
|
||||
use App\Livewire\Component;
|
||||
use App\Models\Ticket;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\Rule;
|
||||
|
||||
#[DisabledIf('tickets_disabled')]
|
||||
class Show extends Component
|
||||
{
|
||||
#[Locked]
|
||||
public Ticket $ticket;
|
||||
|
||||
#[Rule('required', 'string')]
|
||||
public string $message;
|
||||
|
||||
public function save()
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
$this->ticket->messages()->create([
|
||||
'user_id' => Auth::id(),
|
||||
'message' => $this->message,
|
||||
]);
|
||||
|
||||
$this->notify('Message sent successfully');
|
||||
|
||||
$this->message = '';
|
||||
$this->dispatch('saved');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('tickets.show')->layoutData([
|
||||
'sidebar' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
22
app/Livewire/Tickets/Widget.php
Normal file
22
app/Livewire/Tickets/Widget.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Tickets;
|
||||
|
||||
use App\Livewire\Component;
|
||||
use App\Models\Ticket;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
class Widget extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('tickets.widget', [
|
||||
'tickets' => Ticket::where('user_id', Auth::id())->where('status', '!=', 'closed')->latest()->paginate(config('settings.pagination')),
|
||||
])->layoutData([
|
||||
'title' => 'Tickets',
|
||||
]);
|
||||
}
|
||||
}
|
||||
23
app/Livewire/Traits/Disabled.php
Normal file
23
app/Livewire/Traits/Disabled.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Traits;
|
||||
|
||||
trait Disabled
|
||||
{
|
||||
public function bootDisabled()
|
||||
{
|
||||
// Read class-level attributes
|
||||
$reflection = new \ReflectionClass($this);
|
||||
$attributes = $reflection->getAttributes(\App\Attributes\DisabledIf::class);
|
||||
|
||||
// Check if the DisabledIf attribute is present
|
||||
foreach ($attributes as $attribute) {
|
||||
$instance = $attribute->newInstance();
|
||||
// Check the condition based on the attribute's setting
|
||||
if (config('settings.' . $instance->setting, $instance->default)) {
|
||||
// If the condition is met, abort with a 404 error
|
||||
abort(404, 'This feature is currently disabled.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
app/Livewire/Traits/HasNotifications.php
Normal file
27
app/Livewire/Traits/HasNotifications.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Traits;
|
||||
|
||||
use Illuminate\Support\Facades\Session;
|
||||
|
||||
trait HasNotifications
|
||||
{
|
||||
public function bootHasNotifications()
|
||||
{
|
||||
if (Session::has('notification')) {
|
||||
$this->notify(
|
||||
Session::get('notification')['message'],
|
||||
Session::get('notification')['type']
|
||||
);
|
||||
Session::forget('notification');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifications
|
||||
*/
|
||||
public function notify($message, $type = 'success')
|
||||
{
|
||||
$this->dispatch('notify', ['message' => $message, 'type' => $type]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user