Merge pull request #24 from ploi/rjs/provider-plans-per-package

RJS/Assign provider plans to specific packages
This commit is contained in:
Dennis Smink
2023-10-02 11:14:26 +02:00
committed by GitHub
10 changed files with 335 additions and 96 deletions

View File

@@ -2,14 +2,17 @@
namespace App\Filament\Resources;
use Filament\Forms;
use Filament\Tables;
use App\Models\Package;
use Filament\Forms\Form;
use Filament\Tables\Table;
use Filament\Resources\Resource;
use App\Filament\Resources\PackageResource\Pages;
use App\Filament\Resources\PackageResource\RelationManagers;
use App\Models\Package;
use App\Models\Provider;
use App\Models\ProviderPlan;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Support\HtmlString;
class PackageResource extends Resource
@@ -66,6 +69,7 @@ class PackageResource extends Resource
Forms\Components\Grid::make()
->schema([
Forms\Components\Section::make(__('Server permissions'))
->icon(ServerResource::getNavigationIcon())
->schema([
Forms\Components\Checkbox::make('server_permissions.create')
->reactive()
@@ -80,6 +84,7 @@ class PackageResource extends Resource
])
->columnSpan(1),
Forms\Components\Section::make(__('Site permissions'))
->icon(SiteResource::getNavigationIcon())
->schema([
Forms\Components\Checkbox::make('site_permissions.create')
->label('Allow site creation')
@@ -98,9 +103,95 @@ class PackageResource extends Resource
->schema([
Forms\Components\Section::make(__('Available server providers'))
->description(__('These server providers will be available for users that are attached to this package.'))
->icon(ProviderResource::getNavigationIcon())
->schema([
Forms\Components\CheckboxList::make('providers')
->relationship('providers', 'name')
->reactive(),
Forms\Components\Grid::make(1)
->schema([
Forms\Components\Actions::make([
Forms\Components\Actions\Action::make('manage_provider_plans')
->label(__('Manage provider plans'))
->icon('heroicon-o-adjustments-horizontal')
->form(function (Package $record) {
return $record->providers->sortBy('name')->map(function (Provider $provider) {
return Forms\Components\Section::make($provider->label)
->description(__('Select the plans that should be available for this provider on this package.'))
->icon(ProviderResource::getNavigationIcon())
->statePath($provider->id)
->schema([
Forms\Components\Toggle::make('select_specific_provider_plans')
->label(__('Select subset'))
->helperText(__('Check this box if you want to limit the provider plans available on this package.'))
->default(false)
->reactive()
->afterStateUpdated(function (Forms\Components\Toggle $component, Forms\Set $set) use ($provider) {
$set(
path: "provider_plans",
state: $component->getState() ? $provider->plans->pluck('id') : [],
);
}),
Forms\Components\CheckboxList::make("provider_plans")
->label(__('Select plans'))
->options(fn() => $provider->plans->mapWithKeys(fn(ProviderPlan $providerPlan) => [$providerPlan->id => $providerPlan->label ?? $providerPlan->plan_id])->all())
->visible(fn(Forms\Get $get) => $get('select_specific_provider_plans'))
->reactive()
->bulkToggleable()
->columns(2)
])
->collapsible();
})->all();
})
->fillForm(function (Package $record) {
return $record->providers->mapWithKeys(function (Provider $provider) use ($record) {
$providerPlanIds = $record->providerPlans()->whereBelongsTo($provider)->pluck('provider_plans.id');
return [$provider->id => [
'select_specific_provider_plans' => $providerPlanIds->isNotEmpty(),
'provider_plans' => $providerPlanIds->all(),
]];
})->all();
})
->action(function (Package $record, array $data) {
$providerPlanIds = collect($data)
// If `select_specific_provider_plans`, all provider plans are available. It could be that this
// option was deselected, and that we have some left over provider plans in the field that
// is now hidden. We will not include theSE IDs so that they ARE detached automatically.
->where('select_specific_provider_plans', true)
->pluck('provider_plans')
->flatten();
// Detaches provider plans not specifically selected.
$record->providerPlans()->sync($providerPlanIds);
Notification::make()
->title(__('Provider plans saved'))
->success()
->send();
})
->modalSubmitActionLabel(__('Save'))
->color('gray')
->disabled(function (Package $record, Forms\Get $get) {
$providers = collect($get('providers'))
->map(fn(string $id): int => (int)$id)
->sort();
return $record->providers->pluck('id')->map(fn(string $id): int => (int)$id)->sort()->toArray() !== $providers->all();
})
]),
Forms\Components\Placeholder::make('save_warning')
->content(__('You\'ve changed the available server providers. Please save your changes before you can manage the provider plans.'))
->visible(function (Package $record, Forms\Get $get) {
$providers = collect($get('providers'))
->map(fn(string $id): int => (int)$id)
->sort();
return $record->providers->pluck('id')->map(fn(string $id): int => (int)$id)->sort()->toArray() !== $providers->all();
})
->hiddenLabel(),
])
->hiddenOn('create'),
])
->columnSpan(1)
])
@@ -127,10 +218,10 @@ class PackageResource extends Resource
return "Attached to stripe - {$record->price_monthly} {$record->currency}";
}),
Tables\Columns\TextColumn::make('maximum_sites')
->formatStateUsing(fn (int $state) => $state === 0 ? __('Unlimited') : $state)
->formatStateUsing(fn(int $state) => $state === 0 ? __('Unlimited') : $state)
->label(__('Maximum sites')),
Tables\Columns\TextColumn::make('maximum_servers')
->formatStateUsing(fn (int $state) => $state === 0 ? __('Unlimited') : $state)
->formatStateUsing(fn(int $state) => $state === 0 ? __('Unlimited') : $state)
->label(__('Maximum servers')),
Tables\Columns\TextColumn::make('users_count')
->counts('users'),

View File

@@ -16,4 +16,11 @@ class EditPackage extends EditRecord
Actions\DeleteAction::make(),
];
}
public function afterSave(): void
{
// Necessary to refresh, in order to load the updated saved relationships and
// correctly show or hide the "manage provider plans" warning placeholder.
$this->getRecord()->refresh();
}
}

