Merge pull request #123 from ControlPanel-gg/development

Development
This commit is contained in:
AVMG 2021-07-12 14:11:16 +02:00 committed by GitHub
commit af508ded4b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 1753 additions and 23 deletions

View file

@ -78,7 +78,7 @@ class UserController extends Controller
"name" => "required|string|min:4|max:30",
"pterodactyl_id" => "required|numeric|unique:users,pterodactyl_id,{$user->id}",
"email" => "required|string|email",
"credits" => "required|numeric|min:0|max:999999",
"credits" => "required|numeric|min:0|max:99999999",
"server_limit" => "required|numeric|min:0|max:1000000",
"role" => Rule::in(['admin', 'mod', 'client', 'member']),
]);

View file

@ -0,0 +1,196 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Voucher;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Validation\ValidationException;
class VoucherController extends Controller
{
/**
* Display a listing of the resource.
*
* @return Application|Factory|View
*/
public function index()
{
return view('admin.vouchers.index');
}
/**
* Show the form for creating a new resource.
*
* @return Application|Factory|View
*/
public function create()
{
return view('admin.vouchers.create');
}
/**
* Store a newly created resource in storage.
*
* @param Request $request
* @return RedirectResponse
*/
public function store(Request $request)
{
$request->validate([
'memo' => 'nullable|string|max:191',
'code' => 'required|string|alpha_dash|max:36|min:4',
'uses' => 'required|numeric|max:2147483647|min:1',
'credits' => 'required|numeric|between:0,99999999',
'expires_at' => ['nullable','date_format:d-m-Y','after:today',"before:10 years"],
]);
Voucher::create($request->except('_token'));
return redirect()->route('admin.vouchers.index')->with('success', 'voucher has been created!');
}
/**
* Display the specified resource.
*
* @param Voucher $voucher
* @return Response
*/
public function show(Voucher $voucher)
{
//
}
/**
* Show the form for editing the specified resource.
*
* @param Voucher $voucher
* @return Application|Factory|View
*/
public function edit(Voucher $voucher)
{
return view('admin.vouchers.edit' , [
'voucher' => $voucher
]);
}
/**
* Update the specified resource in storage.
*
* @param Request $request
* @param Voucher $voucher
* @return RedirectResponse
*/
public function update(Request $request, Voucher $voucher)
{
$request->validate([
'memo' => 'nullable|string|max:191',
'code' => 'required|string|alpha_dash|max:36|min:4',
'uses' => 'required|numeric|max:2147483647|min:1',
'credits' => 'required|numeric|between:0,99999999',
'expires_at' => ['nullable','date_format:d-m-Y','after:today',"before:10 years"],
]);
$voucher->update($request->except('_token'));
return redirect()->route('admin.vouchers.index')->with('success', 'voucher has been updated!');
}
/**
* Remove the specified resource from storage.
*
* @param Voucher $voucher
* @return RedirectResponse
*/
public function destroy(Voucher $voucher)
{
$voucher->delete();
return redirect()->back()->with('success', 'voucher has been removed!');
}
/**
* @param Request $request
* @return JsonResponse
* @throws ValidationException
*/
public function redeem(Request $request)
{
#general validations
$request->validate([
'code' => 'required|exists:vouchers,code'
]);
#get voucher by code
$voucher = Voucher::where('code' , '=' , $request->input('code'))->firstOrFail();
#extra validations
if ($voucher->getStatus() == 'USES_LIMIT_REACHED') throw ValidationException::withMessages([
'code' => 'This voucher has reached the maximum amount of uses'
]);
if ($voucher->getStatus() == 'EXPIRED') throw ValidationException::withMessages([
'code' => 'This voucher has expired'
]);
if (!$request->user()->vouchers()->where('id' , '=' , $voucher->id)->get()->isEmpty()) throw ValidationException::withMessages([
'code' => 'You already redeemed this voucher code'
]);
if ($request->user()->credits + $voucher->credits >= 99999999) throw ValidationException::withMessages([
'code' => "You can't redeem this voucher because you would exceed the credit limit"
]);
#redeem voucher
$voucher->redeem($request->user());
return response()->json([
'success' => "{$voucher->credits} credits have been added to your balance!"
]);
}
public function dataTable()
{
$query = Voucher::query();
return datatables($query)
->addColumn('actions', function (Voucher $voucher) {
return '
<a data-content="Edit" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.vouchers.edit', $voucher->id) . '" class="btn btn-sm btn-info mr-1"><i class="fas fa-pen"></i></a>
<form class="d-inline" onsubmit="return submitResult();" method="post" action="' . route('admin.vouchers.destroy', $voucher->id) . '">
' . csrf_field() . '
' . method_field("DELETE") . '
<button data-content="Delete" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm btn-danger mr-1"><i class="fas fa-trash"></i></button>
</form>
';
})
->addColumn('status', function (Voucher $voucher) {
$color = 'success';
if ($voucher->getStatus() != 'VALID') $color = 'danger';
return '<span class="badge badge-' . $color . '">' . $voucher->getStatus() . '</span>';
})
->editColumn('uses', function (Voucher $voucher) {
$userCount = $voucher->users()->count();
return "{$userCount} / {$voucher->uses}";
})
->editColumn('credits', function (Voucher $voucher) {
return number_format($voucher->credits, 2, '.', '');
})
->editColumn('expires_at', function (Voucher $voucher) {
if (!$voucher->expires_at) return "";
return $voucher->expires_at ? $voucher->expires_at->diffForHumans() : '';
})
->editColumn('code', function (Voucher $voucher) {
return "<code>{$voucher->code}</code>";
})
->rawColumns(['actions', 'code', 'status'])
->make();
}
}

View file

