Add all files

This commit is contained in:
root
2025-11-30 11:57:22 +01:00
parent 2ee51cac4f
commit 044271db3d
1476 changed files with 142544 additions and 0 deletions

173
app/Classes/Cart.php Normal file
View 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]);
}
}

View 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 [];
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Classes\Extension;
/**
* Class Gateway
*/
class Gateway extends Extension {}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Classes\Extension;
/**
* Class Server
*/
class Server extends Extension {}

View 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
View 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
View 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
View 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
View 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');
}
}

View 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
View 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
View 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);
}
}