Browse Source

Merge pull request #115 from ControlPanel-gg/vouchers

Added a voucher system
AVMG 4 years ago
parent
commit
ae23c5cf3f

+ 1 - 1
app/Http/Controllers/Admin/UserController.php

@@ -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']),
         ]);

+ 187 - 0
app/Http/Controllers/Admin/VoucherController.php

@@ -0,0 +1,187 @@
+<?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\Support\Facades\Validator;
+use Illuminate\Validation\ValidationData;
+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'       => 'sometimes|string|max:191',
+            'code'       => 'required|string|alpha_dash|max:36',
+            'uses'       => 'required|numeric|max:2147483647',
+            'credits'    => 'required|numeric|between:0,99999999',
+            'expires_at' => 'nullable|date|after:1 hour',
+        ]);
+
+        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 Response
+     */
+    public function edit(Voucher $voucher)
+    {
+        //
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param Request $request
+     * @param Voucher $voucher
+     * @return Response
+     */
+    public function update(Request $request, Voucher $voucher)
+    {
+        //
+    }
+
+    /**
+     * 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 a voucher with this many credits"
+        ]);
+
+        #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="Show" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.vouchers.show', $voucher->id) . '" class="btn btn-sm text-white btn-warning mr-1"><i class="fas fa-eye"></i></a>
+                            <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();
+    }
+
+}

+ 47 - 0
app/Models/User.php

@@ -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();
@@ -93,20 +109,32 @@ class User extends Authenticatable implements MustVerifyEmail
         });
     }
 
+    /**
+     *
+     */
     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 +146,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 +157,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);

+ 78 - 0
app/Models/Voucher.php

@@ -0,0 +1,78 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsToMany;
+
+/**
+ * Class Voucher
+ * @package App\Models
+ */
+class Voucher extends Model
+{
+    use HasFactory;
+
+    /**
+     * @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 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
+     */
+    public function redeem(User $user){
+        try {
+            $user->increment('credits' , $this->credits);
+            $this->users()->attach($user);
+        }catch (\Exception $exception) {
+            throw $exception;
+        }
+
+        return $this->credits;
+    }
+
+    /**
+     * @return \Illuminate\Database\Eloquent\Relations\HasMany
+     */
+    public function users()
+    {
+        return $this->belongsToMany(User::class);
+    }
+}

+ 29 - 0
database/factories/VoucherFactory.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace Database\Factories;
+
+use App\Models\voucher;
+use Illuminate\Database\Eloquent\Factories\Factory;
+
+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 [
+            'credits'    => $this->faker->numberBetween(100, 1000),
+            'expires_at' => $this->faker->dateTimeBetween(now(), '+30 days')
+        ];
+    }
+}

+ 36 - 0
database/migrations/2021_07_09_190453_create_vouchers_table.php

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

+ 32 - 0
database/migrations/2021_07_09_191913_create_user_voucher_table.php

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

+ 32 - 0
database/migrations/2021_07_10_062140_update_credits_to_users_table.php

@@ -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();
+        });
+    }
+}

+ 1 - 1
resources/views/admin/users/edit.blade.php

@@ -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')

+ 161 - 0
resources/views/admin/vouchers/create.blade.php

@@ -0,0 +1,161 @@
+@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.products.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">
+                                    <label for="expires_at">Expires at</label>
+                                    <input value="{{old('expires_at')}}" id="expires_at" name="expires_at"
+                                           type="datetime-local"
+                                           class="form-control @error('expires_at') is-invalid @enderror">
+                                    @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>
+        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

+ 162 - 0
resources/views/admin/vouchers/edit.blade.php

@@ -0,0 +1,162 @@
+@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.products.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</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="999999"
+                                           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 *</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">
+                                    <label for="expires_at">Expires at</label>
+                                    <input value="{{ $voucher->expires_at }}" id="expires_at" name="expires_at"
+                                           type="datetime-local"
+                                           class="form-control @error('expires_at') is-invalid @enderror">
+                                    @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>
+        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

+ 94 - 0
resources/views/admin/vouchers/index.blade.php

@@ -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

+ 240 - 0
resources/views/admin/vouchers/show.blade.php

@@ -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

+ 20 - 1
resources/views/layouts/main.blade.php

@@ -96,6 +96,10 @@
                             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
@@ -253,6 +257,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-item">
                             <a href="{{route('admin.usefullinks.index')}}"
                                class="nav-link @if(Request::routeIs('admin.usefullinks.*')) active @endif">
@@ -261,7 +273,6 @@
                             </a>
                         </li>
 
-
                     @endif
 
                 </ul>
@@ -289,6 +300,8 @@
         @endif
 
         @yield('content')
+
+        @include('models.redeem_voucher_modal')
     </div>
     <!-- /.content-wrapper -->
     <footer class="main-footer">
@@ -319,6 +332,12 @@
 <script>
     $(document).ready(function () {
         $('[data-toggle="popover"]').popover();
+
+        $.ajaxSetup({
+            headers: {
+                'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
+            }
+        });
     });
 </script>
 <script>

+ 105 - 0
resources/views/models/redeem_voucher_modal.blade.php

@@ -0,0 +1,105 @@
+<!-- Button to Open the Modal -->
+<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#redeemVoucherModal">
+    Open modal
+</button>
+
+<!-- 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)
+            },
+            error : function (jqXHR, textStatus, errorThrown) {
+                resetForm()
+                redeemVoucherSetError(jqXHR.responseJSON)
+                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.errors.code[0]
+    }
+
+    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>

+ 12 - 0
routes/web.php

@@ -12,6 +12,7 @@ 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;
@@ -65,6 +66,14 @@ 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'])->name('voucher.redeem');
+
+    Route::get('/test' , function (Request $request) {
+        $voucher = \App\Models\Voucher::first();
+        dd($request->user()->vouchers()->where('id' , '=' , $voucher->id)->get()->isEmpty());
+    });
+
     #admin
     Route::prefix('admin')->name('admin.')->middleware('admin')->group(function () {
 
@@ -110,6 +119,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',