@ -5,6 +5,7 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
class LoginController extends Controller
{
@ -37,4 +38,34 @@ class LoginController extends Controller
{
$this->middleware('guest')->except('logout');
}
public function login(Request $request)
{
$request->validate([
$this->username() => 'required|string',
'password' => 'required|string',
'g-recaptcha-response' => ['required','recaptcha'],
]);
// If the class is using the ThrottlesLogins trait, we can automatically throttle
// the login attempts for this application. We'll key this by the username and
// the IP address of the client making these requests into this application.
if (method_exists($this, 'hasTooManyLoginAttempts') &&
$this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);
return $this->sendLockoutResponse($request);
}
if ($this->attemptLogin($request)) {
return $this->sendLoginResponse($request);
}
// If the login attempt was unsuccessful we will increment the number of attempts
// to login and redirect the user back to the login form. Of course, when this
// user surpasses their maximum number of attempts they will get locked out.
$this->incrementLoginAttempts($request);
return $this->sendFailedLoginResponse($request);
}
}

View file

@ -20,7 +20,8 @@ class ProfileController extends Controller
return view('profile.index')->with([
'user' => Auth::user(),
'credits_reward_after_verify_discord' => Configuration::getValueByKey('CREDITS_REWARD_AFTER_VERIFY_DISCORD'),
'discord_verify_command' => Configuration::getValueByKey('DISCORD_VERIFY_COMMAND')
'force_email_verification' => Configuration::getValueByKey('FORCE_EMAIL_VERIFICATION'),
'force_discord_verification' => Configuration::getValueByKey('FORCE_DISCORD_VERIFICATION'),
]);
}

View file

@ -7,17 +7,30 @@ use App\Notifications\Auth\QueuedVerifyEmail;
use App\Notifications\WelcomeMessage;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Spatie\Activitylog\Traits\CausesActivity;
use Spatie\Activitylog\Traits\LogsActivity;
/**
* Class User
* @package App\Models
*/
class User extends Authenticatable implements MustVerifyEmail
{
use HasFactory, Notifiable, LogsActivity, CausesActivity;
/**
* @var string[]
*/
protected static $logAttributes = ['name', 'email'];
/**
* @var string[]
*/
protected static $ignoreChangedAttributes = [
'remember_token',
'credits',
@ -68,6 +81,9 @@ class User extends Authenticatable implements MustVerifyEmail
'last_seen' => 'datetime',
];
/**
*
*/
public static function boot()
{
parent::boot();
@ -89,24 +105,38 @@ class User extends Authenticatable implements MustVerifyEmail
}
});
$user->vouchers()->detach();
Pterodactyl::client()->delete("/application/users/{$user->pterodactyl_id}");
});
}
/**
*
*/
public function sendEmailVerificationNotification()
{
$this->notify(new QueuedVerifyEmail);
}
/**
* @return string
*/
public function credits()
{
return number_format($this->credits, 2, '.', '');
}
/**
* @return string
*/
public function getAvatar(){
return "https://www.gravatar.com/avatar/" . md5(strtolower(trim($this->email)));
}
/**
* @return string
*/
public function creditUsage()
{
$usage = 0;
@ -118,6 +148,9 @@ class User extends Authenticatable implements MustVerifyEmail
return number_format($usage, 2, '.', '');
}
/**
* @return array|string|string[]
*/
public function getVerifiedStatus(){
$status = '';
if ($this->hasVerifiedEmail()) $status .= 'email ';
@ -126,15 +159,31 @@ class User extends Authenticatable implements MustVerifyEmail
return $status;
}
/**
* @return BelongsToMany
*/
public function vouchers(){
return $this->belongsToMany(Voucher::class);
}
/**
* @return HasOne
*/
public function discordUser(){
return $this->hasOne(DiscordUser::class);
}
/**
* @return HasMany
*/
public function servers()
{
return $this->hasMany(Server::class);
}
/**
* @return HasMany
*/
public function payments()
{
return $this->hasMany(Payment::class);

98
app/Models/Voucher.php Normal file
View file

@ -0,0 +1,98 @@
<?php
namespace App\Models;
use Exception;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Spatie\Activitylog\Traits\LogsActivity;
/**
* Class Voucher
* @package App\Models
*/
class Voucher extends Model
{
use HasFactory, LogsActivity;
/**
* @var string[]
*/
protected $fillable = [
'memo',
'code',
'credits',
'uses',
'expires_at',
];
protected $dates = [
'expires_at'
];
/**
*
*/
public static function boot()
{
parent::boot();
static::deleting(function (Voucher $voucher) {
$voucher->users()->detach();
});
}
/**
* @return BelongsToMany
*/
public function users()
{
return $this->belongsToMany(User::class);
}
/**
* @return string
*/
public function getStatus()
{
if ($this->users()->count() >= $this->uses) return 'USES_LIMIT_REACHED';
if (!is_null($this->expires_at)) {
if ($this->expires_at->isPast()) return 'EXPIRED';
}
return 'VALID';
}
/**
* @param User $user
* @return float
* @throws Exception
*/
public function redeem(User $user)
{
try {
$user->increment('credits', $this->credits);
$this->users()->attach($user);
$this->logRedeem($user);
} catch (Exception $exception) {
throw $exception;
}
return $this->credits;
}
/**
* @param User $user
* @return null
*/
private function logRedeem(User $user)
{
activity()
->performedOn($this)
->causedBy($user)
->log('redeemed');
return null;
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Database\Factories;
use App\Models\voucher;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
class VoucherFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = voucher::class;
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'memo' => $this->faker->word(),
'code' => Str::random(36),
'credits' => $this->faker->numberBetween(100, 1000),
'uses' => $this->faker->numberBetween(1, 1000),
'expires_at' => now()->addDays($this->faker->numberBetween(1, 90))->format('d-m-Y')
];
}
}

View file

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateVouchersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('vouchers', function (Blueprint $table) {
$table->id();
$table->string('code', 36)->unique();
$table->string('memo')->nullable();
$table->unsignedFloat('credits', 10);
$table->unsignedInteger('uses')->default(1);
$table->timestamp('expires_at')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('vouchers');
}
}

View file

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

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class UpdateCreditsToUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->unsignedFloat('credits', 10)->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->unsignedFloat('credits')->change();
});
}
}

View file

@ -78,6 +78,9 @@
@case('created')
<small><i class="fas text-success fa-plus mr-2"></i></small>
@break
@case('redeemed')
<small><i class="fas text-success fa-money-check-alt mr-2"></i></small>
@break
@case('deleted')
<small><i class="fas text-danger fa-times mr-2"></i></small>
@break

