Skip to content
34 changes: 3 additions & 31 deletions app/Console/Commands/ChargeServers.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Console\Commands;

use App\Enums\BillingPeriod;
use App\Models\Product;
use App\Models\Server;
use App\Models\User;
Expand Down Expand Up @@ -60,42 +61,13 @@ public function handle()
$product = $server->product;
/** @var User $user */
$user = $server->user;

$billing_period = $product->billing_period;

// check if server is due to be charged by comparing its last_billed date with the current date and the billing period
$newBillingDate = null;
switch ($billing_period) {
case 'annually':
$newBillingDate = Carbon::parse($server->last_billed)->addYear();
break;
case 'half-annually':
$newBillingDate = Carbon::parse($server->last_billed)->addMonths(6);
break;
case 'quarterly':
$newBillingDate = Carbon::parse($server->last_billed)->addMonths(3);
break;
case 'monthly':
$newBillingDate = Carbon::parse($server->last_billed)->addMonth();
break;
case 'weekly':
$newBillingDate = Carbon::parse($server->last_billed)->addWeek();
break;
case 'daily':
$newBillingDate = Carbon::parse($server->last_billed)->addDay();
break;
case 'hourly':
$newBillingDate = Carbon::parse($server->last_billed)->addHour();
default:
$newBillingDate = Carbon::parse($server->last_billed)->addHour();
break;
}

$newBillingDate = $server->getNextBillingDate();

if (!($newBillingDate->isPast())) {
continue;
}


$isCanceled = $server->canceled;
$hasInsufficientCredits = $user->credits < $product->price && $product->price != 0;

Expand Down
65 changes: 65 additions & 0 deletions app/Enums/BillingPeriod.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace App\Enums;

enum BillingPeriod: int
{
case HOURLY = 1;
case DAILY = 2;
case WEEKLY = 3;
case MONTHLY = 4;
case QUARTERLY = 5;
case HALF_ANNUALLY = 6;
case ANNUALLY = 7;

public function label(): string
{
return match($this) {
self::HOURLY => __('Hourly'),
self::DAILY => __('Daily'),
self::WEEKLY => __('Weekly'),
self::MONTHLY => __('Monthly'),
self::QUARTERLY => __('Quarterly'),
self::HALF_ANNUALLY => __('Half Annually'),
self::ANNUALLY => __('Annually'),
};
}

public function description(): string
{
return match($this) {
self::HOURLY => __('Charge the server every hour.'),
self::DAILY => __('Charge the server every day.'),
self::WEEKLY => __('Charge the server every week.'),
self::MONTHLY => __('Charge the server every month.'),
self::QUARTERLY => __('Charge the server every quarter.'),
self::HALF_ANNUALLY => __('Charge the server every half year.'),
self::ANNUALLY => __('Charge the server every year.'),
};
}

public function perPeriod(): string
{
return match($this) {
self::HOURLY => __('per Hour'),
self::DAILY => __('per Day'),
self::WEEKLY => __('per Week'),
self::MONTHLY => __('per Month'),
self::QUARTERLY => __('per 3 Months'),
self::HALF_ANNUALLY => __('per 6 Months'),
self::ANNUALLY => __('per Year'),
};
}

public static function options(): array
{
return collect(self::cases())->mapWithKeys(function ($period) {
return [$period->value => $period->label()];
})->toArray();
}

public static function fromValue(int $value): ?self
{
return self::tryFrom($value);
}
}
3 changes: 1 addition & 2 deletions app/Http/Controllers/Admin/OverViewController.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,7 @@ public function index(GeneralSettings $general_settings, CurrencyHelper $currenc
$nodeId = $server['attributes']['node'];

if ($CPServer = Server::query()->where('pterodactyl_id', $server['attributes']['id'])->first()) {
$product = Product::query()->where('id', $CPServer->product_id)->first();
$price = $product->getMonthlyPrice();
$price = $CPServer->getMonthlyPrice();
if (! $CPServer->suspended) {
$counters['earnings']->active += $price;
$counters['servers']->active++;
Expand Down
43 changes: 37 additions & 6 deletions app/Http/Controllers/Admin/ProductController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Http\Controllers\Admin;

use App\Enums\BillingPeriod;
use App\Enums\BillingPriority;
use App\Helpers\CurrencyHelper;
use App\Http\Controllers\Controller;
Expand Down Expand Up @@ -50,9 +51,12 @@ public function index(LocaleSettings $locale_settings)
public function create(GeneralSettings $general_settings)
{
$this->checkPermission(self::WRITE_PERMISSION);

return view('admin.products.create', [
'locations' => Location::with('nodes')->get(),
'nests' => Nest::with('eggs')->get(),
'billing_periods' => BillingPeriod::options(),
'billing_priorities' => BillingPriority::options(),
'credits_display_name' => $general_settings->credits_display_name
]);
}
Expand All @@ -66,6 +70,8 @@ public function clone(Product $product, GeneralSettings $general_settings)
'credits_display_name' => $general_settings->credits_display_name,
'locations' => Location::with('nodes')->get(),
'nests' => Nest::with('eggs')->get(),
'billing_periods' => BillingPeriod::options(),
'billing_priorities' => BillingPriority::options(),
]);
}