View File

@@ -28,10 +28,10 @@ class UsersRelationManager extends RelationManager
...$table->getHeaderActions(),
Action::make('add_user')
->label(__('Add user'))
->form(fn(self $livewire) => [
->form(fn (self $livewire) => [
Select::make('user_id')
->label('User')
->options(User::orderBy('name')->get()->mapWithKeys(fn(User $user) => [$user->id => $user->name]))
->options(User::orderBy('name')->get()->mapWithKeys(fn (User $user) => [$user->id => $user->name]))
->preload()
->searchable()
->required(),

View File

@@ -3,12 +3,14 @@
namespace App\Http\Controllers;
use App\Models\Server;
use App\Models\ProviderPlan;
use Illuminate\Http\Request;
use App\Jobs\Servers\DeleteServer;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\RedirectResponse;
use App\Http\Resources\ServerResource;
use App\DataTransferObjects\ServerData;
use Illuminate\Database\Eloquent\Builder;
use App\Actions\Server\CreateServerAction;
use App\Http\Requests\ServerUpdateRequest;
@@ -101,19 +103,29 @@ class ServerController extends Controller
public function plansAndRegions(Request $request, $providerId)
{
$provider = $request->user()->package->providers()->findOrFail($providerId);
$package = $request->user()->package;
$regions = $provider->regions()
$provider = $package->providers()->findOrFail($providerId);
$regions = $provider
->regions()
->when($provider->allowed_regions, function ($query) use ($provider) {
return $query->whereIn('id', $provider->allowed_regions);
})
->pluck('label', 'id');
$plans = $provider->plans()
$plans = $provider
->plans()
->when($provider->allowed_plans, function ($query) use ($provider) {
return $query->whereIn('id', $provider->allowed_plans);
})
->pluck('label', 'id');
->when($package->providerPlans()->whereBelongsTo($provider)->exists(), function (Builder $query) use ($provider, $package) {
return $query->whereIn('id', $package->providerPlans()->whereBelongsTo($provider)->pluck('provider_plans.id'));
})
->get()
->mapWithKeys(function (ProviderPlan $providerPlan) {
return [$providerPlan->id => $providerPlan->label ?? $providerPlan->plan_id];
});
return [
'regions' => $regions,

View File

@@ -11,12 +11,12 @@ class HasAccessToThisGroup
public function handle(Request $request, Closure $next, $group)
{
if ($group === 'servers') {
$package = $request->user()->package ?? [];
$package = $request->user()->package ?? null;
if (
!Arr::get($package->server_permissions, 'create', false) &&
!Arr::get($package->server_permissions, 'update', false) &&
!Arr::get($package->server_permissions, 'delete', false)
!Arr::get($package->server_permissions ?? [], 'create', false) &&
!Arr::get($package->server_permissions ?? [], 'update', false) &&
!Arr::get($package->server_permissions ?? [], 'delete', false)
) {
abort(404);
}

View File

@@ -4,7 +4,9 @@ namespace App\Models;
use App\Casts\PermissionCast;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Package extends Model
{
@@ -39,19 +41,24 @@ class Package extends Model
'server_permissions' => PermissionCast::class,
];
public function users()
public function users(): HasMany
{
return $this->hasMany(User::class);
}
public function providers()
public function providers(): BelongsToMany
{
return $this->belongsToMany(Provider::class);
return $this->belongsToMany(Provider::class)->using(PackageProvider::class);
}
protected static function booted()
public function providerPlans(): BelongsToMany
{
static::deleting(function ($package) {
return $this->belongsToMany(ProviderPlan::class);
}
protected static function booted(): void
{
static::deleting(function (self $package) {
$package->users()->update(['package_id' => null]);
});
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class PackageProvider extends Pivot
{
protected static function booted(): void
{
static::deleting(function (self $packageProvider) {
$packageProvider->package->providerPlans()->whereBelongsTo($packageProvider->provider)->detach();
});
}
public function package(): BelongsTo
{
return $this->belongsTo(Package::class);
}
public function provider(): BelongsTo
{
return $this->belongsTo(Provider::class);
}
}

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('package_provider_plan', function (Blueprint $table) {
$table->id();
$table->foreignId('package_id')->constrained();
$table->foreignId('provider_plan_id')->constrained();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('package_provider_plan');
}
};

View File

@@ -1,22 +1,43 @@
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\PageController;
use App\Http\Controllers\SiteController;
use App\Http\Controllers\SearchController;
use App\Http\Controllers\ServerController;
use App\Http\Controllers\SiteAppController;
use App\Http\Controllers\SiteDnsController;
use App\Http\Controllers\SupportController;
use App\Http\Controllers\DashboardController;
use App\Http\Controllers\SiteAliasController;
use App\Http\Controllers\SiteCronjobController;
use App\Http\Controllers\SiteSettingController;
use App\Http\Controllers\SiteDatabaseController;
use App\Http\Controllers\SiteRedirectController;
use App\Http\Controllers\DocumentationController;
use App\Http\Controllers\ServerSettingController;
use App\Http\Controllers\Profile\ProfileController;
use App\Http\Controllers\SiteCertificateController;
use App\Http\Controllers\Auth\CreatePasswordController;
use App\Http\Controllers\Profile\ProfileBillingController;
use App\Http\Controllers\Profile\ProfileSettingController;
use App\Http\Controllers\Profile\ProfileSecurityController;
use App\Http\Controllers\Auth\AuthenticateTwoFactorController;
use App\Http\Controllers\Profile\ProfileIntegrationController;
use App\Http\Controllers\Profile\TwoFactorAuthentication\RegenerateRecoveryCodesController;
use App\Http\Controllers\Profile\TwoFactorAuthentication\TwoFactorAuthenticationController;
use App\Http\Controllers\Profile\TwoFactorAuthentication\ConfirmTwoFactorAuthenticationController;
Auth::routes();
Route::get('password-creation', 'Auth\CreatePasswordController@index')->name('password-creation')
Route::get('password-creation', [CreatePasswordController::class, 'index'])->name('password-creation')
->middleware(['signed', 'guest']);
Route::post('password-creation', 'Auth\CreatePasswordController@start')->name('password-creation.start')
Route::post('password-creation', [CreatePasswordController::class, 'start'])->name('password-creation.start')
->middleware('guest');
Route::get('installation-incomplete', 'PageController@installationIncomplete')->name('installation-incomplete');
Route::get('installation-incomplete', [PageController::class, 'installationIncomplete'])->name('installation-incomplete');
Route::get('page/{slug}', 'PageController@show')->name('page.show');
Route::get('page/{slug}', [PageController::class, 'show'])->name('page.show');
// All auth protected routes
Route::group(['middleware' => ['auth', 'auth.blocked']], function () {
@@ -24,92 +45,92 @@ Route::group(['middleware' => ['auth', 'auth.blocked']], function () {
Route::post('confirm-2fa', [AuthenticateTwoFactorController::class, 'confirm'])->name('auth.confirm-2fa.confirm');
Route::middleware('auth.2fa')->group(function () {
Route::get('/', 'DashboardController@index')->name('dashboard');
Route::get('search', 'SearchController@handle')->name('search');
Route::get('/', [DashboardController::class, 'index'])->name('dashboard');
Route::get('search', [SearchController::class, 'handle'])->name('search');
// Site routes
Route::group(['prefix' => 'sites', 'as' => 'sites.'], function () {
Route::get('/', 'SiteController@index')->name('index');
Route::get('{site}', 'SiteController@show')->name('show');
Route::delete('{site}', 'SiteController@destroy')->name('delete');
Route::post('/', 'SiteController@store')->name('store');
Route::post('{site}/request-ftp-password', 'SiteController@requestFtpPassword')->name('request-ftp-password');
Route::get('/', [SiteController::class, 'index'])->name('index');
Route::get('{site}', [SiteController::class, 'show'])->name('show');
Route::delete('{site}', [SiteController::class, 'destroy'])->name('delete');
Route::post('/', [SiteController::class, 'store'])->name('store');
Route::post('{site}/request-ftp-password', [SiteController::class, 'requestFtpPassword'])->name('request-ftp-password');
Route::get('{site}/settings', 'SiteSettingController@show')->name('settings.show');
Route::patch('{site}/settings', 'SiteSettingController@update')->name('settings.update');
Route::patch('{site}/settings/php-version', 'SiteSettingController@changePhpVersion')->name('settings.php-version');
Route::get('{site}/settings', [SiteSettingController::class, 'show'])->name('settings.show');
Route::patch('{site}/settings', [SiteSettingController::class, 'update'])->name('settings.update');
Route::patch('{site}/settings/php-version', [SiteSettingController::class, 'changePhpVersion'])->name('settings.php-version');
// Site apps
Route::group(['prefix' => '{site}/apps', 'as' => 'apps.'], function () {
Route::get('/', 'SiteAppController@index')->name('index');
Route::post('/', 'SiteAppController@store')->name('store');
Route::delete('/', 'SiteAppController@destroy')->name('delete');
Route::get('/', [SiteAppController::class, 'index'])->name('index');
Route::post('/', [SiteAppController::class, 'store'])->name('store');
Route::delete('/', [SiteAppController::class, 'destroy'])->name('delete');
});
// Site databases
Route::group(['prefix' => '{site}/databases', 'as' => 'databases.'], function () {
Route::get('/', 'SiteDatabaseController@index')->name('index');
Route::post('/', 'SiteDatabaseController@store')->name('store');
Route::delete('{database}', 'SiteDatabaseController@destroy')->name('delete');
Route::get('/', [SiteDatabaseController::class, 'index'])->name('index');
Route::post('/', [SiteDatabaseController::class, 'store'])->name('store');
Route::delete('{database}', [SiteDatabaseController::class, 'destroy'])->name('delete');
});
// Site cronjobs
Route::group(['prefix' => '{site}/cronjobs', 'as' => 'cronjobs.'], function () {
Route::get('/', 'SiteCronjobController@index')->name('index');
Route::post('/', 'SiteCronjobController@store')->name('store');
Route::delete('{cronjob}', 'SiteCronjobController@destroy')->name('delete');
Route::get('/', [SiteCronjobController::class, 'index'])->name('index');
Route::post('/', [SiteCronjobController::class, 'store'])->name('store');
Route::delete('{cronjob}', [SiteCronjobController::class, 'destroy'])->name('delete');
});
// Site redirects
Route::group(['prefix' => '{site}/redirects', 'as' => 'redirects.'], function () {
Route::get('/', 'SiteRedirectController@index')->name('index');
Route::post('/', 'SiteRedirectController@store')->name('store');
Route::delete('{redirect}', 'SiteRedirectController@destroy')->name('delete');
Route::get('/', [SiteRedirectController::class, 'index'])->name('index');
Route::post('/', [SiteRedirectController::class, 'store'])->name('store');
Route::delete('{redirect}', [SiteRedirectController::class, 'destroy'])->name('delete');
});
// Site SSL
Route::group(['prefix' => '{site}/certificates', 'as' => 'certificates.'], function () {
Route::get('/', 'SiteCertificateController@index')->name('index');
Route::post('/', 'SiteCertificateController@store')->name('store');
Route::delete('{certificate}', 'SiteCertificateController@destroy')->name('delete');
Route::get('/', [SiteCertificateController::class, 'index'])->name('index');
Route::post('/', [SiteCertificateController::class, 'store'])->name('store');
Route::delete('{certificate}', [SiteCertificateController::class, 'destroy'])->name('delete');
});
// Site aliases
Route::group(['prefix' => '{site}/aliases', 'as' => 'aliases.'], function () {
Route::get('/', 'SiteAliasController@index')->name('index');
Route::post('/', 'SiteAliasController@store')->name('store');
Route::delete('{alias}', 'SiteAliasController@destroy')->name('delete');
Route::get('/', [SiteAliasController::class, 'index'])->name('index');
Route::post('/', [SiteAliasController::class, 'store'])->name('store');
Route::delete('{alias}', [SiteAliasController::class, 'destroy'])->name('delete');
});
// Site DNS
Route::group(['prefix' => '{site}/dns', 'as' => 'dns.'], function () {
Route::get('/', 'SiteDnsController@index')->name('index');
Route::get('records', 'SiteDnsController@records')->name('records');
Route::post('/', 'SiteDnsController@store')->name('store');
Route::delete('{record}', 'SiteDnsController@destroy')->name('delete');
Route::put('{record}', 'SiteDnsController@update')->name('update');
Route::get('/', [SiteDnsController::class, 'index'])->name('index');
Route::get('records', [SiteDnsController::class, 'records'])->name('records');
Route::post('/', [SiteDnsController::class, 'store'])->name('store');
Route::delete('{record}', [SiteDnsController::class, 'destroy'])->name('delete');
Route::put('{record}', [SiteDnsController::class, 'update'])->name('update');
});
});
// Server routes
Route::group(['prefix' => 'servers', 'as' => 'servers.', 'middleware' => 'has.access:servers'], function () {
Route::get('/', 'ServerController@index')->name('index');
Route::get('{provider}/plans-and-regions', 'ServerController@plansAndRegions')->name('plans-and-regions');
Route::get('{server}', 'ServerController@show')->name('show');
Route::get('{server}/settings', 'ServerController@show')->name('settings.show');
Route::patch('{server}/settings', 'ServerController@update')->name('settings.update');
Route::post('/', 'ServerController@store')->name('store');
Route::get('/', [ServerController::class, 'index'])->name('index');
Route::get('{provider}/plans-and-regions', [ServerController::class, 'plansAndRegions'])->name('plans-and-regions');
Route::get('{server}', [ServerController::class, 'show'])->name('show');
Route::get('{server}/settings', [ServerController::class, 'show'])->name('settings.show');
Route::patch('{server}/settings', [ServerController::class, 'update'])->name('settings.update');
Route::post('/', [ServerController::class, 'store'])->name('store');
Route::get('{server}/settings', 'ServerSettingController@show')->name('settings.show');
Route::get('{server}/settings', [ServerSettingController::class, 'show'])->name('settings.show');
Route::delete('{server}', 'ServerController@destroy')->name('delete');
Route::delete('{server}', [ServerController::class, 'destroy'])->name('delete');
});
// Profile routes
Route::group(['prefix' => 'profile', 'as' => 'profile.', 'namespace' => 'Profile'], function () {
Route::get('/', 'ProfileController@index')->name('index');
Route::patch('/', 'ProfileController@update')->name('update');
Route::delete('destroy', 'ProfileController@destroy')->name('delete-account');
Route::get('/', [ProfileController::class, 'index'])->name('index');
Route::patch('/', [ProfileController::class, 'update'])->name('update');
Route::delete('destroy', [ProfileController::class, 'destroy'])->name('delete-account');
// Security
Route::group(['prefix' => 'security', 'as' => 'security.'], function () {
@@ -126,53 +147,53 @@ Route::group(['middleware' => ['auth', 'auth.blocked']], function () {
// Settings
Route::group(['prefix' => 'settings', 'as' => 'settings.'], function () {
Route::get('/', 'ProfileSettingController@index')->name('index');
Route::patch('/', 'ProfileSettingController@update')->name('update');
Route::get('/', [ProfileSettingController::class, 'index'])->name('index');
Route::patch('/', [ProfileSettingController::class, 'update'])->name('update');
});
// Integrations
Route::group(['prefix' => 'integrations', 'as' => 'integrations.'], function () {
Route::get('/', 'ProfileIntegrationController@index')->name('index');
Route::post('/', 'ProfileIntegrationController@store')->name('store');
Route::delete('{provider}', 'ProfileIntegrationController@destroy')->name('destroy');
Route::get('/', [ProfileIntegrationController::class, 'index'])->name('index');
Route::post('/', [ProfileIntegrationController::class, 'store'])->name('store');
Route::delete('{provider}', [ProfileIntegrationController::class, 'destroy'])->name('destroy');
});
if (config('cashier.key') && config('cashier.secret')) {
Route::group(['prefix' => 'billing', 'as' => 'billing.'], function () {
Route::get('/', 'ProfileBillingController@index')->name('index');
Route::get('/', [ProfileBillingController::class, 'index'])->name('index');
Route::post('card/update', 'ProfileBillingController@updateCard')->name('update.card');
Route::delete('card', 'ProfileBillingController@deleteCard')->name('delete.card');
Route::post('plan/update', 'ProfileBillingController@updatePlan')->name('update.plan');
Route::delete('plan/cancel', 'ProfileBillingController@cancel')->name('cancel.plan');
Route::get('invoices', 'ProfileBillingController@invoices')->name('invoices');
Route::get('invoices/{id}/pdf', 'ProfileBillingController@pdf')->name('invoices.pdf');
Route::post('card/update', [ProfileBillingController::class, 'updateCard'])->name('update.card');
Route::delete('card', [ProfileBillingController::class, 'deleteCard'])->name('delete.card');
Route::post('plan/update', [ProfileBillingController::class, 'updatePlan'])->name('update.plan');
Route::delete('plan/cancel', [ProfileBillingController::class, 'cancel'])->name('cancel.plan');
Route::get('invoices', [ProfileBillingController::class, 'invoices'])->name('invoices');
Route::get('invoices/{id}/pdf', [ProfileBillingController::class, 'pdf'])->name('invoices.pdf');
});
}
Route::post('toggle-theme', 'ProfileController@toggleTheme')->name('toggle-theme');
Route::post('toggle-theme', [ProfileController::class, 'toggleTheme'])->name('toggle-theme');
Route::post('request-ftp-password', 'ProfileController@requestFtpPassword')->name('request-ftp-password');
Route::post('request-ftp-password', [ProfileController::class, 'requestFtpPassword'])->name('request-ftp-password');
});
// Support routes
if (setting('support')) {
Route::group(['prefix' => 'support', 'as' => 'support.'], function () {
Route::get('/', 'SupportController@index')->name('index');
Route::get('create', 'SupportController@create')->name('create');
Route::get('closed', 'SupportController@indexClosed')->name('index.closed');
Route::post('/', 'SupportController@store')->name('store');
Route::get('{support}', 'SupportController@show')->name('show');
Route::post('{support}/reply', 'SupportController@reply')->name('reply');
Route::post('{support}/close', 'SupportController@close')->name('close');
Route::get('/', [SupportController::class, 'index'])->name('index');
Route::get('create', [SupportController::class, 'create'])->name('create');
Route::get('closed', [SupportController::class, 'indexClosed'])->name('index.closed');
Route::post('/', [SupportController::class, 'store'])->name('store');
Route::get('{support}', [SupportController::class, 'show'])->name('show');
Route::post('{support}/reply', [SupportController::class, 'reply'])->name('reply');
Route::post('{support}/close', [SupportController::class, 'close'])->name('close');
});
}
if (setting('documentation')) {
Route::group(['prefix' => 'documentation', 'as' => 'documentation.'], function () {
Route::get('/', 'DocumentationController@index')->name('index');
Route::get('{documentationCategory}', 'DocumentationController@show')->name('show');
Route::get('{documentationCategory}/article/{documentationItem}', 'DocumentationController@showArticle')->name('article.show');
Route::get('/', [DocumentationController::class, 'index'])->name('index');
Route::get('{documentationCategory}', [DocumentationController::class, 'show'])->name('show');
Route::get('{documentationCategory}/article/{documentationItem}', [DocumentationController::class, 'showArticle'])->name('article.show');
});
}
});

View File

@@ -2,15 +2,16 @@
use App\Models\User;
use App\Models\Server;
use App\Models\Package;
use App\Models\Provider;
use App\Models\ProviderPlan;
use App\Models\ProviderRegion;
use function Pest\Laravel\get;
use function Pest\Laravel\post;
use function Pest\Laravel\actingAs;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;
use Database\Factories\PackageFactory;
use function Pest\Laravel\assertDatabaseHas;
use App\Mail\Admin\Server\AdminServerCreatedEmail;
@@ -101,3 +102,49 @@ it('cannot create a server without permissions', function () {
// However, if the validation fails, we get an HTTP failed assertion for a stray request.
->assertOk();
});
it('can list only server provider plans that are allowed in the package', function () {
$provider = Provider::factory()
->withRegion()
->has(ProviderPlan::factory()->set('label', 'Provider Plan A'), 'plans')
->has(ProviderPlan::factory()->set('label', 'Provider Plan B'), 'plans')
->create();
$providerRegion = $provider->regions->sole();
[$providerPlanA, $providerPlanB] = $provider->plans;
$package = Package::factory()
->hasAttached($provider)
->serverPermissions()
->create();
actingAs($user = User::factory()->set('package_id', $package)->create());
// No provider plans attached to package for this provider, so all provider plans should be visible.
get(route('servers.plans-and-regions', ['provider' => $provider]))
->assertOk()
->assertExactJson([
'regions' => [
$providerRegion->getKey() => $providerRegion->label,
],
'plans' => [
$providerPlanA->getKey() => 'Provider Plan A',
$providerPlanB->getKey() => 'Provider Plan B',
]
]);
$package->providerPlans()->attach($providerPlanB);
// Only provider plan B for this provider attached to package, so only provider plan B should be visible
get(route('servers.plans-and-regions', ['provider' => $provider]))
->assertOk()
->assertExactJson([
'regions' => [
$providerRegion->getKey() => $providerRegion->label,
],
'plans' => [
$providerPlanB->getKey() => 'Provider Plan B',
]
]);
});