View file

@ -72,7 +72,7 @@
<div class="form-group">
<label for="credits">Credits</label>
<input value="{{$user->credits}}" id="credits" name="credits" step="any" min="0"
max="1000000"
max="99999999"
type="number" class="form-control @error('credits') is-invalid @enderror"
required="required">
@error('credits')

View file

@ -0,0 +1,181 @@
@extends('layouts.main')
@section('content')
<!-- CONTENT HEADER -->
<section class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1>Vouchers</h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="{{route('home')}}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{route('admin.vouchers.index')}}">Vouchers</a></li>
<li class="breadcrumb-item"><a class="text-muted" href="{{route('admin.products.create')}}">Create</a>
</li>
</ol>
</div>
</div>
</div>
</section>
<!-- END CONTENT HEADER -->
<!-- MAIN CONTENT -->
<section class="content">
<div class="container-fluid">
<div class="row">
<div class="col-lg-6">
<div class="card">
<div class="card-header">
<h5 class="card-title">
<i class="fas fa-money-check-alt mr-2"></i>Voucher details
</h5>
</div>
<div class="card-body">
<form action="{{route('admin.vouchers.store')}}" method="POST">
@csrf
<div class="form-group">
<label for="memo">Memo <i data-toggle="popover" data-trigger="hover" data-content="Only admins can see this" class="fas fa-info-circle"></i></label>
<input value="{{old('memo')}}" placeholder="Summer break voucher" id="memo"
name="memo" type="text"
class="form-control @error('memo') is-invalid @enderror">
@error('memo')
<div class="text-danger">
{{$message}}
</div>
@enderror
</div>
<div class="form-group">
<label for="credits">* Credits</label>
<input value="{{old('credits')}}" placeholder="500" id="credits"
name="credits" type="number" step="any" min="0"
max="99999999"
class="form-control @error('credits') is-invalid @enderror">
@error('credits')
<div class="text-danger">
{{$message}}
</div>
@enderror
</div>
<div class="form-group">
<label for="code">* Code</label>
<div class="input-group">
<input value="{{old('code')}}" placeholder="SUMMER" id="code" name="code"
type="text"
class="form-control @error('code') is-invalid @enderror"
required="required">
<div class="input-group-append">
<button class="btn btn-info" onclick="setRandomCode()" type="button">
Random
</button>
</div>
</div>
@error('code')
<div class="text-danger">
{{$message}}
</div>
@enderror
</div>
<div class="form-group">
<label for="uses">* Uses<i data-toggle="popover" data-trigger="hover" data-content="A voucher can only be used one time per user. Uses specifies the number of different users that can use this voucher." class="fas fa-info-circle"></i></label>
<div class="input-group">
<input value="{{old('uses') ?? 1}}" id="uses" min="1" max="2147483647"
name="uses" type="number"
class="form-control @error('uses') is-invalid @enderror"
required="required">
<div class="input-group-append">
<button class="btn btn-info" onclick="setMaxUses()" type="button">Max
</button>
</div>
</div>
@error('uses')
<div class="text-danger">
{{$message}}
</div>
@enderror
</div>
<div class="form-group mb-3">
<label for="expires_at">Expires at</label>
<div class="input-group date" id="expires_at" data-target-input="nearest">
<input value="{{old('expires_at')}}" name="expires_at" placeholder="dd-mm-yyyy" type="text" class="form-control @error('expires_at') is-invalid @enderror datetimepicker-input" data-target="#expires_at"/>
<div class="input-group-append" data-target="#expires_at" data-toggle="datetimepicker">
<div class="input-group-text"><i class="fa fa-calendar"></i></div>
</div>
</div>
@error('expires_at')
<div class="text-danger">
{{$message}}
</div>
@enderror
</div>
<div class="form-group text-right">
<button type="submit" class="btn btn-primary">
Submit
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<i class="fas"></i>
</div>
</section>
<!-- END CONTENT -->
<script>
document.addEventListener('DOMContentLoaded', (event) => {
$('#expires_at').datetimepicker({
format : 'DD-MM-yyyy',
icons: {
time: 'far fa-clock',
date: 'far fa-calendar',
up: 'fas fa-arrow-up',
down: 'fas fa-arrow-down',
previous: 'fas fa-chevron-left',
next: 'fas fa-chevron-right',
today: 'fas fa-calendar-check',
clear: 'far fa-trash-alt',
close: 'far fa-times-circle'
}
});
})
function setMaxUses() {
let element = document.getElementById('uses')
element.value = element.max;
console.log(element.max)
}
function setRandomCode() {
let element = document.getElementById('code')
element.value = getRandomCode(36)
}
function getRandomCode(length) {
let result = '';
let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-';
let charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() *
charactersLength));
}
return result;
}
</script>
@endsection

View file