Expand Down Expand Up @@ -95,11 +101,12 @@ public function store(Request $request)
'eggs.*' => 'required|exists:eggs,id',
'disabled' => 'nullable',
'oom_killer' => 'nullable',
'billing_period' => 'required|in:hourly,daily,weekly,monthly,quarterly,half-annually,annually',
'default_billing_priority' => ['required', new Enum(BillingPriority::class)]
'default_billing_priority' => ['required', new Enum(BillingPriority::class)],
'billing_periods' => 'required|array|min:1',
'billing_periods.*.billing_period' => ['required', 'integer', new Enum(BillingPeriod::class)],
'billing_periods.*.price' => 'required|numeric|min:0',
]);


$disabled = ! is_null($request->input('disabled'));
$oomkiller = ! is_null($request->input('oom_killer'));
$product = Product::create(array_merge($request->all(), ['disabled' => $disabled, 'oom_killer' => $oomkiller]));
Expand All @@ -108,6 +115,13 @@ public function store(Request $request)
$product->eggs()->attach($request->input('eggs'));
$product->nodes()->attach($request->input('nodes'));

$product->billingPeriods()->createMany(
collect($request->array('billing_periods', []))->map(fn($period) => [
'billing_period' => $period['billing_period'],
'price' => $period['price']
])->toArray()
);

return redirect()->route('admin.products.index')->with('success', __('Product has been created!'));
}

Expand Down Expand Up @@ -142,6 +156,8 @@ public function edit(Product $product, GeneralSettings $general_settings)
'product' => $product,
'locations' => Location::with('nodes')->get(),
'nests' => Nest::with('eggs')->get(),
'billing_periods' => BillingPeriod::options(),
'billing_priorities' => BillingPriority::options(),
'credits_display_name' => $general_settings->credits_display_name
]);
}
Expand All @@ -153,7 +169,7 @@ public function edit(Product $product, GeneralSettings $general_settings)
* @param Product $product
* @return RedirectResponse
*/
public function update(Request $request, Product $product): RedirectResponse
public function update(Request $request, Product $product)
{
$request->validate([
'name' => 'required|max:30',
Expand All @@ -173,8 +189,10 @@ public function update(Request $request, Product $product): RedirectResponse
'eggs.*' => 'required|exists:eggs,id',
'disabled' => 'nullable',
'oom_killer' => 'nullable',
'billing_period' => 'required|in:hourly,daily,weekly,monthly,quarterly,half-annually,annually',
'default_billing_priority' => ['required', new Enum(BillingPriority::class)]
'default_billing_priority' => ['required', new Enum(BillingPriority::class)],
'billing_periods' => 'required|array|min:1',
'billing_periods.*.billing_period' => ['required', 'integer', new Enum(BillingPeriod::class)],
'billing_periods.*.price' => 'required|numeric|min:0',
]);

$disabled = ! is_null($request->input('disabled'));
Expand All @@ -187,6 +205,19 @@ public function update(Request $request, Product $product): RedirectResponse
$product->eggs()->attach($request->input('eggs'));
$product->nodes()->attach($request->input('nodes'));

$billingPeriods = collect($request->billing_periods)->pluck('billing_period')->toArray();

$product->billingPeriods()
->whereNotIn('billing_period', $billingPeriods)
->delete();

foreach ($request->array('billing_periods', []) as $period) {
$product->billingPeriods()->updateOrCreate(
['product_id' => $product->id, 'billing_period' => $period['billing_period']],
['billing_period' => $period['billing_period'], 'price' => $period['price']]
);
}

return redirect()->route('admin.products.index')->with('success', __('Product has been updated!'));
}

