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('Documentation'), '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('Documentation'), '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('Documentation'), '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'); } }