@ -0,0 +1,186 @@
@extends('layouts.main')
@section('content')
<!-- CONTENT HEADER -->
<section class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1>Vouchers</h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="{{route('home')}}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{route('admin.vouchers.index')}}">Vouchers</a></li>
<li class="breadcrumb-item"><a class="text-muted"
href="{{route('admin.products.edit' , $voucher->id)}}">Edit</a>
</li>
</ol>
</div>
</div>
</div>
</section>
<!-- END CONTENT HEADER -->
<!-- MAIN CONTENT -->
<section class="content">
<div class="container-fluid">
<div class="row">
<div class="col-lg-6">
<div class="card">
<div class="card-header">
<h5 class="card-title">
<i class="fas fa-money-check-alt mr-2"></i>Voucher details
</h5>
</div>
<div class="card-body">
<form action="{{route('admin.vouchers.update' , $voucher->id)}}" method="POST">
@csrf
@method('PATCH')
<div class="form-group">
<label for="memo">Memo <i data-toggle="popover" data-trigger="hover"
data-content="Only admins can see this"
class="fas fa-info-circle"></i></label>
<input value="{{ $voucher->memo }}" placeholder="Summer break voucher" id="memo"
name="memo" type="text"
class="form-control @error('memo') is-invalid @enderror">
@error('memo')
<div class="text-danger">
{{$message}}
</div>
@enderror
</div>
<div class="form-group">
<label for="credits">Credits *</label>
<input value="{{$voucher->credits}}" placeholder="500" id="credits"
name="credits" type="number" step="any" min="0"
max="99999999"
class="form-control @error('credits') is-invalid @enderror">
@error('credits')
<div class="text-danger">
{{$message}}
</div>
@enderror
</div>
<div class="form-group">
<label for="code">Code *</label>
<div class="input-group">
<input value="{{$voucher->code}}" placeholder="SUMMER" id="code" name="code"
type="text"
class="form-control @error('code') is-invalid @enderror"
required="required">
<div class="input-group-append">
<button class="btn btn-info" onclick="setRandomCode()" type="button">
Random
</button>
</div>
</div>
@error('code')
<div class="text-danger">
{{$message}}
</div>
@enderror
</div>
<div class="form-group">
<label for="uses">Uses * <i data-toggle="popover" data-trigger="hover"
data-content="A voucher can only be used one time per user. Uses specifies the number of different users that can use this voucher."
class="fas fa-info-circle"></i></label>
<div class="input-group">
<input value="{{$voucher->uses}}" id="uses" min="1" max="2147483647"
name="uses" type="number"
class="form-control @error('uses') is-invalid @enderror"
required="required">
<div class="input-group-append">
<button class="btn btn-info" onclick="setMaxUses()" type="button">Max
</button>
</div>
</div>
@error('uses')
<div class="text-danger">
{{$message}}
</div>
@enderror
</div>
<div class="form-group mb-3">
<label for="expires_at">Expires at</label>
<div class="input-group date" id="expires_at" data-target-input="nearest">
<input value="{{$voucher->expires_at ? $voucher->expires_at->format('d-m-Y') : ''}}" name="expires_at" placeholder="dd-mm-yyyy" type="text" class="form-control @error('expires_at') is-invalid @enderror datetimepicker-input" data-target="#expires_at"/>
<div class="input-group-append" data-target="#expires_at" data-toggle="datetimepicker">
<div class="input-group-text"><i class="fa fa-calendar"></i></div>
</div>
</div>
@error('expires_at')
<div class="text-danger">
{{$message}}
</div>
@enderror
</div>
<div class="form-group text-right">
<button type="submit" class="btn btn-primary">
Submit
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- END CONTENT -->
<script>
document.addEventListener('DOMContentLoaded', (event) => {
$('#expires_at').datetimepicker({
format : 'DD-MM-yyyy',
icons: {
time: 'far fa-clock',
date: 'far fa-calendar',
up: 'fas fa-arrow-up',
down: 'fas fa-arrow-down',
previous: 'fas fa-chevron-left',
next: 'fas fa-chevron-right',
today: 'fas fa-calendar-check',
clear: 'far fa-trash-alt',
close: 'far fa-times-circle'
}
});
})
function setMaxUses() {
let element = document.getElementById('uses')
element.value = element.max;
console.log(element.max)
}
function setRandomCode() {
let element = document.getElementById('code')
element.value = getRandomCode(36)
}
function getRandomCode(length) {
let result = '';
let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-';
let charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() *
charactersLength));
}
return result;
}
</script>
@endsection

View file

@ -0,0 +1,94 @@
@extends('layouts.main')
@section('content')
<!-- CONTENT HEADER -->
<section class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1>Vouchers</h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="{{route('home')}}">Dashboard</a></li>
<li class="breadcrumb-item"><a class="text-muted"
href="{{route('admin.vouchers.index')}}">Vouchers</a></li>
</ol>
</div>
</div>
</div>
</section>
<!-- END CONTENT HEADER -->
<!-- MAIN CONTENT -->
<section class="content">
<div class="container-fluid">
<div class="card">
<div class="card-header">
<div class="d-flex justify-content-between">
<h5 class="card-title"><i class="fas fa-money-check-alt mr-2"></i>Vouchers</h5>
<a href="{{route('admin.vouchers.create')}}" class="btn btn-sm btn-primary"><i
class="fas fa-plus mr-1"></i>Create new</a>
</div>
</div>
<div class="card-body table-responsive">
<table id="datatable" class="table table-striped">
<thead>
<tr>
<th>Status</th>
<th>Code</th>
<th>Memo</th>
<th>Credits</th>
<th>Used / Uses</th>
<th>Expires</th>
<th></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
<!-- END CUSTOM CONTENT -->
</section>
<!-- END CONTENT -->
<script>
function submitResult() {
return confirm("Are you sure you wish to delete?") !== false;
}
document.addEventListener("DOMContentLoaded", function () {
$('#datatable').DataTable({
processing: true,
serverSide: true,
stateSave: true,
ajax: "{{route('admin.vouchers.datatable')}}",
columns: [
{data: 'status'},
{data: 'code'},
{data: 'memo'},
{data: 'credits'},
{data: 'uses'},
{data: 'expires_at'},
{data: 'actions', sortable: false},
],
fnDrawCallback: function( oSettings ) {
$('[data-toggle="popover"]').popover();
}
});
});
</script>
@endsection

View file

