Add all files
This commit is contained in:
173
app/Classes/Cart.php
Normal file
173
app/Classes/Cart.php
Normal file
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
use App\Models\Coupon;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
|
||||
class Cart
|
||||
{
|
||||
public static function get()
|
||||
{
|
||||
return collect(session('cart', []));
|
||||
}
|
||||
|
||||
public static function add($product, $plan, $configOptions, $checkoutConfig, Price $price, $quantity = 1, $key = null)
|
||||
{
|
||||
if (isset($key)) {
|
||||
$cart = self::get();
|
||||
// Check if key exists
|
||||
$cart[$key] = (object) [
|
||||
'product' => (object) $product,
|
||||
'plan' => (object) $plan,
|
||||
'configOptions' => (object) $configOptions,
|
||||
'checkoutConfig' => (object) $checkoutConfig,
|
||||
'price' => $price,
|
||||
'quantity' => $quantity,
|
||||
];
|
||||
} else {
|
||||
$cart = self::get()->push((object) [
|
||||
'product' => (object) $product,
|
||||
'plan' => (object) $plan,
|
||||
'configOptions' => (object) $configOptions,
|
||||
'checkoutConfig' => (object) $checkoutConfig,
|
||||
'price' => $price,
|
||||
'quantity' => $quantity,
|
||||
]);
|
||||
}
|
||||
|
||||
session(['cart' => $cart]);
|
||||
|
||||
if (Session::has('coupon')) {
|
||||
// Reapply coupon to the cart
|
||||
try {
|
||||
$coupon = Session::get('coupon');
|
||||
self::removeCoupon();
|
||||
self::applyCoupon($coupon->code);
|
||||
} catch (DisplayException $e) {
|
||||
// Ignore exception
|
||||
}
|
||||
}
|
||||
|
||||
// Return index of the newly added item
|
||||
return $key ?? $cart->count() - 1;
|
||||
}
|
||||
|
||||
public static function remove($index)
|
||||
{
|
||||
$cart = self::get();
|
||||
$cart->forget($index);
|
||||
session(['cart' => $cart]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if a coupon is valid for the current user and cart
|
||||
*
|
||||
* @param string $coupon_code
|
||||
* @return \App\Models\Coupon
|
||||
*
|
||||
* @throws \App\Exceptions\DisplayException
|
||||
*/
|
||||
public static function validateCoupon($coupon_code)
|
||||
{
|
||||
$coupon = Coupon::where('code', $coupon_code)->first();
|
||||
|
||||
if (!$coupon) {
|
||||
throw new DisplayException('Coupon code not found');
|
||||
}
|
||||
|
||||
if ($coupon->expires_at && $coupon->expires_at->isPast()) {
|
||||
throw new DisplayException('Coupon code has expired');
|
||||
}
|
||||
if ($coupon->starts_at && $coupon->starts_at->isFuture()) {
|
||||
throw new DisplayException('Coupon code is not active yet');
|
||||
}
|
||||
if ($coupon->max_uses && $coupon->services->count() >= $coupon->max_uses) {
|
||||
throw new DisplayException('Coupon code has reached its maximum uses');
|
||||
}
|
||||
if (Auth::check() && $coupon->hasExceededMaxUsesPerUser(Auth::id())) {
|
||||
throw new DisplayException('You have already used this coupon the maximum number of times allowed');
|
||||
}
|
||||
|
||||
return $coupon;
|
||||
}
|
||||
|
||||
public static function applyCoupon($code)
|
||||
{
|
||||
$coupon = self::validateCoupon($code);
|
||||
|
||||
$wasSuccessful = false;
|
||||
$items = self::get()->map(function ($item) use ($coupon, &$wasSuccessful) {
|
||||
if ($coupon->products->where('id', $item->product->id)->isEmpty() && $coupon->products->isNotEmpty()) {
|
||||
return (object) $item;
|
||||
}
|
||||
$wasSuccessful = true;
|
||||
$discount = 0;
|
||||
if ($coupon->type === 'percentage') {
|
||||
$discount = $item->price->price * $coupon->value / 100;
|
||||
} elseif ($coupon->type === 'fixed') {
|
||||
$discount = $coupon->value;
|
||||
} else {
|
||||
$discount = $item->price->setup_fee;
|
||||
$item->price->setup_fee = 0;
|
||||
}
|
||||
if ($item->price->price < $discount) {
|
||||
$discount = $item->price->price;
|
||||
}
|
||||
$item->price->setDiscount($discount);
|
||||
$item->price->price -= $discount;
|
||||
|
||||
return (object) $item;
|
||||
});
|
||||
|
||||
session(['cart' => $items]);
|
||||
Session::put(['coupon' => $coupon]);
|
||||
|
||||
if ($wasSuccessful) {
|
||||
return $items;
|
||||
} else {
|
||||
throw new DisplayException('Coupon code is not valid for any items in your cart');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and refreshes the coupon in the session
|
||||
*
|
||||
* @return bool True if coupon is valid, false otherwise
|
||||
*/
|
||||
public static function validateAndRefreshCoupon()
|
||||
{
|
||||
if (!Session::has('coupon')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
$coupon = Session::get('coupon');
|
||||
self::validateCoupon($coupon->code);
|
||||
|
||||
return true;
|
||||
} catch (DisplayException $e) {
|
||||
// Coupon is invalid, remove it
|
||||
self::removeCoupon();
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static function removeCoupon()
|
||||
{
|
||||
Session::forget('coupon');
|
||||
$items = self::get()->map(function ($item) {
|
||||
$item->price = new Price([
|
||||
'price' => $item->price->original_price,
|
||||
'setup_fee' => $item->price->original_setup_fee,
|
||||
'currency' => $item->price->currency,
|
||||
], apply_exclusive_tax: true);
|
||||
|
||||
return (object) $item;
|
||||
});
|
||||
session(['cart' => $items]);
|
||||
}
|
||||
}
|
||||
73
app/Classes/Extension/Extension.php
Normal file
73
app/Classes/Extension/Extension.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\Extension;
|
||||
|
||||
use App\Models\Extension as ModelsExtension;
|
||||
use App\Models\Gateway;
|
||||
use App\Models\Server;
|
||||
|
||||
/**
|
||||
* Base class for extensions
|
||||
*
|
||||
* @link https://docs.paymenter.org/development/extensions
|
||||
*/
|
||||
class Extension
|
||||
{
|
||||
public function __construct(public $config = []) {}
|
||||
|
||||
/**
|
||||
* Get a configuration value
|
||||
*
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function config($key)
|
||||
{
|
||||
if (empty($this->config)) {
|
||||
// Check from which type its being called
|
||||
$type = debug_backtrace()[1]['class'];
|
||||
$type = str_replace('Paymenter\Extensions\\', '', $type);
|
||||
$type = str_replace('\\' . class_basename(static::class), '', $type);
|
||||
if (in_array($type, ['Servers', 'Gateways'])) {
|
||||
$type = substr($type, 0, -1);
|
||||
$type = ($type == 'Gateway') ? Gateway::class : Server::class;
|
||||
$this->config = $type::where('extension', class_basename(static::class))->first()->settings->pluck('value', 'key')->toArray();
|
||||
} else {
|
||||
$this->config = ModelsExtension::where('extension', class_basename(static::class))->first()->settings->pluck('value', 'key')->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->config[$key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configuration fields for the extension
|
||||
*
|
||||
* @link https://docs.paymenter.org
|
||||
*
|
||||
* @param array $values The current values of the configuration (is empty on first load)
|
||||
* @return array
|
||||
*/
|
||||
public function getConfig($values = [])
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the meta data for the extension
|
||||
* E.g. name, description, version
|
||||
*
|
||||
* @example return [
|
||||
* 'name' => 'Paymenter',
|
||||
* 'description' => 'Manage and optimize your hosting business with Paymenter',
|
||||
* ]
|
||||
*
|
||||
* @link https://docs.paymenter.org
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getMetadata()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
8
app/Classes/Extension/Gateway.php
Normal file
8
app/Classes/Extension/Gateway.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\Extension;
|
||||
|
||||
/**
|
||||
* Class Gateway
|
||||
*/
|
||||
class Gateway extends Extension {}
|
||||
8
app/Classes/Extension/Server.php
Normal file
8
app/Classes/Extension/Server.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\Extension;
|
||||
|
||||
/**
|
||||
* Class Server
|
||||
*/
|
||||
class Server extends Extension {}
|
||||
284
app/Classes/FilamentInput.php
Normal file
284
app/Classes/FilamentInput.php
Normal file
@@ -0,0 +1,284 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes;
|
||||
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\ColorPicker;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\MarkdownEditor;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Spatie\Color\Factory as ColorFactory;
|
||||
|
||||
class FilamentInput
|
||||
{
|
||||
/**
|
||||
* Convert array or object to Filament input
|
||||
*
|
||||
* @param array|object $setting
|
||||
* @return mixed
|
||||
*/
|
||||
public static function convert($setting)
|
||||
{
|
||||
// If its already a filament component, return it
|
||||
if (is_object($setting) && method_exists($setting, 'getName')) {
|
||||
return $setting;
|
||||
}
|
||||
if (is_array($setting)) {
|
||||
$setting = (object) $setting;
|
||||
}
|
||||
|
||||
switch ($setting->type) {
|
||||
case 'select':
|
||||
return Select::make($setting->name)
|
||||
->label($setting->label ?? $setting->name)
|
||||
->helperText($setting->description ?? '')
|
||||
->options(function () use ($setting) {
|
||||
/* Possiblities:
|
||||
1. ['value1', 'value2', 'value3']
|
||||
2. ['value1' => 'label1', 'value2' => 'label2', 'value3' => 'label3']
|
||||
3. [[
|
||||
'value' => 'value1',
|
||||
'label' => 'label1',
|
||||
], [
|
||||
'value' => 'value2',
|
||||
'label' => 'label2',
|
||||
]]
|
||||
*/
|
||||
if (isset($setting->options)) {
|
||||
if (is_array($setting->options)) {
|
||||
$options = [];
|
||||
// Check if the keys are explicitly set or sequential
|
||||
$keys = array_keys($setting->options);
|
||||
$isSequential = $keys === range(0, count($keys) - 1);
|
||||
|
||||
foreach ($setting->options as $key => $value) {
|
||||
// Explicitly set keys (e.g., ['key1' => 'value1', 'key2' => 'value2'])
|
||||
if (is_array($value)) {
|
||||
$options[$value['value']] = $value['label'];
|
||||
} else {
|
||||
if ($isSequential) {
|
||||
// Sequential keys (e.g., [0 => 'value1', 1 => 'value2'])
|
||||
$options[$value] = $value;
|
||||
} else {
|
||||
$options[$key] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $options;
|
||||
} else {
|
||||
return (array) $setting->options;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
})
|
||||
->preload()
|
||||
->multiple($setting->multiple ?? false)
|
||||
->required($setting->required ?? false)
|
||||
->hint($setting->hint ?? '')
|
||||
->hintColor('primary')
|
||||
->live(condition: $setting->live ?? false)
|
||||
->default($setting->default ?? '')
|
||||
->suffix($setting->suffix ?? null)
|
||||
->prefix($setting->prefix ?? null)
|
||||
->disabled($setting->disabled ?? false)
|
||||
->rules($setting->validation ?? []);
|
||||
break;
|
||||
|
||||
case 'tags':
|
||||
return TagsInput::make($setting->name)
|
||||
->label($setting->label ?? $setting->name)
|
||||
->placeholder($setting->placeholder ?? '')
|
||||
->required($setting->required ?? false)
|
||||
->disabled($setting->disabled ?? false)
|
||||
->hint($setting->hint ?? '')
|
||||
->hintColor('primary')
|
||||
->helperText($setting->description ?? '');
|
||||
break;
|
||||
|
||||
case 'text':
|
||||
return TextInput::make($setting->name)
|
||||
->label($setting->label ?? $setting->name)
|
||||
->helperText($setting->description ?? '')
|
||||
->placeholder($setting->placeholder ?? $setting->default ?? '')
|
||||
->hint($setting->hint ?? '')
|
||||
->hintColor('primary')
|
||||
->required($setting->required ?? false)
|
||||
->live(condition: $setting->live ?? false)
|
||||
->default($setting->default ?? '')
|
||||
->suffix($setting->suffix ?? null)
|
||||
->prefix($setting->prefix ?? null)
|
||||
->disabled($setting->disabled ?? false)
|
||||
->rules($setting->validation ?? []);
|
||||
break;
|
||||
|
||||
case 'textarea':
|
||||
return MarkdownEditor::make($setting->name)
|
||||
->label($setting->label ?? $setting->name)
|
||||
->helperText($setting->description ?? '')
|
||||
->placeholder($setting->placeholder ?? $setting->default ?? '')
|
||||
->hint($setting->hint ?? '')
|
||||
->hintColor('primary')
|
||||
->required($setting->required ?? false)
|
||||
->live(condition: $setting->live ?? false)
|
||||
->default($setting->default ?? '')
|
||||
->disableAllToolbarButtons($setting->disable_toolbar ?? false)
|
||||
->rules($setting->validation ?? [])
|
||||
->disabled($setting->disabled ?? false);
|
||||
break;
|
||||
case 'markdown':
|
||||
return MarkdownEditor::make($setting->name)
|
||||
->label($setting->label ?? $setting->name)
|
||||
->helperText($setting->description ?? '')
|
||||
->placeholder($setting->placeholder ?? $setting->default ?? '')
|
||||
->hint($setting->hint ?? '')
|
||||
->hintColor('primary')
|
||||
->required($setting->required ?? false)
|
||||
->live(condition: $setting->live ?? false)
|
||||
->default($setting->default ?? '')
|
||||
->disableAllToolbarButtons($setting->disable_toolbar ?? false)
|
||||
->rules($setting->validation ?? [])
|
||||
->disabled($setting->disabled ?? false);
|
||||
break;
|
||||
case 'password':
|
||||
return TextInput::make($setting->name)
|
||||
->label($setting->label ?? $setting->name)
|
||||
->helperText($setting->description ?? '')
|
||||
->placeholder($setting->placeholder ?? $setting->default ?? '')
|
||||
->hint($setting->hint ?? '')
|
||||
->hintColor('primary')
|
||||
->required($setting->required ?? false)
|
||||
->password()
|
||||
->revealable()
|
||||
->live(condition: $setting->live ?? false)
|
||||
->default($setting->default ?? '')
|
||||
->suffix($setting->suffix ?? null)
|
||||
->prefix($setting->prefix ?? null)
|
||||
->disabled($setting->disabled ?? false)
|
||||
->rules($setting->validation ?? []);
|
||||
break;
|
||||
case 'email':
|
||||
return TextInput::make($setting->name)
|
||||
->label($setting->label ?? $setting->name)
|
||||
->helperText($setting->description ?? '')
|
||||
->placeholder($setting->placeholder ?? $setting->default ?? '')
|
||||
->hint($setting->hint ?? '')
|
||||
->hintColor('primary')
|
||||
->required($setting->required ?? false)
|
||||
->email()
|
||||
->live(condition: $setting->live ?? false)
|
||||
->default($setting->default ?? '')
|
||||
->suffix($setting->suffix ?? null)
|
||||
->prefix($setting->prefix ?? null)
|
||||
->disabled($setting->disabled ?? false)
|
||||
->rules($setting->validation ?? []);
|
||||
break;
|
||||
case 'number':
|
||||
return TextInput::make($setting->name)
|
||||
->label($setting->label ?? $setting->name)
|
||||
->helperText($setting->description ?? '')
|
||||
->placeholder($setting->placeholder ?? $setting->default ?? '')
|
||||
->hint($setting->hint ?? '')
|
||||
->hintColor('primary')
|
||||
->required($setting->required ?? false)
|
||||
->numeric()
|
||||
->minValue($setting->min_value ?? null)
|
||||
->maxValue($setting->max_value ?? null)
|
||||
->live(condition: $setting->live ?? false)
|
||||
->default($setting->default ?? '')
|
||||
->suffix($setting->suffix ?? null)
|
||||
->prefix($setting->prefix ?? null)
|
||||
->disabled($setting->disabled ?? false)
|
||||
->rules($setting->validation ?? []);
|
||||
|
||||
break;
|
||||
case 'color':
|
||||
$mode = $setting->color_mode ?? 'hsl';
|
||||
$color = ColorPicker::make($setting->name)
|
||||
->label($setting->label ?? $setting->name)
|
||||
->helperText($setting->description ?? '')
|
||||
->placeholder($setting->placeholder ?? $setting->default ?? '')
|
||||
->hint($setting->hint ?? '')
|
||||
->hintColor('primary')
|
||||
->required($setting->required ?? false)
|
||||
->live(condition: $setting->live ?? true)
|
||||
->default($setting->default ?? '')
|
||||
->suffix($setting->suffix ?? null)
|
||||
->prefix($setting->prefix ?? null)
|
||||
->disabled($setting->disabled ?? false)
|
||||
->rules($setting->validation ?? [])
|
||||
->rules(function () {
|
||||
return function ($attribute, $value, $fail) {
|
||||
try {
|
||||
ColorFactory::fromString(trim($value));
|
||||
} catch (\Exception $e) {
|
||||
$fail('The :attribute must be a valid color.');
|
||||
}
|
||||
};
|
||||
})
|
||||
->afterStateUpdated(function ($state, callable $set) use ($setting, $mode) {
|
||||
try {
|
||||
$set($setting->name, preg_replace('/,\s*/', ', ', ColorFactory::fromString(trim($state))->{'to' . ucfirst($mode)}()->__toString()));
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
});
|
||||
$color->$mode();
|
||||
|
||||
return $color;
|
||||
break;
|
||||
case 'file':
|
||||
$input = FileUpload::make($setting->name)
|
||||
->label($setting->label ?? $setting->name)
|
||||
->helperText($setting->description ?? '')
|
||||
->hint($setting->hint ?? '')
|
||||
->hintColor('primary')
|
||||
->required($setting->required ?? false)
|
||||
->acceptedFileTypes($setting->accept ?? [])
|
||||
->live(condition: $setting->live ?? false)
|
||||
->default($setting->default ?? '')
|
||||
->disk($setting->disk ?? 'public')
|
||||
->preserveFilenames($setting->preserve_filenames ?? true)
|
||||
->disabled($setting->disabled ?? false)
|
||||
->downloadable()
|
||||
->rules($setting->validation ?? []);
|
||||
|
||||
if (isset($setting->file_name)) {
|
||||
$input->getUploadedFileNameForStorageUsing(
|
||||
fn (): string => (string) $setting->file_name,
|
||||
);
|
||||
}
|
||||
|
||||
return $input;
|
||||
|
||||
break;
|
||||
|
||||
case 'checkbox':
|
||||
return Checkbox::make($setting->name)
|
||||
->label($setting->label ?? $setting->name)
|
||||
->helperText($setting->description ?? '')
|
||||
->required($setting->required ?? false)
|
||||
->hint($setting->hint ?? '')
|
||||
->hintColor('primary')
|
||||
->live(condition: $setting->live ?? false)
|
||||
->default($setting->default ?? '')
|
||||
->disabled($setting->disabled ?? false)
|
||||
->rules($setting->validation ?? []);
|
||||
break;
|
||||
|
||||
case 'placeholder':
|
||||
return Placeholder::make($setting->name)
|
||||
->content($setting->label ?? null)
|
||||
->helperText($setting->description ?? '')
|
||||
->hint($setting->hint ?? '')
|
||||
->hintColor('primary');
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \Exception("Unknown input type: {$setting->type}");
|
||||
}
|
||||
}
|
||||
}
|
||||
261
app/Classes/Navigation.php
Normal file
261
app/Classes/Navigation.php
Normal file
@@ -0,0 +1,261 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes;
|
||||
|
||||
use App\Helpers\EventHelper;
|
||||
use App\Models\Category;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class Navigation
|
||||
{
|
||||
public static function getLinks()
|
||||
{
|
||||
return once(function () {
|
||||
|
||||
$categories = Category::whereNull('parent_id')->where(function ($query) {
|
||||
$query->whereHas('children')->orWhereHas('products', function ($query) {
|
||||
$query->where('hidden', false);
|
||||
});
|
||||
})->get();
|
||||
|
||||
$routes = [
|
||||
[
|
||||
'name' => __('navigation.home'),
|
||||
'route' => 'home',
|
||||
'icon' => 'ri-home-2',
|
||||
],
|
||||
[
|
||||
'name' => __('navigation.shop'),
|
||||
'children' => $categories->map(function ($category) {
|
||||
return [
|
||||
'name' => $category->name,
|
||||
'route' => 'category.show',
|
||||
'params' => ['category' => $category->slug],
|
||||
];
|
||||
})->toArray(),
|
||||
'condition' => count($categories) > 0,
|
||||
'separator' => true,
|
||||
'icon' => 'ri-shopping-bag',
|
||||
],
|
||||
];
|
||||
|
||||
$routes = EventHelper::itemEvent('navigation', $routes);
|
||||
|
||||
$routes = array_filter($routes, function ($route) {
|
||||
return isset($route['condition']) ? $route['condition'] : true;
|
||||
});
|
||||
|
||||
return Navigation::markActiveRoute($routes);
|
||||
});
|
||||
}
|
||||
|
||||
// Get navigation items for user dropdown menu
|
||||
public static function getAccountDropdownLinks()
|
||||
{
|
||||
return once(function () {
|
||||
|
||||
$routes = [
|
||||
[
|
||||
'name' => __('navigation.dashboard'),
|
||||
'route' => 'dashboard',
|
||||
],
|
||||
[
|
||||
'name' => __('navigation.tickets'),
|
||||
'route' => 'tickets',
|
||||
'condition' => !config('settings.tickets_disabled', false),
|
||||
],
|
||||
[
|
||||
'name' => __('navigation.account'),
|
||||
'route' => 'account',
|
||||
],
|
||||
[
|
||||
'name' => __('navigation.admin'),
|
||||
'route' => 'filament.admin.pages.dashboard',
|
||||
'spa' => false,
|
||||
'condition' => Auth::user()->role_id !== null,
|
||||
],
|
||||
];
|
||||
|
||||
$routes = EventHelper::itemEvent('navigation.account-dropdown', $routes);
|
||||
|
||||
$routes = array_filter($routes, function ($route) {
|
||||
return isset($route['condition']) ? $route['condition'] : true;
|
||||
});
|
||||
|
||||
return Navigation::markActiveRoute($routes);
|
||||
});
|
||||
}
|
||||
|
||||
public static function getDashboardLinks()
|
||||
{
|
||||
return once(function () {
|
||||
|
||||
$routes = [
|
||||
[
|
||||
'name' => __('navigation.dashboard'),
|
||||
'route' => 'dashboard',
|
||||
'icon' => 'ri-function',
|
||||
'condition' => Auth::check(),
|
||||
'priority' => 10,
|
||||
],
|
||||
[
|
||||
'name' => __('navigation.services'),
|
||||
'route' => 'services',
|
||||
'icon' => 'ri-archive-stack',
|
||||
'condition' => Auth::check(),
|
||||
'priority' => 20,
|
||||
],
|
||||
[
|
||||
'name' => __('navigation.invoices'),
|
||||
'route' => 'invoices',
|
||||
'icon' => 'ri-receipt',
|
||||
'separator' => true,
|
||||
'condition' => Auth::check(),
|
||||
'priority' => 30,
|
||||
],
|
||||
[
|
||||
'name' => __('navigation.tickets'),
|
||||
'route' => 'tickets',
|
||||
'icon' => 'ri-customer-service',
|
||||
'separator' => true,
|
||||
'condition' => Auth::check() && !config('settings.tickets_disabled', false),
|
||||
'priority' => 40,
|
||||
],
|
||||
[
|
||||
'name' => __('navigation.account'),
|
||||
'icon' => 'ri-settings-3',
|
||||
'condition' => Auth::check(),
|
||||
'priority' => 50,
|
||||
'children' => EventHelper::itemEvent(
|
||||
'navigation.account',
|
||||
[
|
||||
[
|
||||
'name' => __('navigation.personal_details'),
|
||||
'route' => 'account',
|
||||
'params' => [],
|
||||
'priority' => 10,
|
||||
],
|
||||
[
|
||||
'name' => __('navigation.security'),
|
||||
'route' => 'account.security',
|
||||
'params' => [],
|
||||
'priority' => 20,
|
||||
],
|
||||
[
|
||||
'name' => __('account.credits'),
|
||||
'route' => 'account.credits',
|
||||
'params' => [],
|
||||
'condition' => config('settings.credits_enabled'),
|
||||
'priority' => 30,
|
||||
],
|
||||
]
|
||||
),
|
||||
],
|
||||
];
|
||||
|
||||
$routes = EventHelper::itemEvent('navigation.dashboard', $routes);
|
||||
|
||||
$routes = array_filter($routes, function ($route) {
|
||||
return isset($route['condition']) ? $route['condition'] : true;
|
||||
});
|
||||
|
||||
return Navigation::markActiveRoute($routes);
|
||||
});
|
||||
}
|
||||
|
||||
public static function getActiveRoute()
|
||||
{
|
||||
$route = request()->route()->getName();
|
||||
$routes = [
|
||||
...self::getLinks(),
|
||||
...self::getAccountDropdownLinks(),
|
||||
...self::getDashboardLinks(),
|
||||
];
|
||||
// Get current parnet of the route
|
||||
$parent = null;
|
||||
foreach ($routes as $item) {
|
||||
if ($item['route'] == $route) {
|
||||
$parent = $item;
|
||||
break;
|
||||
}
|
||||
if (isset($item['children'])) {
|
||||
foreach ($item['children'] as $child) {
|
||||
if ($child['route'] == $route) {
|
||||
$parent = $item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set `active` to true if the route is currently active,
|
||||
* or falce if route isn't active (prevents `Undefined array key "active"` errors)
|
||||
*
|
||||
* @return array routes
|
||||
*/
|
||||
public static function markActiveRoute(array $routes): array
|
||||
{
|
||||
$currentRoute = request()->route()->getName();
|
||||
|
||||
foreach ($routes as &$route) {
|
||||
$route['active'] = self::isActiveRoute($route, $currentRoute);
|
||||
|
||||
if (isset($route['icon'])) {
|
||||
$route['icon'] .= $route['active'] ? '-fill' : '-line';
|
||||
}
|
||||
|
||||
if (isset($route['children'])) {
|
||||
foreach ($route['children'] as &$child) {
|
||||
$child['active'] = self::isActiveRoute($child, $currentRoute);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $routes;
|
||||
}
|
||||
|
||||
private static function isActiveRoute(array $route, string $currentRoute): bool
|
||||
{
|
||||
if (($route['route'] ?? '') === $currentRoute) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!empty($route['children'])) {
|
||||
foreach ($route['children'] as $child) {
|
||||
if (($child['route'] ?? '') === $currentRoute) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function getCurrent()
|
||||
{
|
||||
$route = request()->route()->getName();
|
||||
$routes = self::getLinks();
|
||||
// Get current parnet of the route
|
||||
$parent = null;
|
||||
foreach ($routes as $item) {
|
||||
if ($item['route'] == $route) {
|
||||
$parent = $item;
|
||||
break;
|
||||
}
|
||||
if (isset($item['children'])) {
|
||||
foreach ($item['children'] as $child) {
|
||||
if ($child['route'] == $route) {
|
||||
$parent = $item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $parent;
|
||||
}
|
||||
}
|
||||
16
app/Classes/PDF.php
Normal file
16
app/Classes/PDF.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes;
|
||||
|
||||
use App\Models\Invoice;
|
||||
use Barryvdh\DomPDF\Facade\Pdf as DomPDF;
|
||||
|
||||
class PDF
|
||||
{
|
||||
public static function generateInvoice(Invoice $invoice)
|
||||
{
|
||||
$pdf = DomPDF::loadView('pdf.invoice', ['invoice' => $invoice]);
|
||||
|
||||
return $pdf;
|
||||
}
|
||||
}
|
||||
147
app/Classes/Price.php
Normal file
147
app/Classes/Price.php
Normal file
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes;
|
||||
|
||||
/**
|
||||
* Class Price
|
||||
*/
|
||||
class Price
|
||||
{
|
||||
public $price;
|
||||
|
||||
public $currency;
|
||||
|
||||
public $setup_fee;
|
||||
|
||||
public $has_setup_fee;
|
||||
|
||||
public $is_free;
|
||||
|
||||
public $dontShowUnavailablePrice;
|
||||
|
||||
public object $formatted;
|
||||
|
||||
public $tax = 0;
|
||||
|
||||
public $setup_fee_tax = 0;
|
||||
|
||||
public $discount = 0;
|
||||
|
||||
public $original_price;
|
||||
|
||||
public $original_setup_fee;
|
||||
|
||||
public function setDiscount($discount)
|
||||
{
|
||||
$this->discount = $discount;
|
||||
}
|
||||
|
||||
public function __construct($priceAndCurrency = null, $free = false, $dontShowUnavailablePrice = false, $apply_exclusive_tax = false)
|
||||
{
|
||||
if (is_array($priceAndCurrency)) {
|
||||
$priceAndCurrency = (object) $priceAndCurrency;
|
||||
}
|
||||
if ($free) {
|
||||
$this->price = 0;
|
||||
$this->currency = $priceAndCurrency->currency ?? null;
|
||||
$this->is_free = true;
|
||||
|
||||
$this->formatted = (object) [
|
||||
'price' => $this->format($this->price),
|
||||
'setup_fee' => $this->format($this->setup_fee),
|
||||
'tax' => $this->format($this->tax),
|
||||
'setup_fee_tax' => $this->format($this->setup_fee_tax),
|
||||
];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->price = $priceAndCurrency->price->price ?? $priceAndCurrency->price ?? null;
|
||||
$this->currency = $priceAndCurrency->currency;
|
||||
if (is_array($this->currency)) {
|
||||
$this->currency = (object) $this->currency;
|
||||
}
|
||||
$this->setup_fee = $priceAndCurrency->price->setup_fee ?? $priceAndCurrency->setup_fee ?? null;
|
||||
// We save the original so we can revert back to it when removing a coupon
|
||||
$this->original_price = $this->price;
|
||||
$this->original_setup_fee = $this->setup_fee;
|
||||
|
||||
// Calculate taxes
|
||||
if (config('settings.tax_enabled')) {
|
||||
$tax = Settings::tax();
|
||||
if ($tax) {
|
||||
// Inclusive has the tax included in the price
|
||||
if (config('settings.tax_type') == 'inclusive' || !$apply_exclusive_tax) {
|
||||
$this->tax = number_format($this->price - ($this->price / (1 + $tax->rate / 100)), 2, '.', '');
|
||||
if ($this->setup_fee) {
|
||||
$this->setup_fee_tax = number_format($this->setup_fee - ($this->setup_fee / (1 + $tax->rate / 100)), 2, '.', '');
|
||||
}
|
||||
} else {
|
||||
// Exclusive has the tax added to the price as an extra
|
||||
$this->tax = number_format($this->price * $tax->rate / 100, 2, '.', '');
|
||||
$this->original_price = $this->price + $this->tax;
|
||||
$this->price = number_format($this->price + $this->tax, 2, '.', '');
|
||||
if ($this->setup_fee) {
|
||||
$this->setup_fee_tax = number_format($this->setup_fee * $tax->rate / 100, 2, '.', '');
|
||||
$this->original_setup_fee = $this->setup_fee + $this->setup_fee_tax;
|
||||
$this->setup_fee = number_format($this->setup_fee + $this->setup_fee_tax, 2, '.', '');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->has_setup_fee = isset($this->setup_fee) ? $this->setup_fee > 0 : false;
|
||||
$this->dontShowUnavailablePrice = $dontShowUnavailablePrice;
|
||||
$this->formatted = (object) [
|
||||
'price' => $this->format($this->price),
|
||||
'setup_fee' => $this->format($this->setup_fee),
|
||||
'tax' => $this->format($this->tax),
|
||||
'setup_fee_tax' => $this->format($this->setup_fee_tax),
|
||||
];
|
||||
}
|
||||
|
||||
public function format($price)
|
||||
{
|
||||
if ($this->is_free) {
|
||||
return 'Free';
|
||||
}
|
||||
if (!$this->currency) {
|
||||
if ($this->dontShowUnavailablePrice) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return 'Not available in your currency';
|
||||
}
|
||||
// Get the format
|
||||
$format = $this->currency->format;
|
||||
switch ($format) {
|
||||
case '1.000,00':
|
||||
$price = number_format($price, 2, ',', '.');
|
||||
break;
|
||||
case '1,000.00':
|
||||
$price = number_format($price, 2, '.', ',');
|
||||
break;
|
||||
case '1 000,00':
|
||||
$price = number_format($price, 2, ',', ' ');
|
||||
break;
|
||||
case '1 000.00':
|
||||
$price = number_format($price, 2, '.', ' ');
|
||||
break;
|
||||
}
|
||||
|
||||
return $this->currency->prefix . $price . $this->currency->suffix;
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return $this->formatted->price;
|
||||
}
|
||||
|
||||
public function __get($name)
|
||||
{
|
||||
if ($name == 'available') {
|
||||
return $this->currency || $this->is_free ? true : false;
|
||||
} else {
|
||||
return $this->$name;
|
||||
}
|
||||
}
|
||||
}
|
||||
611
app/Classes/Settings.php
Normal file
611
app/Classes/Settings.php
Normal file
@@ -0,0 +1,611 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes;
|
||||
|
||||
use App\Models\Setting;
|
||||
use App\Models\TaxRate;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
class Settings
|
||||
{
|
||||
public static function settings()
|
||||
{
|
||||
try {
|
||||
// Only code is needed
|
||||
$currencies = once(function () {
|
||||
return \App\Models\Currency::pluck('code')->toArray();
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
$currencies = [];
|
||||
}
|
||||
$settings = [
|
||||
// Split settings into groups (only used in the settings page for organization)
|
||||
'general' => [
|
||||
[
|
||||
'name' => 'timezone',
|
||||
'label' => 'Timezone',
|
||||
'type' => 'select',
|
||||
// Read timezones from PHP
|
||||
'options' => \DateTimeZone::listIdentifiers(\DateTimeZone::ALL),
|
||||
'default' => 'UTC',
|
||||
'required' => true,
|
||||
'override' => 'app.timezone',
|
||||
],
|
||||
[
|
||||
'name' => 'app_language',
|
||||
'label' => 'Default Language',
|
||||
'default' => 'en',
|
||||
'type' => 'select',
|
||||
// Read languages from resources/lang directory
|
||||
// The ternary operator is only present for now. Since there are no lang files, it returns [], which breaks the frontend, so we return ['en']
|
||||
'options' => glob(base_path('lang/*'), GLOB_ONLYDIR) ? array_map('basename', glob(base_path('lang/*'), GLOB_ONLYDIR)) : ['en'],
|
||||
'required' => true,
|
||||
'validation' => 'in:' . implode(',', glob(base_path('lang/*'), GLOB_ONLYDIR) ? array_map('basename', glob(base_path('lang/*'), GLOB_ONLYDIR)) : ['en']),
|
||||
'override' => 'app.locale',
|
||||
],
|
||||
[
|
||||
'name' => 'app_url',
|
||||
'label' => 'App URL',
|
||||
'default' => 'http://localhost',
|
||||
'type' => 'text',
|
||||
'required' => true,
|
||||
'validation' => 'url',
|
||||
'override' => 'app.url',
|
||||
],
|
||||
[
|
||||
'name' => 'logo',
|
||||
'label' => 'Logo',
|
||||
'type' => 'file',
|
||||
'required' => false,
|
||||
'accept' => ['image/*'],
|
||||
'file_name' => 'logo.webp',
|
||||
],
|
||||
[
|
||||
'name' => 'tos',
|
||||
'label' => 'Terms of Service',
|
||||
'description' => 'URL to your terms of service. Leave blank to disable.',
|
||||
'type' => 'text',
|
||||
'required' => false,
|
||||
],
|
||||
],
|
||||
|
||||
// Security (captcha, rate limiting, etc.)
|
||||
'security' => [
|
||||
[
|
||||
'name' => 'captcha',
|
||||
'label' => 'Captcha',
|
||||
'type' => 'select',
|
||||
'options' => [
|
||||
'disabled' => 'Disabled',
|
||||
'recaptcha-v2' => 'Google reCAPTCHA v2',
|
||||
'recaptcha-v3' => 'Google reCAPTCHA v3',
|
||||
'turnstile' => 'Cloudflare Turnstile',
|
||||
'hcaptcha' => 'hCaptcha',
|
||||
],
|
||||
'default' => 'disabled',
|
||||
],
|
||||
[
|
||||
'name' => 'captcha_site_key',
|
||||
'label' => 'Captcha Site Key',
|
||||
'type' => 'text',
|
||||
'required' => false,
|
||||
],
|
||||
[
|
||||
'name' => 'captcha_secret',
|
||||
'label' => 'Captcha Secret',
|
||||
'type' => 'text',
|
||||
'required' => false,
|
||||
],
|
||||
|
||||
[
|
||||
'name' => 'trusted_proxies',
|
||||
'label' => 'Trusted Proxies',
|
||||
'type' => 'tags',
|
||||
'database_type' => 'array',
|
||||
'placeholder' => 'IP Addresses or CIDR (e.g. 1.1.1.1/32 or 2606:4700:4700::1111)',
|
||||
],
|
||||
],
|
||||
|
||||
'social-login' => [
|
||||
[
|
||||
'name' => 'oauth_google',
|
||||
'label' => 'Google Enabled',
|
||||
'description' => new HtmlString('<a href="https://paymenter.org/docs/guides/OAuth#google" target="_blank">Documentation</a>'),
|
||||
'type' => 'checkbox',
|
||||
'database_type' => 'boolean',
|
||||
'default' => false,
|
||||
'required' => false,
|
||||
],
|
||||
[
|
||||
'name' => 'oauth_google_client_id',
|
||||
'label' => 'Google Client ID',
|
||||
'type' => 'text',
|
||||
'required' => false,
|
||||
],
|
||||
[
|
||||
'name' => 'oauth_google_client_secret',
|
||||
'label' => 'Google Client Secret',
|
||||
'type' => 'text',
|
||||
'required' => false,
|
||||
],
|
||||
[
|
||||
'name' => 'oauth_github',
|
||||
'label' => 'GitHub Enabled',
|
||||
'description' => new HtmlString('<a href="https://paymenter.org/docs/guides/OAuth#github" target="_blank">Documentation</a>'),
|
||||
'type' => 'checkbox',
|
||||
'database_type' => 'boolean',
|
||||
'default' => false,
|
||||
'required' => false,
|
||||
],
|
||||
[
|
||||
'name' => 'oauth_github_client_id',
|
||||
'label' => 'Github Client ID',
|
||||
'type' => 'text',
|
||||
'required' => false,
|
||||
],
|
||||
[
|
||||
'name' => 'oauth_github_client_secret',
|
||||
'label' => 'Github Client Secret',
|
||||
'type' => 'text',
|
||||
'required' => false,
|
||||
],
|
||||
[
|
||||
'name' => 'oauth_discord',
|
||||
'label' => 'Discord Enabled',
|
||||
'description' => new HtmlString('<a href="https://paymenter.org/docs/guides/OAuth#discord" target="_blank">Documentation</a>'),
|
||||
'type' => 'checkbox',
|
||||
'database_type' => 'boolean',
|
||||
'default' => false,
|
||||
'required' => false,
|
||||
],
|
||||
[
|
||||
'name' => 'oauth_discord_client_id',
|
||||
'label' => 'Discord Client ID',
|
||||
'type' => 'text',
|
||||
'required' => false,
|
||||
],
|
||||
[
|
||||
'name' => 'oauth_discord_client_secret',
|
||||
'label' => 'Discord Client Secret',
|
||||
'type' => 'text',
|
||||
'required' => false,
|
||||
],
|
||||
],
|
||||
|
||||
'company-details' => [
|
||||
[
|
||||
'name' => 'company_name',
|
||||
'label' => 'Company Name',
|
||||
'type' => 'text',
|
||||
'override' => 'app.name',
|
||||
'default' => 'Paymenter',
|
||||
],
|
||||
[
|
||||
'name' => 'company_email',
|
||||
'label' => 'Company Email',
|
||||
'type' => 'email',
|
||||
],
|
||||
[
|
||||
'name' => 'company_phone',
|
||||
'label' => 'Company Phone',
|
||||
'type' => 'text',
|
||||
],
|
||||
[
|
||||
'name' => 'company_address',
|
||||
'label' => 'Company Address',
|
||||
'type' => 'text',
|
||||
],
|
||||
[
|
||||
'name' => 'company_address2',
|
||||
'label' => 'Company Address 2',
|
||||
'type' => 'text',
|
||||
],
|
||||
[
|
||||
'name' => 'company_city',
|
||||
'label' => 'Company City',
|
||||
'type' => 'text',
|
||||
],
|
||||
[
|
||||
'name' => 'company_state',
|
||||
'label' => 'Company State',
|
||||
'type' => 'text',
|
||||
],
|
||||
[
|
||||
'name' => 'company_zip',
|
||||
'label' => 'Company Zip',
|
||||
'type' => 'text',
|
||||
],
|
||||
[
|
||||
'name' => 'company_country',
|
||||
'label' => 'Company Country',
|
||||
'type' => 'select',
|
||||
'options' => array_merge(['' => 'None'], config('app.countries')),
|
||||
],
|
||||
[
|
||||
'name' => 'company_tax_id',
|
||||
'label' => 'Company Tax ID',
|
||||
'type' => 'text',
|
||||
],
|
||||
[
|
||||
'name' => 'company_id',
|
||||
'label' => 'Company ID',
|
||||
'type' => 'text',
|
||||
],
|
||||
],
|
||||
'tax' => [
|
||||
[
|
||||
'name' => 'tax_enabled',
|
||||
'label' => 'Tax Enabled',
|
||||
'type' => 'checkbox',
|
||||
'database_type' => 'boolean',
|
||||
'default' => false,
|
||||
],
|
||||
[
|
||||
'name' => 'tax_type',
|
||||
'label' => 'Tax Type',
|
||||
'type' => 'select',
|
||||
'options' => [
|
||||
'inclusive' => 'Inclusive (Price includes tax)',
|
||||
'exclusive' => 'Exclusive (Price does not include tax)',
|
||||
],
|
||||
'default' => 'inclusive',
|
||||
],
|
||||
],
|
||||
'mail' => [
|
||||
// SMTP etc
|
||||
[
|
||||
'name' => 'mail_disable',
|
||||
'label' => 'Disable Mail',
|
||||
'type' => 'checkbox',
|
||||
'database_type' => 'boolean',
|
||||
'default' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'mail_must_verify',
|
||||
'label' => 'Users must verify email before buying',
|
||||
'type' => 'checkbox',
|
||||
'database_type' => 'boolean',
|
||||
'default' => false,
|
||||
],
|
||||
[
|
||||
'name' => 'mail_host',
|
||||
'label' => 'Mail Host',
|
||||
'type' => 'text',
|
||||
'required' => false,
|
||||
'override' => 'mail.mailers.smtp.host',
|
||||
],
|
||||
[
|
||||
'name' => 'mail_port',
|
||||
'label' => 'Mail Port',
|
||||
'type' => 'text',
|
||||
'required' => false,
|
||||
'override' => 'mail.mailers.smtp.port',
|
||||
],
|
||||
[
|
||||
'name' => 'mail_username',
|
||||
'label' => 'Mail Username',
|
||||
'type' => 'text',
|
||||
'required' => false,
|
||||
'override' => 'mail.mailers.smtp.username',
|
||||
],
|
||||
[
|
||||
'name' => 'mail_password',
|
||||
'label' => 'Mail Password',
|
||||
'type' => 'password',
|
||||
'required' => false,
|
||||
'encrypted' => true,
|
||||
'override' => 'mail.mailers.smtp.password',
|
||||
],
|
||||
[
|
||||
'name' => 'mail_encryption',
|
||||
'label' => 'Mail Encryption',
|
||||
'type' => 'select',
|
||||
'options' => [
|
||||
'tls' => 'TLS',
|
||||
'ssl' => 'SSL',
|
||||
null => 'None',
|
||||
],
|
||||
'default' => 'tls',
|
||||
'required' => false,
|
||||
'override' => 'mail.mailers.smtp.encryption',
|
||||
],
|
||||
[
|
||||
'name' => 'mail_from_address',
|
||||
'label' => 'Mail From Address',
|
||||
'type' => 'email',
|
||||
'required' => false,
|
||||
'override' => 'mail.from.address',
|
||||
],
|
||||
[
|
||||
'name' => 'mail_from_name',
|
||||
'label' => 'Mail From Name',
|
||||
'type' => 'text',
|
||||
'required' => false,
|
||||
'override' => 'mail.from.name',
|
||||
],
|
||||
|
||||
// Theming
|
||||
[
|
||||
'name' => 'mail_header',
|
||||
'label' => 'Header',
|
||||
'type' => 'markdown',
|
||||
'required' => false,
|
||||
'default' => '',
|
||||
'disable_toolbar' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'mail_footer',
|
||||
'label' => 'Footer',
|
||||
'type' => 'markdown',
|
||||
'required' => false,
|
||||
'default' => '',
|
||||
'disable_toolbar' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'mail_css',
|
||||
'label' => 'Mail CSS',
|
||||
'type' => 'markdown',
|
||||
'required' => false,
|
||||
'default' => '',
|
||||
'disable_toolbar' => true,
|
||||
],
|
||||
],
|
||||
'cronjob' => [
|
||||
[
|
||||
'name' => 'cronjob_invoice',
|
||||
'label' => 'Send invoice if due date is x days away',
|
||||
'type' => 'number',
|
||||
'default' => 7,
|
||||
'required' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'cronjob_invoice_reminder',
|
||||
'label' => 'Send invoice reminder if due date is x days away',
|
||||
'type' => 'number',
|
||||
'default' => 3,
|
||||
'required' => true,
|
||||
],
|
||||
[
|
||||
// Cancel order is pending for x days
|
||||
'name' => 'cronjob_order_cancel',
|
||||
'label' => 'Cancel order if pending for x days',
|
||||
'type' => 'number',
|
||||
'default' => 7,
|
||||
'required' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'cronjob_order_suspend',
|
||||
'label' => 'Suspend server if invoice is x days overdue',
|
||||
'type' => 'number',
|
||||
'default' => 2,
|
||||
'required' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'cronjob_order_terminate',
|
||||
'label' => 'Delete server if invoice is x days overdue (also cancels the invoice)',
|
||||
'type' => 'number',
|
||||
'default' => 14,
|
||||
'required' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'cronjob_delete_email_logs',
|
||||
'label' => 'Delete email logs older than x days',
|
||||
'type' => 'number',
|
||||
'default' => 90,
|
||||
'required' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'cronjob_close_ticket',
|
||||
'label' => 'Close tickets if no response for x days',
|
||||
'type' => 'number',
|
||||
'default' => 7,
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
'credits' => [
|
||||
[
|
||||
'name' => 'credits_enabled',
|
||||
'label' => 'Credits Enabled',
|
||||
'type' => 'checkbox',
|
||||
'database_type' => 'boolean',
|
||||
'default' => false,
|
||||
],
|
||||
[
|
||||
'name' => 'credits_minimum_deposit',
|
||||
'label' => 'Minimum Deposit',
|
||||
'type' => 'number',
|
||||
'default' => 5,
|
||||
'required' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'credits_maximum_deposit',
|
||||
'label' => 'Maximum Deposit',
|
||||
'type' => 'number',
|
||||
'default' => 100,
|
||||
'required' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'credits_maximum_credit',
|
||||
'label' => 'Maximum Credit',
|
||||
'type' => 'number',
|
||||
'default' => 300,
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
'theme' => [
|
||||
[
|
||||
'name' => 'theme',
|
||||
'label' => 'Theme',
|
||||
'default' => 'default',
|
||||
'type' => 'select',
|
||||
'required' => true,
|
||||
// Read themes from themes directory
|
||||
'options' => array_map('basename', glob(base_path('themes/*'), GLOB_ONLYDIR)),
|
||||
'validation' => 'in:' . implode(',', array_map('basename', glob(base_path('themes/*'), GLOB_ONLYDIR))),
|
||||
],
|
||||
],
|
||||
'invoices' => [
|
||||
[
|
||||
'name' => 'invoice_number',
|
||||
'label' => 'Invoice Number',
|
||||
'type' => 'number',
|
||||
'default' => 1,
|
||||
'required' => false,
|
||||
'description' => 'The next invoice number to use. This will be incremented automatically.',
|
||||
],
|
||||
[
|
||||
'name' => 'invoice_number_padding',
|
||||
'label' => 'Invoice Number Padding',
|
||||
'type' => 'number',
|
||||
'default' => 1,
|
||||
'required' => false,
|
||||
'description' => 'Number of digits to use for invoice numbers. Example: 0001, 0002, etc.',
|
||||
],
|
||||
[
|
||||
'name' => 'invoice_number_format',
|
||||
'label' => 'Invoice number format',
|
||||
'type' => 'text',
|
||||
'default' => '{number}',
|
||||
'required' => false,
|
||||
'description' => 'Format to use for invoice numbers. Use {number} to insert the zero padded number and use {year}, {month} and {day} placeholders to insert the current date. Example: INV-{year}-{month}-{day}-{number} or INV-{year}{number}. It must at least contain {number}.',
|
||||
'validation' => 'regex:/{number}/',
|
||||
],
|
||||
],
|
||||
'other' => [
|
||||
[
|
||||
'name' => 'gravatar_default',
|
||||
'label' => 'Gravatar Default',
|
||||
'description' => 'Default image to use when a user does not have a Gravatar. ',
|
||||
'link' => 'https://docs.gravatar.com/general/images/#default-image',
|
||||
'type' => 'select',
|
||||
'options' => [
|
||||
'mp' => 'Mystery Person',
|
||||
'identicon' => 'Identicon',
|
||||
'monsterid' => 'Monster',
|
||||
'wavatar' => 'Wavatar',
|
||||
'retro' => 'Retro',
|
||||
'robohash' => 'Robohash',
|
||||
'blank' => 'Blank',
|
||||
],
|
||||
'default' => 'wavatar',
|
||||
],
|
||||
[
|
||||
'name' => 'default_currency',
|
||||
'label' => 'Default Currency',
|
||||
'type' => 'select',
|
||||
'options' => $currencies,
|
||||
'default' => 'USD',
|
||||
'required' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'ticket_departments',
|
||||
'label' => 'Ticket Departments',
|
||||
'type' => 'tags',
|
||||
'default' => ['Support', 'Sales'],
|
||||
'required' => true,
|
||||
'database_type' => 'array',
|
||||
],
|
||||
[
|
||||
'name' => 'registration_disabled',
|
||||
'label' => 'Disable User Registration',
|
||||
'type' => 'checkbox',
|
||||
'database_type' => 'boolean',
|
||||
'default' => false,
|
||||
'description' => 'Only allow existing users to log in. This will hide the registration page and prevent new users from signing up.',
|
||||
],
|
||||
[
|
||||
'name' => 'tickets_disabled',
|
||||
'label' => 'Disable Tickets',
|
||||
'type' => 'checkbox',
|
||||
'database_type' => 'boolean',
|
||||
'default' => false,
|
||||
'description' => 'Disable the ticket system. This will disable all client side ticket functionality, including the ability to create new tickets and view existing tickets.',
|
||||
],
|
||||
[
|
||||
'name' => 'pagination',
|
||||
'label' => 'Pagination',
|
||||
'type' => 'number',
|
||||
'default' => 10,
|
||||
'required' => true,
|
||||
'description' => 'Number of items to show per page',
|
||||
],
|
||||
[
|
||||
'name' => 'debug',
|
||||
'label' => 'Debug Mode',
|
||||
'type' => 'checkbox',
|
||||
'database_type' => 'boolean',
|
||||
'default' => false,
|
||||
'description' => 'Enable debug mode to log HTTP requests and errors',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// Set theme settings
|
||||
$settings['theme'] = [...$settings['theme'], ...\App\Classes\Theme::getSettings()];
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
public static function tax()
|
||||
{
|
||||
// Use once so the query is only run once
|
||||
return once(function () {
|
||||
$country = Auth::user()?->properties()->where('key', 'country')->value('value') ?? null;
|
||||
|
||||
// Change country to a two-letter country code if it's not already
|
||||
if ($country) {
|
||||
$country = array_search($country, config('app.countries')) ?: $country;
|
||||
}
|
||||
|
||||
if ($taxRate = TaxRate::where('country', $country)->first()) {
|
||||
return $taxRate;
|
||||
} elseif ($taxRate = TaxRate::where('country', 'all')->first()) {
|
||||
return $taxRate;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
public static function settingsObject()
|
||||
{
|
||||
return (object) json_decode(json_encode(static::settings()));
|
||||
}
|
||||
|
||||
public static function getSetting($key)
|
||||
{
|
||||
$setting = (object) collect(static::settings())->flatten(1)->firstWhere('name', $key);
|
||||
$setting->value = Setting::where('settingable_type', null)->where('key', $key)->value('value') ?? $setting->default ?? null;
|
||||
|
||||
return $setting;
|
||||
}
|
||||
|
||||
public static function getTelemetry()
|
||||
{
|
||||
try {
|
||||
$uuid = Setting::where('key', 'telemetry_uuid')->value('value');
|
||||
} catch (\Exception $e) {
|
||||
$uuid = null;
|
||||
}
|
||||
if (is_null($uuid)) {
|
||||
$uuid = Uuid::uuid4()->toString();
|
||||
try {
|
||||
Setting::updateOrCreate(
|
||||
['key' => 'telemetry_uuid'],
|
||||
['value' => $uuid]
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
// Avoid errors in workflows
|
||||
}
|
||||
}
|
||||
|
||||
// Daily fixed time based on UUID
|
||||
$time = hexdec(str_replace('-', '', substr($uuid, 27))) % 1440;
|
||||
$hour = floor($time / 60);
|
||||
$minute = $time % 60;
|
||||
|
||||
return compact('uuid', 'hour', 'minute');
|
||||
}
|
||||
}
|
||||
49
app/Classes/Synths/PriceSynth.php
Normal file
49
app/Classes/Synths/PriceSynth.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\Synths;
|
||||
|
||||
use App\Classes\Price;
|
||||
use Livewire\Mechanisms\HandleComponents\Synthesizers\Synth;
|
||||
|
||||
/**
|
||||
* Class PriceSynth
|
||||
*/
|
||||
class PriceSynth extends Synth
|
||||
{
|
||||
public static $key = 'price';
|
||||
|
||||
public static function match($target)
|
||||
{
|
||||
return $target instanceof Price;
|
||||
}
|
||||
|
||||
public static function dehydrate($instance)
|
||||
{
|
||||
return [[
|
||||
'price' => $instance->price,
|
||||
'currency' => $instance->currency,
|
||||
'setup_fee' => $instance->setup_fee,
|
||||
'has_setup_fee' => $instance->has_setup_fee,
|
||||
'is_free' => $instance->is_free,
|
||||
'dontShowUnavailablePrice' => $instance->dontShowUnavailablePrice,
|
||||
'tax' => $instance->tax,
|
||||
'setup_fee_tax' => $instance->setup_fee_tax,
|
||||
'discount' => $instance->discount,
|
||||
'original_price' => $instance->original_price,
|
||||
'original_setup_fee' => $instance->original_setup_fee,
|
||||
'formatted' => $instance->formatted,
|
||||
], []];
|
||||
}
|
||||
|
||||
public static function hydrate($instance)
|
||||
{
|
||||
$price = new Price(['price' => $instance['price'], 'setup_fee' => $instance['setup_fee'], 'currency' => $instance['currency']], $instance['is_free'], $instance['dontShowUnavailablePrice']);
|
||||
$price->tax = $instance['tax'];
|
||||
$price->setup_fee_tax = $instance['setup_fee_tax'];
|
||||
$price->discount = $instance['discount'];
|
||||
$price->original_price = $instance['original_price'];
|
||||
$price->original_setup_fee = $instance['original_setup_fee'];
|
||||
|
||||
return $price;
|
||||
}
|
||||
}
|
||||
29
app/Classes/Theme.php
Normal file
29
app/Classes/Theme.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes;
|
||||
|
||||
class Theme
|
||||
{
|
||||
public static function getSettings()
|
||||
{
|
||||
try {
|
||||
$theme = require base_path('themes/' . config('settings.theme', 'default') . '/theme.php');
|
||||
} catch (\Throwable $th) {
|
||||
// If not ran from the command line, throw an exception
|
||||
if (php_sapi_name() !== 'cli') {
|
||||
throw new \Exception('Theme file could not be read. ' . $th);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Add theme settings prefix to name <theme_name>_<setting_name>
|
||||
$settings = [];
|
||||
foreach ($theme['settings'] as $setting) {
|
||||
$setting['name'] = 'theme_' . config('settings.theme', 'default') . '_' . $setting['name'];
|
||||
$settings[] = $setting;
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
}
|
||||
33
app/Classes/helpers.php
Normal file
33
app/Classes/helpers.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
if (!function_exists('theme')) {
|
||||
/**
|
||||
* Get the specified configuration value.
|
||||
*
|
||||
* If an array is passed as the key, we will assume you want to set an array of values.
|
||||
*
|
||||
* @param array|string $key
|
||||
* @param mixed $default
|
||||
* @return mixed|\Illuminate\Config\Repository
|
||||
*/
|
||||
function theme($key, $default = null)
|
||||
{
|
||||
$current_theme = config('settings.theme', 'default');
|
||||
|
||||
return config("settings.theme_$current_theme" . "_$key", $default) ?? $default;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('hook')) {
|
||||
/**
|
||||
* Dispatch an event and return the items
|
||||
*
|
||||
* @param string $event
|
||||
* @param array $items
|
||||
* @return array
|
||||
*/
|
||||
function hook($event)
|
||||
{
|
||||
return \App\Helpers\EventHelper::renderEvent($event);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user