Expand Down
2 changes: 1 addition & 1 deletion app/Http/Controllers/Admin/UserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ public function notify(Request $request)
try {
$user->notify(new DynamicNotification($data['via'], $database, $mail));
$successCount++;
} catch (\Throwable $e)
} catch (\Throwable $e) {
Log::error('Mass notification error for user ' . $user->id . ': ' . $e->getMessage());
}
}
Expand Down
15 changes: 4 additions & 11 deletions app/Http/Controllers/HomeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,12 @@ protected function calculateCreditRunout($user, $credits)
$serverStates = [];
foreach ($servers as $server) {
$product = $server->product;
$period = $product->billing_period;
$period = $server->billing_period;
$price = $product->price;
$lastBilled = $server->last_billed ? Carbon::parse($server->last_billed) : now();
$nextBilling = $lastBilled->copy();
while ($nextBilling->lessThanOrEqualTo(now())) {
switch ($period) {
case 'hourly': $nextBilling->addHour(); break;
case 'daily': $nextBilling->addDay(); break;
case 'weekly': $nextBilling->addWeek(); break;
case 'monthly': $nextBilling->addMonth(); break;
case 'quarterly': $nextBilling->addMonths(3); break;
case 'half-annually': $nextBilling->addMonths(6); break;
case 'annually': $nextBilling->addYear(); break;
}
$nextBilling = $server->getNextBillingDate();
}
$serverStates[] = [
'server' => $server,
Expand All @@ -81,7 +73,7 @@ protected function calculateCreditRunout($user, $credits)
$actions = [];
foreach ($dueServers as $idx => $s) {
$sum += $s['price'];
$actions[] = $s['product']->name . ' (' . $s['period'] . ')';
$actions[] = $s['product']->name . ' (' . $s['period']->label() . ')';
}
if ($currentCredits < $sum) {
$runOutDate = $minDate;
Expand Down Expand Up @@ -176,6 +168,7 @@ public function index(GeneralSettings $general_settings, WebsiteSettings $websit

if ($credits > 0) {
$cacheKey = 'user_credits_left:' . $user->id;

$calculation = Cache::remember($cacheKey, now()->addMinutes(5), function() use ($user, $credits) {
return $this->calculateCreditRunout($user, $credits);
});
Expand Down
1 change: 1 addition & 0 deletions app/Http/Controllers/ProductController.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ public function getProductsBasedOnLocation(Egg $egg, int $location)
->withCount(['servers' => function ($query) use ($user) {
$query->where('user_id', $user->id);
}])
->with('billingPeriods')
->get();

// Check if the product fits in at least one node
Expand Down
16 changes: 12 additions & 4 deletions app/Http/Controllers/ServerController.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
app/Http/Controllers/ServerController.php
<?php

namespace App\Http\Controllers;
Expand All @@ -17,6 +16,7 @@
use App\Settings\ServerSettings;
use App\Settings\PterodactylSettings;
use App\Classes\PterodactylClient;
use App\Enums\BillingPeriod;
use App\Enums\BillingPriority;
use App\Settings\GeneralSettings;
use Exception;
Expand All @@ -25,9 +25,11 @@
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Request as FacadesRequest;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Enum;

class ServerController extends Controller
Expand Down Expand Up @@ -129,6 +131,13 @@ public function store(Request $request): RedirectResponse
'product' => 'required|exists:products,id',
'egg_variables' => 'nullable|string',
'billing_priority' => ['nullable', new Enum(BillingPriority::class)],
'billing_period' => [
'required',
new Enum(BillingPeriod::class),
Rule::exists('product_billing_periods', 'billing_period')->where(function ($query) use ($request) {
$query->where('product_id', $request->input('product'));
})
],
]);

$server = $this->createServer($request);
Expand Down Expand Up @@ -275,6 +284,7 @@ private function createServer(Request $request): ?Server
'product_id' => $product->id,
'last_billed' => Carbon::now(),
'billing_priority' => $request->input('billing_priority', $product->default_billing_priority),
'billing_period' => $request->input('billing_period'),
]);

$allocationId = $this->pterodactyl->getFreeAllocationId($node);
Expand Down Expand Up @@ -309,8 +319,6 @@ private function createServer(Request $request): ?Server

private function handlePostCreation(User $user, Server $server): void
{
logger('Product Price: ' . $server->product->price);

$user->decrement('credits', $server->product->price);

try {
Expand Down Expand Up @@ -388,7 +396,7 @@ public function cancel(Server $server): RedirectResponse
}
}

public function show(Server $server): \Illuminate\View\View
public function show(Server $server): \Illuminate\View\View|RedirectResponse
{
if ($server->user_id !== Auth::id()) {
return back()->with('error', __('This is not your Server!'));
Expand Down
Loading