@ -0,0 +1,240 @@
@extends('layouts.main')
@section('content')
<!-- CONTENT HEADER -->
<section class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1>Products</h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="{{route('home')}}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{route('admin.users.index')}}">Products</a></li>
<li class="breadcrumb-item"><a class="text-muted"
href="{{route('admin.products.show' , $product->id)}}">Show</a>
</li>
</ol>
</div>
</div>
</div>
</section>
<!-- END CONTENT HEADER -->
<!-- MAIN CONTENT -->
<section class="content">
<div class="container-fluid">
<div class="card">
<div class="card-header d-flex justify-content-between">
<h5 class="card-title"><i class="fas fa-sliders-h mr-2"></i>Product</h5>
<div class="ml-auto">
<a data-content="Edit" data-trigger="hover" data-toggle="tooltip" href="{{ route('admin.products.edit', $product->id) }}" class="btn btn-sm btn-info mr-1"><i class="fas fa-pen"></i></a>
<form class="d-inline" onsubmit="return submitResult();" method="post" action="{{ route('admin.products.destroy', $product->id) }}">
{{ csrf_field() }}
{{ method_field("DELETE") }}
<button data-content="Delete" data-trigger="hover" data-toggle="tooltip" class="btn btn-sm btn-danger mr-1"><i class="fas fa-trash"></i></button>
</form>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-lg-6">
<div class="row">
<div class="col-lg-4">
<label>ID</label>
</div>
<div class="col-lg-8">
<span style="max-width: 250px;" class="d-inline-block text-truncate">
{{$product->id}}
</span>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="row">
<div class="col-lg-4">
<label>Name</label>
</div>
<div class="col-lg-8">
<span style="max-width: 250px;" class="d-inline-block text-truncate">
{{$product->name}}
</span>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="row">
<div class="col-lg-4">
<label>Price</label>
</div>
<div class="col-lg-8">
<span style="max-width: 250px;" class="d-inline-block text-truncate">
<i class="fas fa-coins mr-1"></i>{{$product->price}}
</span>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="row">
<div class="col-lg-4">
<label>Memory</label>
</div>
<div class="col-lg-8">
<span style="max-width: 250px;" class="d-inline-block text-truncate">
{{$product->memory}}
</span>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="row">
<div class="col-lg-4">
<label>CPU</label>
</div>
<div class="col-lg-8">
<span style="max-width: 250px;" class="d-inline-block text-truncate">
{{$product->cpu}}
</span>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="row">
<div class="col-lg-4">
<label>Swap</label>
</div>
<div class="col-lg-8">
<span style="max-width: 250px;" class="d-inline-block text-truncate">
{{$product->swap}}
</span>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="row">
<div class="col-lg-4">
<label>Disk</label>
</div>
<div class="col-lg-8">
<span style="max-width: 250px;" class="d-inline-block text-truncate">
{{$product->disk}}
</span>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="row">
<div class="col-lg-4">
<label>IO</label>
</div>
<div class="col-lg-8">
<span style="max-width: 250px;" class="d-inline-block text-truncate">
{{$product->io}}
</span>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="row">
<div class="col-lg-4">
<label>Databases</label>
</div>
<div class="col-lg-8">
<span style="max-width: 250px;" class="d-inline-block text-truncate">
{{$product->databases}}
</span>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="row">
<div class="col-lg-4">
<label>Allocations</label>
</div>
<div class="col-lg-8">
<span style="max-width: 250px;" class="d-inline-block text-truncate">
{{$product->allocations}}
</span>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="row">
<div class="col-lg-4">
<label>Created At</label>
</div>
<div class="col-lg-8">
<span style="max-width: 250px;" class="d-inline-block text-truncate">
{{$product->created_at ? $product->created_at->diffForHumans() : ''}}
</span>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="row">
<div class="col-lg-4">
<label>Description</label>
</div>
<div class="col-lg-8">
<span class="d-inline-block text-truncate">
{{$product->description}}
</span>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="row">
<div class="col-lg-4">
<label>Updated At</label>
</div>
<div class="col-lg-8">
<span style="max-width: 250px;" class="d-inline-block text-truncate">
{{$product->updated_at ? $product->updated_at->diffForHumans() : ''}}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h5 class="card-title"><i class="fas fa-server mr-2"></i>Servers</h5>
</div>
<div class="card-body table-responsive">
@include('admin.servers.table' , ['filter' => '?product=' . $product->id])
</div>
</div>
</div>
<!-- END CUSTOM CONTENT -->
</div>
</section>
<!-- END CONTENT -->
@endsection

View file

@ -54,6 +54,15 @@
@enderror
</div>
<div class="input-group mb-3">
{!! htmlFormSnippet() !!}
@error('g-recaptcha-response')
<span class="text-danger" role="alert">
<small><strong>{{ $message }}</strong></small>
</span>
@enderror
</div>
<div class="row">
<div class="col-8">
<div class="icheck-primary">

View file

@ -117,6 +117,9 @@
@case('created')
<small><i class="fas text-success fa-plus mr-2"></i></small>
@break
@case('redeemed')
<small><i class="fas text-success fa-money-check-alt mr-2"></i></small>
@break
@case('deleted')
<small><i class="fas text-danger fa-times mr-2"></i></small>
@break

View file

@ -16,6 +16,9 @@
{{-- summernote --}}
<link rel="stylesheet" href="{{asset('plugins/summernote/summernote-bs4.min.css')}}">
{{-- datetimepicker --}}
<link rel="stylesheet" href="{{asset('plugins/tempusdominus-bootstrap-4/css/tempusdominus-bootstrap-4.min.css')}}">
<link rel="stylesheet" href="{{asset('css/app.css')}}">
<link rel="preload" href="{{asset('plugins/fontawesome-free/css/all.min.css')}}" as="style"
onload="this.onload=null;this.rel='stylesheet'">
@ -96,6 +99,11 @@
Log back in
</a>
@endif
<a class="dropdown-item" data-toggle="modal" data-target="#redeemVoucherModal"
href="javascript:void(0)">
<i class="fas fa-money-check-alt fa-sm fa-fw mr-2 text-gray-400"></i>
Redeem code
</a>
<div class="dropdown-divider"></div>
<form method="post" action="{{route('logout')}}">
@csrf
@ -146,13 +154,15 @@
</a>
</li>
<li class="nav-item">
<a href="{{route('store.index')}}"
class="nav-link @if(Request::routeIs('store.*') || Request::routeIs('checkout')) active @endif">
<i class="nav-icon fa fa-coins"></i>
<p>Store</p>
</a>
</li>
@if(env('PAYPAL_SECRET') && env('PAYPAL_CLIENT_ID') || env('APP_ENV', 'local') == 'local')
<li class="nav-item">
<a href="{{route('store.index')}}"
class="nav-link @if(Request::routeIs('store.*') || Request::routeIs('checkout')) active @endif">
<i class="nav-icon fa fa-coins"></i>
<p>Store</p>
</a>
</li>
@endif
@if(Auth::user()->role == 'admin')
<li class="nav-header">Admin</li>
@ -189,6 +199,14 @@
</a>
</li>
<li class="nav-item">
<a href="{{route('admin.vouchers.index')}}"
class="nav-link @if(Request::routeIs('admin.vouchers.*')) active @endif">
<i class="nav-icon fas fa-money-check-alt"></i>
<p>Vouchers</p>
</a>
</li>
<li class="nav-header">Pterodactyl</li>
<li class="nav-item">
@ -227,6 +245,7 @@
</a>
</li>
<li class="nav-header">Dashboard</li>
<li class="nav-item">
@ -237,6 +256,16 @@
</a>
</li>
<li class="nav-item">
<a href="{{route('admin.usefullinks.index')}}"
class="nav-link @if(Request::routeIs('admin.usefullinks.*')) active @endif">
<i class="nav-icon fas fa-link"></i>
<p>Useful Links</p>
</a>
</li>
<li class="nav-header">Settings</li>
<li class="nav-item">
<a href="{{route('admin.configurations.index')}}"
class="nav-link @if(Request::routeIs('admin.configurations.*')) active @endif">
@ -253,15 +282,6 @@
</a>
</li>
<li class="nav-item">
<a href="{{route('admin.usefullinks.index')}}"
class="nav-link @if(Request::routeIs('admin.usefullinks.*')) active @endif">
<i class="nav-icon fas fa-link"></i>
<p>Useful Links</p>
</a>
</li>
@endif
</ul>
@ -276,7 +296,7 @@
<div class="content-wrapper">
@if(!Auth::user()->hasVerifiedEmail())
@if(Auth::user()->created_at->diffInHours(now(), false) > 2)
@if(Auth::user()->created_at->diffInHours(now(), false) > 1)
<div class="alert alert-warning p-2 m-2">
<h5><i class="icon fas fa-exclamation-circle"></i> Warning!</h5>
You have not yet verified your email address <a class="text-primary"
@ -289,6 +309,8 @@
@endif
@yield('content')
@include('models.redeem_voucher_modal')
</div>
<!-- /.content-wrapper -->
<footer class="main-footer">
@ -316,9 +338,21 @@
<!-- Summernote -->
<script src="{{asset('plugins/summernote/summernote-bs4.min.js')}}"></script>
<!-- Moment.js -->
<script src="{{asset('plugins/moment/moment.min.js')}}"></script>
<!-- Datetimepicker -->
<script src="{{asset('plugins/tempusdominus-bootstrap-4/js/tempusdominus-bootstrap-4.min.js')}}"></script>
<script>
$(document).ready(function () {
$('[data-toggle="popover"]').popover();
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});
});
</script>
<script>

View file

@ -0,0 +1,107 @@
<!-- The Modal -->
<div class="modal fade" id="redeemVoucherModal">
<div class="modal-dialog">
<div class="modal-content">
<!-- Modal Header -->
<div class="modal-header">
<h4 class="modal-title">Redeem voucher code</h4>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<!-- Modal body -->
<div class="modal-body">
<form id="redeemVoucherForm" onsubmit="return false" method="post" action="{{route('voucher.redeem')}}">
<div class="form-group">
<label for="redeemVoucherCode">Code</label>
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text">
<i class="fas fa-money-check-alt"></i>
</div>
</div>
<input id="redeemVoucherCode" name="code" placeholder="SUMMER" type="text"
class="form-control">
</div>
<span id="redeemVoucherCodeError" class="text-danger"></span>
<span id="redeemVoucherCodeSuccess" class="text-success"></span>
</div>
</form>
</div>
<!-- Modal footer -->
<div class="modal-footer">
<button type="button" class="btn btn-danger" data-dismiss="modal">Close</button>
<button name="submit" id="redeemVoucherSubmit" onclick="redeemVoucherCode()" type="button"
class="btn btn-primary">Redeem
</button>
</div>
</div>
</div>
</div>
<script>
function redeemVoucherCode() {
let form = document.getElementById('redeemVoucherForm')
let button = document.getElementById('redeemVoucherSubmit')
let input = document.getElementById('redeemVoucherCode')
console.log(form.method, form.action)
button.disabled = true
$.ajax({
method: form.method,
url: form.action,
dataType: 'json',
data: {
code: input.value
},
success: function (response) {
resetForm()
redeemVoucherSetSuccess(response)
setTimeout(() => {
$('#redeemVoucherModal').modal('toggle');
} , 1500)
},
error: function (jqXHR, textStatus, errorThrown) {
resetForm()
redeemVoucherSetError(jqXHR)
console.error(jqXHR.responseJSON)
},
})
}
function resetForm() {
let button = document.getElementById('redeemVoucherSubmit')
let input = document.getElementById('redeemVoucherCode')
let successLabel = document.getElementById('redeemVoucherCodeSuccess')
let errorLabel = document.getElementById('redeemVoucherCodeError')
input.classList.remove('is-invalid')
input.classList.remove('is-valid')
successLabel.innerHTML = ''
errorLabel.innerHTML = ''
button.disabled = false
}
function redeemVoucherSetError(error) {
let input = document.getElementById('redeemVoucherCode')
let errorLabel = document.getElementById('redeemVoucherCodeError')
input.classList.add("is-invalid")
errorLabel.innerHTML = error.status === 422 ? error.responseJSON.errors.code[0] : error.responseJSON.message
}
function redeemVoucherSetSuccess(response) {
let input = document.getElementById('redeemVoucherCode')
let successLabel = document.getElementById('redeemVoucherCodeSuccess')
successLabel.innerHTML = response.success
input.classList.remove('is-invalid')
input.classList.add('is-valid')
}
</script>

View file

@ -25,7 +25,33 @@
<div class="container-fluid">
<div class="row">
<div class="col-lg-4">
<div class="col-lg-12 px-0">
@if(!Auth::user()->hasVerifiedEmail() && strtolower($force_email_verification) == 'true')
<div class="alert alert-warning p-2 m-2">
<h5><i class="icon fas fa-exclamation-circle"></i>Required Email verification!</h5>
You have not yet verified your email address
<a class="text-primary" href="{{route('verification.send')}}">Click here to resend
verification email</a> <br>
Please contact support If you didn't receive your verification email.
</div>
@endif
@if(is_null(Auth::user()->discordUser) && strtolower($force_discord_verification) == 'true')
@if(!empty(env('DISCORD_CLIENT_ID')) && !empty(env('DISCORD_CLIENT_SECRET')))
<div class="alert alert-warning p-2 m-2">
<h5><i class="icon fas fa-exclamation-circle"></i>Required Discord verification!</h5>
You have not yet verified your discord account
<a class="text-primary" href="{{route('auth.redirect')}}">Login with discord</a> <br>
Please contact support If you face any issues.
</div>
@else
<div class="alert alert-danger p-2 m-2">
<h5><i class="icon fas fa-exclamation-circle"></i>Required Discord verification!</h5>
Due to system settings you are required to verify your discord account! <br>
It looks like this hasn't been set-up correctly! Please contact support.
</div>
@endif
@endif
</div>
</div>
@ -50,7 +76,14 @@
<div class="col d-flex flex-column flex-sm-row justify-content-between mb-3">
<div class="text-center text-sm-left mb-2 mb-sm-0"><h4
class="pt-sm-2 pb-1 mb-0 text-nowrap">{{$user->name}}</h4>
<p class="mb-0">{{$user->email}}</p>
<p class="mb-0">{{$user->email}}
@if($user->hasVerifiedEmail())
<i data-toggle="popover" data-trigger="hover" data-content="Verified" class="text-success fas fa-check-circle"></i>
@else
<i data-toggle="popover" data-trigger="hover" data-content="Not verified" class="text-danger fas fa-exclamation-circle"></i>
@endif
</p>
<div class="mt-1">
<span class="badge badge-primary"><i class="fa fa-coins mr-2"></i>{{$user->Credits()}}</span>
</div>

View file

@ -24,6 +24,12 @@
<section class="content">
<div class="container-fluid">
<div class="text-right mb-3">
<button type="button" data-toggle="modal" data-target="#redeemVoucherModal" class="btn btn-primary">
<i class="fas fa-money-check-alt mr-2"></i>Redeem code
</button>
</div>
@if($isPaypalSetup && $products->count() > 0)
<div class="card">

View file

@ -12,15 +12,16 @@ use App\Http\Controllers\Admin\ServerController as AdminServerController;
use App\Http\Controllers\Admin\SettingsController;
use App\Http\Controllers\Admin\UsefulLinkController;
use App\Http\Controllers\Admin\UserController;
use App\Http\Controllers\Admin\VoucherController;
use App\Http\Controllers\Auth\SocialiteController;
use App\Http\Controllers\HomeController;
use App\Http\Controllers\NotificationController;
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\ServerController;
use App\Http\Controllers\StoreController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
use Illuminate\Http\Request;
/*
|--------------------------------------------------------------------------
@ -65,6 +66,9 @@ Route::middleware('auth')->group(function () {
Route::get('/auth/redirect', [SocialiteController::class, 'redirect'])->name('auth.redirect');
Route::get('/auth/callback', [SocialiteController::class, 'callback'])->name('auth.callback');
#voucher redeem
Route::post('/voucher/redeem', [VoucherController::class, 'redeem'])->middleware('throttle:5,1')->name('voucher.redeem');
#admin
Route::prefix('admin')->name('admin.')->middleware('admin')->group(function () {
@ -110,6 +114,9 @@ Route::middleware('auth')->group(function () {
Route::get('usefullinks/datatable', [UsefulLinkController::class, 'datatable'])->name('usefullinks.datatable');
Route::resource('usefullinks', UsefulLinkController::class);
Route::get('vouchers/datatable', [VoucherController::class, 'datatable'])->name('vouchers.datatable');
Route::resource('vouchers', VoucherController::class);
Route::get('api/datatable', [ApplicationApiController::class, 'datatable'])->name('api.datatable');
Route::resource('api', ApplicationApiController::class)->parameters([
'api' => 'applicationApi',

View file

@ -0,0 +1,318 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use App\Models\Voucher;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Str;
use Tests\TestCase;
/**
* Class TestUsefulLinksController
* @package Tests\Feature
*/
class TestVouchersController extends TestCase
{
use DatabaseTransactions;
/**
* @dataProvider accessibleRoutesDataProvider
* @param string $method
* @param string $route
* @param int $expectedStatus
*/
function test_accessible_routes(string $method, string $route, int $expectedStatus)
{
Voucher::factory()->create([
'id' => 1
]);
$response = $this->actingAs(User::factory()->create([
'role' => 'admin',
'pterodactyl_id' => '1'
]))->{$method}($route);
$response->assertStatus($expectedStatus);
}
/**
* @dataProvider VoucherDataProvider
* @param array $dataSet
* @param int $expectedCount
* @param bool $assertValidationErrors
*/
function test_creating_vouchers(array $dataSet, int $expectedCount, bool $assertValidationErrors)
{
$response = $this->actingAs($this->getTestUser())->post(route('admin.vouchers.store'), $dataSet);
if ($assertValidationErrors) $response->assertSessionHasErrors();
else $response->assertSessionHasNoErrors();
$response->assertRedirect();
$this->assertDatabaseCount('vouchers', $expectedCount);
}
/**
* @return User
*/
private function getTestUser(): User
{
return User::factory()->create([
'role' => 'admin',
'pterodactyl_id' => '1'
]);
}
/**
* @dataProvider VoucherDataProvider
* @param array $dataSet
* @param int $expectedCount
* @param bool $assertValidationErrors
*/
function test_updating_voucher(array $dataSet, int $expectedCount, bool $assertValidationErrors)
{
$voucher = Voucher::factory()->create([
'id' => 1
]);
$response = $this->actingAs($this->getTestUser())->patch(route('admin.vouchers.update', $voucher->id), $dataSet);
if ($assertValidationErrors) $response->assertSessionHasErrors();
else $response->assertSessionHasNoErrors();
$response->assertRedirect();
$this->assertDatabaseCount('vouchers', 1);
}
/**
*
*/
function test_deleting_vouchers()
{
$voucher = Voucher::factory()->create([
'id' => 1
]);
$response = $this->actingAs($this->getTestUser())->delete(route('admin.vouchers.update', $voucher->id));
$response->assertRedirect();
$this->assertDatabaseCount('vouchers', 0);
}
/**
* @return array
*/
function VoucherDataProvider(): array
{
return [
'Valid dataset 1' => [
'dataSet' => [
"memo" => "TESTING",
"code" => Str::random(20),
"credits" => 500,
"uses" => 500,
"expires_at" => now()->addDay()->format('d-m-Y'),
],
'expectedCount' => 1,
'assertValidationErrors' => false
],
'Valid dataset 2' => [
'dataSet' => [
"code" => Str::random(36),
"credits" => 500,
"uses" => 500,
],
'expectedCount' => 1,
'assertValidationErrors' => false
],
'Valid dataset 3' => [
'dataSet' => [
"memo" => "TESTING",
"code" => Str::random(4),
"credits" => 1000000,
"uses" => 1,
"expires_at" => now()->addYears(6)->format('d-m-Y'),
],
'expectedCount' => 1,
'assertValidationErrors' => false
],
'Invalid dataset (memo to long)' => [
'dataSet' => [
"memo" => Str::random(250),
"code" => Str::random(20),
"credits" => 500,
"uses" => 500,
"expires_at" => now()->addDay()->format('d-m-Y'),
],
'expectedCount' => 0,
'assertValidationErrors' => true
],
'Invalid dataset (code to short)' => [
'dataSet' => [
"memo" => Str::random(250),
"code" => Str::random(1),
"credits" => 500,
"uses" => 500,
"expires_at" => now()->addDay()->format('d-m-Y'),
],
'expectedCount' => 0,
'assertValidationErrors' => true
],
'Invalid dataset (code missing)' => [
'dataSet' => [
"memo" => Str::random(250),
"credits" => 500,
"uses" => 500,
"expires_at" => now()->addDay()->format('d-m-Y'),
],
'expectedCount' => 0,
'assertValidationErrors' => true
],
'Invalid dataset (code to long)' => [
'dataSet' => [
"memo" => Str::random(250),
"code" => Str::random(60),
"credits" => 500,
"uses" => 500,
"expires_at" => now()->addDay()->format('d-m-Y'),
],
'expectedCount' => 0,
'assertValidationErrors' => true
],
'Invalid dataset (credits missing)' => [
'dataSet' => [
"memo" => Str::random(250),
"code" => Str::random(1),
"uses" => 500,
"expires_at" => now()->addDay()->format('d-m-Y'),
],
'expectedCount' => 0,
'assertValidationErrors' => true
],
'Invalid dataset (0 credits)' => [
'dataSet' => [
"memo" => Str::random(250),
"code" => Str::random(1),
"credits" => 0,
"uses" => 500,
"expires_at" => now()->addDay()->format('d-m-Y'),
],
'expectedCount' => 0,
'assertValidationErrors' => true
],
'Invalid dataset (to many credits)' => [
'dataSet' => [
"memo" => Str::random(250),
"code" => Str::random(1),
"credits" => 99999999999,
"uses" => 500,
"expires_at" => now()->addDay()->format('d-m-Y'),
],
'expectedCount' => 0,
'assertValidationErrors' => true
],
'Invalid dataset (uses missing)' => [
'dataSet' => [
"memo" => Str::random(250),
"code" => Str::random(1),
"credits" => 99999999999,
"expires_at" => now()->addDay()->format('d-m-Y'),
],
'expectedCount' => 0,
'assertValidationErrors' => true
],
'Invalid dataset (0 uses)' => [
'dataSet' => [
"memo" => Str::random(250),
"code" => Str::random(1),
"credits" => 99999999999,
"uses" => 0,
"expires_at" => now()->addDay()->format('d-m-Y'),
],
'expectedCount' => 0,
'assertValidationErrors' => true
],
'Invalid dataset (expires_at today)' => [
'dataSet' => [
"memo" => Str::random(250),
"code" => Str::random(1),
"credits" => 99999999999,
"uses" => 500,
"expires_at" => now()->format('d-m-Y'),
],
'expectedCount' => 0,
'assertValidationErrors' => true
],
'Invalid dataset (expires_at earlier)' => [
'dataSet' => [
"memo" => Str::random(250),
"code" => Str::random(1),
"credits" => 99999999999,
"uses" => 500,
"expires_at" => now()->subDays(5)->format('d-m-Y'),
],
'expectedCount' => 0,
'assertValidationErrors' => true
],
'Invalid dataset (expires_at to far)' => [
'dataSet' => [
"memo" => Str::random(250),
"code" => Str::random(1),
"credits" => 99999999999,
"uses" => 500,
"expires_at" => now()->addYears(100)->format('d-m-Y'),
],
'expectedCount' => 0,
'assertValidationErrors' => true
],
'Invalid dataset (expires_at invalid format 1)' => [
'dataSet' => [
"memo" => Str::random(250),
"code" => Str::random(1),
"credits" => 99999999999,
"uses" => 500,
"expires_at" => now()->addYears(100)->format('Y-m-d'),
],
'expectedCount' => 0,
'assertValidationErrors' => true
],
'Invalid dataset (expires_at invalid value)' => [
'dataSet' => [
"memo" => Str::random(250),
"code" => Str::random(1),
"credits" => 99999999999,
"uses" => 500,
"expires_at" => Str::random(20),
],
'expectedCount' => 0,
'assertValidationErrors' => true
],
];
}
/**
* @return array[]
*/
public function accessibleRoutesDataProvider(): array
{
return [
'index page' => [
'method' => 'get',
'route' => '/admin/vouchers',
'expectedStatus' => 200
],
'Create page' => [
'method' => 'get',
'route' => '/admin/vouchers/create',
'expectedStatus' => 200
],
'Edit page' => [
'method' => 'get',
'route' => '/admin/vouchers/1/edit',
'expectedStatus' => 200
],
];
}
}