瀏覽代碼

Feat: Let's implement the coupons now

Ferks-FK 2 年之前
父節點
當前提交
72f3decd3f

+ 23 - 2
.editorconfig

@@ -3,9 +3,9 @@ root = true
 [*]
 charset = utf-8
 end_of_line = lf
-insert_final_newline = true
+indent_size = 2
 indent_style = space
-indent_size = 4
+insert_final_newline = true
 trim_trailing_whitespace = true
 
 [*.md]
@@ -13,3 +13,24 @@ trim_trailing_whitespace = false
 
 [*.{yml,yaml}]
 indent_size = 2
+
+[docker-compose.yml]
+indent_size = 2
+
+[*.php]
+indent_size = 4
+
+[*.blade.php]
+indent_size = 2
+
+[*.js]
+indent_size = 4
+
+[*.jsx]
+indent_size = 2
+
+[*.tsx]
+indent_size = 2
+
+[*.json]
+indent_size = 4

+ 1 - 0
.gitignore

@@ -10,6 +10,7 @@ storage/debugbar
 .env.backup
 .idea
 .phpunit.result.cache
+.editorconfig
 docker-compose.override.yml
 Homestead.json
 Homestead.yaml

+ 22 - 3
app/Extensions/PaymentGateways/PayPal/PayPalExtension.php

@@ -10,6 +10,7 @@ use App\Models\PartnerDiscount;
 use App\Models\Payment;
 use App\Models\ShopProduct;
 use App\Models\User;
+use App\Models\Coupon;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Auth;
 use Illuminate\Support\Facades\Redirect;
@@ -41,6 +42,17 @@ class PayPalExtension extends AbstractExtension
         $user = Auth::user();
         $shopProduct = ShopProduct::findOrFail($request->shopProduct);
         $discount = PartnerDiscount::getDiscount();
+        $discountPrice = $request->get('discountPrice');
+
+        dd($discountPrice);
+
+        // Partner Discount.
+        $price = $shopProduct->price - ($shopProduct->price * $discount / 100);
+
+        // Coupon Discount.
+        // if ($discountPrice) {
+        //     $price = $price - ($price * floatval($coupon_percentage) / 100);
+        // }
 
         // create a new payment
         $payment = Payment::create([
@@ -50,7 +62,7 @@ class PayPalExtension extends AbstractExtension
             'type' => $shopProduct->type,
             'status' => 'open',
             'amount' => $shopProduct->quantity,
-            'price' => $shopProduct->price - ($shopProduct->price * $discount / 100),
+            'price' => $price,
             'tax_value' => $shopProduct->getTaxValue(),
             'tax_percent' => $shopProduct->getTaxPercent(),
             'total_price' => $shopProduct->getTotalPrice(),
@@ -73,7 +85,7 @@ class PayPalExtension extends AbstractExtension
                             'item_total' =>
                             [
                                 'currency_code' => strtoupper($shopProduct->currency_code),
-                                'value' => $shopProduct->getPriceAfterDiscount(),
+                                'value' => number_format($price, 2),
                             ],
                             'tax_total' =>
                             [
@@ -86,7 +98,7 @@ class PayPalExtension extends AbstractExtension
             ],
             "application_context" => [
                 "cancel_url" => route('payment.Cancel'),
-                "return_url" => route('payment.PayPalSuccess', ['payment' => $payment->id]),
+                "return_url" => route('payment.PayPalSuccess', ['payment' => $payment->id, 'couponCode' => $coupon_code]),
                 'brand_name' =>  config('app.name', 'CtrlPanel.GG'),
                 'shipping_preference'  => 'NO_SHIPPING'
             ]
@@ -126,6 +138,7 @@ class PayPalExtension extends AbstractExtension
 
         $payment = Payment::findOrFail($laravelRequest->payment);
         $shopProduct = ShopProduct::findOrFail($payment->shop_item_product_id);
+				$coupon_code = $laravelRequest->input('couponCode');
 
         $request = new OrdersCaptureRequest($laravelRequest->input('token'));
         $request->prefer('return=representation');
@@ -140,6 +153,12 @@ class PayPalExtension extends AbstractExtension
                     'payment_id' => $response->result->id,
                 ]);
 
+								// increase the use of the coupon when the payment is confirmed.
+								if ($coupon_code) {
+									$coupon = new Coupon;
+									$coupon->incrementUses($coupon_code);
+								}
+
                 event(new UserUpdateCreditsEvent($user));
                 event(new PaymentEvent($user, $payment, $shopProduct));
 

+ 159 - 0
app/Http/Controllers/Admin/CouponController.php

@@ -0,0 +1,159 @@
+<?php
+
+namespace App\Http\Controllers\Admin;
+
+use App\Http\Controllers\Controller;
+use App\Models\Coupon;
+use App\Traits\Coupon as CouponTrait;
+use Illuminate\Http\Request;
+use Carbon\Carbon;
+
+class CouponController extends Controller
+{
+    const READ_PERMISSION = "admin.coupons.read";
+    const WRITE_PERMISSION = "admin.coupons.write";
+
+    use CouponTrait;
+
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index()
+    {
+        $this->checkPermission(self::READ_PERMISSION);
+
+        return view('admin.coupons.index');
+    }
+
+    /**
+     * Show the form for creating a new resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function create()
+    {
+        $this->checkPermission(self::WRITE_PERMISSION);
+
+        return view('admin.coupons.create');
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        $coupon_code = $request->input('coupon_code');
+        $coupon_type = $request->input('coupon_type');
+        $coupon_value = $request->input('coupon_value');
+        $coupon_max_uses = $request->input('coupon_uses');
+        $coupon_datepicker = $request->input('datepicker');
+        $random_codes_amount = $request->input('range_codes');
+        $rules = [
+            "coupon_type" => "required|string|in:percentage,amount",
+            "coupon_uses" => "required|integer|digits_between:1,100",
+            "coupon_value" => "required|numeric|between:0,100",
+            "datepicker" => "required|date|after:" . Carbon::now()->format(Coupon::formatDate())
+        ];
+
+        // If for some reason you pass both fields at once.
+        if ($coupon_code && $random_codes_amount) {
+            return redirect()->back()->with('error', __('Only one of the two code inputs must be provided.'))->withInput($request->all());
+        }
+
+        if (!$coupon_code && !$random_codes_amount) {
+            return redirect()->back()->with('error', __('At least one of the two code inputs must be provided.'))->withInput($request->all());
+        }
+
+        if ($coupon_code) {
+            $rules['coupon_code'] = 'required|string|min:4';
+        } elseif ($random_codes_amount) {
+            $rules['range_codes'] = 'required|integer|digits_between:1,100';
+        }
+
+        $request->validate($rules);
+
+        if (array_key_exists('range_codes', $rules)) {
+            $data = [];
+            $coupons = Coupon::generateRandomCoupon($random_codes_amount);
+
+            // Scroll through all the randomly generated coupons.
+            foreach ($coupons as $coupon) {
+                $data[] = [
+                    'code' => $coupon,
+                    'type' => $coupon_type,
+                    'value' => $coupon_value,
+                    'max_uses' => $coupon_max_uses,
+                    'expires_at' => $coupon_datepicker,
+                    'created_at' => Carbon::now(), // Does not fill in by itself when using the 'insert' method.
+                    'updated_at' => Carbon::now()
+                ];
+            }
+            Coupon::insert($data);
+        } else {
+            Coupon::create([
+                'code' => $coupon_code,
+                'type' => $coupon_type,
+                'value' => $coupon_value,
+                'max_uses' => $coupon_max_uses,
+                'expires_at' => $coupon_datepicker,
+            ]);
+        }
+
+        return redirect()->route('admin.coupons.index')->with('success', __("The coupon's was registered successfully."));
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  \App\Models\Coupon  $coupon
+     * @return \Illuminate\Http\Response
+     */
+    public function show(Coupon $coupon)
+    {
+        //
+    }
+
+    /**
+     * Show the form for editing the specified resource.
+     *
+     * @param  \App\Models\Coupon  $coupon
+     * @return \Illuminate\Http\Response
+     */
+    public function edit(Coupon $coupon)
+    {
+        //
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\Coupon  $coupon
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, Coupon $coupon)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  \App\Models\Coupon  $coupon
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(Coupon $coupon)
+    {
+        //
+    }
+
+    public function redeem(Request $request)
+    {
+        return $this->validateCoupon($request);
+    }
+}

+ 30 - 0
app/Http/Controllers/Admin/PaymentController.php

@@ -9,6 +9,8 @@ use App\Models\PartnerDiscount;
 use App\Models\Payment;
 use App\Models\User;
 use App\Models\ShopProduct;
+use App\Models\Coupon;
+use App\Traits\Coupon as CouponTrait;
 use Exception;
 use Illuminate\Contracts\Foundation\Application;
 use Illuminate\Contracts\View\Factory;
@@ -20,11 +22,16 @@ use Illuminate\Support\Facades\Auth;
 use App\Helpers\ExtensionHelper;
 use App\Settings\GeneralSettings;
 use App\Settings\LocaleSettings;
+use Carbon\Carbon;
+use Illuminate\Support\Str;
 
 class PaymentController extends Controller
 {
     const BUY_PERMISSION = 'user.shop.buy';
     const VIEW_PERMISSION = "admin.payments.read";
+
+    use CouponTrait;
+
     /**
      * @return Application|Factory|View
      */
@@ -123,6 +130,8 @@ class PaymentController extends Controller
     {
         $product = ShopProduct::find($request->product_id);
         $paymentGateway = $request->payment_method;
+        $coupon_data = null;
+        $coupon_code = $request->coupon_code;
 
         // on free products, we don't need to use a payment gateway
         $realPrice = $product->price - ($product->price * PartnerDiscount::getDiscount() / 100);
@@ -130,6 +139,22 @@ class PaymentController extends Controller
             return $this->handleFreeProduct($product);
         }
 
+        if ($coupon_code) {
+            $isValidCoupon = $this->validateCoupon($request);
+
+            if ($isValidCoupon->getStatusCode() == 200) {
+                $coupon_data = $isValidCoupon;
+                $discountPrice = $this->calcDiscount($product, $isValidCoupon->getData());
+            }
+        }
+
+        if ($coupon_data) {
+            return redirect()->route('payment.' . $paymentGateway . 'Pay', [
+                'shopProduct' => $product->id,
+                'discountPrice' => $discountPrice
+            ]);
+        }
+
         return redirect()->route('payment.' . $paymentGateway . 'Pay', ['shopProduct' => $product->id]);
     }
 
@@ -141,6 +166,11 @@ class PaymentController extends Controller
         return redirect()->route('store.index')->with('info', 'Payment was Canceled');
     }
 
+    protected function getCouponDiscount(float $productPrice, string $discount)
+    {
+        return $productPrice - ($productPrice * $discount / 100);
+    }
+
     /**
      * @return JsonResponse|mixed
      *

+ 1 - 2
app/Http/Controllers/ProfileController.php

@@ -34,8 +34,7 @@ class ProfileController extends Controller
             'force_discord_verification' => $user_settings->force_discord_verification,
             'discord_client_id' => $discord_settings->client_id,
             'discord_client_secret' => $discord_settings->client_secret,
-            'referral_enabled' => $referral_settings->enabled,
-            'referral_allowed' => $referral_settings->allowed
+            'referral_enabled' => $referral_settings->enabled
         ]);
     }
 

+ 162 - 0
app/Models/Coupon.php

@@ -0,0 +1,162 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Carbon\Carbon;
+
+class Coupon extends Model
+{
+    use HasFactory;
+
+    protected $fillable = [
+        'code',
+        'type',
+        'value',
+        'uses',
+        'max_uses',
+        'expires_at'
+    ];
+
+    /**
+     * Returns the date format used by the coupons.
+     *
+     * @return string
+     */
+    public static function formatDate(): string
+    {
+        return 'Y-MM-DD HH:mm:ss';
+    }
+
+    /**
+     * Returns the current state of the coupon.
+     *
+     * @return string
+     */
+    public function getStatus()
+    {
+        if ($this->uses >= $this->max_uses) {
+            return 'USES_LIMIT_REACHED';
+        }
+
+        if (! is_null($this->expires_at)) {
+            $expires_at = Carbon::createFromTimeString($this->expires_at)->timestamp;
+
+            if ($expires_at <= Carbon::now()->timestamp) {
+                return __('EXPIRED');
+            }
+        }
+
+        return __('VALID');
+    }
+
+    /**
+     * Check if a user has already exceeded the uses of a coupon.
+     *
+     * @param Request $request The request being made.
+     * @param CouponSettings $coupon_settings The instance of the coupon settings.
+     *
+     * @return bool
+     */
+    public function isLimitsUsesReached($request, $coupon_settings): bool
+    {
+        $coupon_uses = $request->user()->coupons()->where('id', $this->id)->count();
+
+        return $coupon_uses >= $coupon_settings->max_uses_per_user ? true : false;
+    }
+
+    /**
+     * Generate a specified quantity of coupon codes.
+     *
+     * @param int $amount Amount of coupons to be generated.
+     *
+     * @return array
+     */
+    public static function generateRandomCoupon(int $amount = 10): array
+    {
+        $coupons = [];
+
+        for ($i = 0; $i < $amount; $i++) {
+            $random_coupon = strtoupper(bin2hex(random_bytes(3)));
+
+            $coupons[] = $random_coupon;
+        }
+
+        return $coupons;
+    }
+
+    /**
+     * Standardize queries into one single function.
+     *
+     * @param string $code Coupon Code.
+     * @param array $attributes Attributes to be returned.
+     *
+     * @return mixed
+     */
+    protected function getQueryData(string $code, array $attributes): mixed
+    {
+        $query = (Coupon::where('code', $code)
+            ->where('expires_at', '>', Carbon::now())
+            ->whereColumn('uses', '<=', 'max_uses')
+            ->get($attributes)->toArray()
+        );
+
+        // When there are results, it comes nested arrays, idk why. This is the solution for now.
+        $results = count($query) > 0 ? $query[0] : $query;
+
+        if (empty($results)) {
+            return [];
+        }
+
+        return $results;
+    }
+
+    /**
+     * Get the data from a coupon.
+     *
+     * @param string $code Coupon Code.
+     * @param array $attributes Attributes of a coupon.
+     *
+     * @return mixed
+     */
+    public function getCoupon(string $code, array $attributes = ['percentage']): mixed
+    {
+        $coupon = $this->getQueryData($code, $attributes);
+
+        if (is_null($coupon)) {
+            return null;
+        }
+
+        return $coupon;
+    }
+
+    /**
+     * Increments the use of a coupon.
+     *
+     * @param string $code Coupon Code.
+     * @param int $amount Amount to increment.
+     *
+     * @return null|bool
+     */
+    public function incrementUses(string $code, int $amount = 1): null|bool
+    {
+        $coupon = $this->getQueryData($code, ['uses', 'max_uses']);
+
+        if (empty($coupon) || $coupon['uses'] == $coupon['max_uses']) {
+            return null;
+        }
+
+        $this->where('code', $code)->increment('uses', $amount);
+
+        return true;
+    }
+
+    /**
+     * @return BelongsToMany
+     */
+    public function users()
+    {
+        return $this->belongsToMany(User::class, 'user_coupons');
+    }
+}

+ 8 - 2
app/Models/User.php

@@ -4,8 +4,6 @@ namespace App\Models;
 
 use App\Notifications\Auth\QueuedVerifyEmail;
 use App\Notifications\WelcomeMessage;
-use App\Settings\GeneralSettings;
-use App\Settings\UserSettings;
 use App\Classes\PterodactylClient;
 use App\Settings\PterodactylSettings;
 use Illuminate\Contracts\Auth\MustVerifyEmail;
@@ -170,6 +168,14 @@ class User extends Authenticatable implements MustVerifyEmail
         return $this->belongsToMany(Voucher::class);
     }
 
+    /**
+     * @return BelongsToMany
+     */
+    public function coupons()
+    {
+        return $this->belongsToMany(Coupon::class, 'user_coupons');
+    }
+
     /**
      * @return HasOne
      */

+ 43 - 0
app/Settings/CouponSettings.php

@@ -0,0 +1,43 @@
+<?php
+
+namespace App\Settings;
+
+use Spatie\LaravelSettings\Settings;
+
+class CouponSettings extends Settings
+{
+	public ?int $max_uses_per_user;
+
+    public static function group(): string
+    {
+        return 'coupon';
+    }
+
+		/**
+     * Summary of validations array
+     * @return array<string, string>
+     */
+    public static function getValidations()
+    {
+        return [
+            'max_uses_per_user' => 'required|integer'
+        ];
+    }
+
+		/**
+     * Summary of optionInputData array
+     * Only used for the settings page
+     * @return array<array<'type'|'label'|'description'|'options', string|bool|float|int|array<string, string>>>
+     */
+    public static function getOptionInputData()
+    {
+        return [
+            "category_icon" => "fas fa-ticket-alt",
+            'max_uses_per_user' => [
+                'label' => 'Max Uses Per User',
+                'type' => 'number',
+                'description' => 'Maximum number of uses that a user can make of the same coupon.',
+            ]
+        ];
+    }
+}

+ 86 - 0
app/Traits/Coupon.php

@@ -0,0 +1,86 @@
+<?php
+
+namespace App\Traits;
+
+use App\Settings\CouponSettings;
+use App\Models\Coupon as CouponModel;
+use App\Models\ShopProduct;
+use Illuminate\Http\Request;
+use Illuminate\Http\JsonResponse;
+use stdClass;
+
+trait Coupon
+{
+    public function validateCoupon(Request $request): JsonResponse
+    {
+        $coupon = CouponModel::where('code', $request->input('coupon_code'))->first();
+        $coupon_settings = new CouponSettings;
+        $response = response()->json([
+            'isValid' => false,
+            'error' => __('This coupon does not exist.')
+        ], 404);
+
+        if (!is_null($coupon)) {
+            if ($coupon->getStatus() == 'USES_LIMIT_REACHED') {
+                $response = response()->json([
+                    'isValid' => false,
+                    'error' => __('This coupon has reached the maximum amount of uses.')
+                ], 422);
+
+                return $response;
+            }
+
+            if ($coupon->getStatus() == 'EXPIRED') {
+                $response = response()->json([
+                    'isValid' => false,
+                    'error' => __('This coupon has expired.')
+                ], 422);
+
+                return $response;
+            }
+
+            if ($coupon->isLimitsUsesReached($request, $coupon_settings)) {
+                $response = response()->json([
+                    'isValid' => false,
+                    'error' => __('You have reached the maximum uses of this coupon.')
+                ], 422);
+
+                return $response;
+            }
+
+            $response = response()->json([
+                'isValid' => true,
+                'couponCode' => $coupon->code,
+                'couponType' => $coupon->type,
+                'couponValue' => $coupon->value
+            ]);
+        }
+
+        return $response;
+    }
+
+    public function calcDiscount(ShopProduct $product, stdClass $data)
+    {
+
+        if ($data->isValid) {
+            $productPrice = $product->price;
+
+            if (is_string($productPrice)) {
+                $productPrice = floatval($product->price);
+            }
+
+            if ($data->couponType === 'percentage') {
+                return $productPrice - ($productPrice * $data->couponValue / 100);
+            }
+
+            if ($data->couponType === 'amount') {
+                // There is no discount if the value of the coupon is greater than or equal to the value of the product.
+                if ($data->couponValue >= $productPrice) {
+                    return $productPrice;
+                }
+            }
+
+            return $productPrice - $data->couponValue;
+        }
+    }
+}

+ 3 - 0
bootstrap/cache/.gitignore

@@ -74,6 +74,9 @@ return [
     'admin.partners.read',
     'admin.partners.write',
 
+    'admin.coupons.read',
+    'admin.coupons.write',
+
     'admin.logs.read',
 
     /*

+ 2 - 0
config/settings.php

@@ -12,6 +12,7 @@ use App\Settings\ServerSettings;
 use App\Settings\UserSettings;
 use App\Settings\WebsiteSettings;
 use App\Settings\TicketSettings;
+use App\Settings\CouponSettings;
 
 return [
 
@@ -31,6 +32,7 @@ return [
         UserSettings::class,
         WebsiteSettings::class,
         TicketSettings::class,
+				CouponSettings::class,
     ],
 
     /*

+ 37 - 0
database/migrations/2023_05_11_153719_create_coupons_table.php

@@ -0,0 +1,37 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('coupons', function (Blueprint $table) {
+            $table->id();
+            $table->string('code')->unique();
+            $table->enum('type', ['percentage', 'amount']);
+            $table->integer('value');
+            $table->integer('uses')->default(0);
+            $table->integer('max_uses');
+            $table->timestamp('expires_at');
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('coupons');
+    }
+};

+ 32 - 0
database/migrations/2023_05_14_152604_create_user_coupons_table.php

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

+ 16 - 0
database/settings/2023_05_12_170041_create_coupon_settings.php

@@ -0,0 +1,16 @@
+<?php
+
+use Spatie\LaravelSettings\Migrations\SettingsMigration;
+
+return new class extends SettingsMigration
+{
+    public function up(): void
+    {
+        $this->migrator->add('coupon.max_uses_per_user', 1);
+    }
+
+    public function down(): void
+    {
+        $this->migrator->delete('coupon.max_uses_per_user');
+    }
+};

+ 1 - 1
package-lock.json

@@ -1,5 +1,5 @@
 {
-    "name": "controllpanelgg",
+    "name": "controlpanel",
     "lockfileVersion": 2,
     "requires": true,
     "packages": {

+ 5 - 0
routes/web.php

@@ -22,6 +22,7 @@ use App\Http\Controllers\Admin\TicketsController as AdminTicketsController;
 use App\Http\Controllers\Admin\UsefulLinkController;
 use App\Http\Controllers\Admin\UserController;
 use App\Http\Controllers\Admin\VoucherController;
+use App\Http\Controllers\Admin\CouponController;
 use App\Http\Controllers\Auth\SocialiteController;
 use App\Http\Controllers\HomeController;
 use App\Http\Controllers\NotificationController;
@@ -199,6 +200,10 @@ Route::middleware(['auth', 'checkSuspended'])->group(function () {
         Route::get('partners/{voucher}/users', [PartnerController::class, 'users'])->name('partners.users');
         Route::resource('partners', PartnerController::class);
 
+        //coupons
+        Route::post('coupons/redeem', [CouponController::class, 'redeem'])->name('coupon.redeem');
+        Route::resource('coupons', CouponController::class);
+
         //api-keys
         Route::get('api/datatable', [ApplicationApiController::class, 'datatable'])->name('api.datatable');
         Route::resource('api', ApplicationApiController::class)->parameters([

+ 289 - 0
storage/app/.gitignore

@@ -0,0 +1,289 @@
+@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>{{__('Coupon')}}</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.coupons.index')}}">{{__('Coupons')}}</a>
+                    </li>
+                    <li class="breadcrumb-item"><a class="text-muted"
+                                                   href="{{route('admin.coupons.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="nav-icon fas fa-ticket-alt"></i>
+                {{__('Coupon Details')}}
+              </h5>
+            </div>
+            <div class="card-body">
+              <form action="{{ route('admin.coupons.store') }}" method="POST">
+                @csrf
+
+                <div class="d-flex flex-row-reverse">
+                  <div class="custom-control custom-switch">
+                    <input
+                      type="checkbox"
+                      id="random_codes"
+                      name="random_codes"
+                      class="custom-control-input"
+                    >
+                    <label for="random_codes" class="custom-control-label">
+                      {{ __('Random Codes') }}
+                      <i
+                        data-toggle="popover"
+                        data-trigger="hover"
+                        data-content="{{__('Replace the creation of a single code with several at once with a custom field.')}}"
+                        class="fas fa-info-circle">
+                      </i>
+                    </label>
+                  </div>
+                </div>
+                <div id="range_codes_element" style="display: none;" class="form-group">
+                  <label for="range_codes">
+                    {{ __('Range Codes') }}
+                    <i
+                      data-toggle="popover"
+                      data-trigger="hover"
+                      data-content="{{__('Generate a number of random codes.')}}"
+                      class="fas fa-info-circle">
+                    </i>
+                  </label>
+                  <input
+                    type="number"
+                    id="range_codes"
+                    name="range_codes"
+                    step="any"
+                    min="1"
+                    max="100"
+                    class="form-control @error('range_codes') is-invalid @enderror"
+                  >
+                  @error('range_codes')
+                    <div class="text-danger">
+                      {{ $message }}
+                    </div>
+                  @enderror
+                </div>
+                <div id="coupon_code_element" class="form-group">
+                  <label for="coupon_code">
+                    {{ __('Coupon Code') }}
+                    <i
+                      data-toggle="popover"
+                      data-trigger="hover"
+                      data-content="{{__('The coupon code to be registered.')}}"
+                      class="fas fa-info-circle">
+                    </i>
+                  </label>
+                  <input
+                    type="text"
+                    id="coupon_code"
+                    name="coupon_code"
+                    placeholder="SUMMER"
+                    class="form-control @error('coupon_code') is-invalid @enderror"
+                    value="{{ old('coupon_code') }}"
+                  >
+                  @error('coupon_code')
+                    <div class="text-danger">
+                      {{ $message }}
+                    </div>
+                  @enderror
+                </div>
+                <div class="form-group">
+                  <div class="custom-control mb-3 p-0">
+                    <label for="coupon_type">
+                      {{ __('Coupon Type') }}
+                      <i
+                        data-toggle="popover"
+                        data-trigger="hover"
+                        data-content="{{__('The way the coupon should discount.')}}"
+                        class="fas fa-info-circle">
+                      </i>
+                    </label>
+                    <select
+                      name="coupon_type"
+                      id="coupon_type"
+                      class="custom-select @error('coupon_type') is_invalid @enderror"
+                      style="width: 100%; cursor: pointer;"
+                      autocomplete="off"
+                      required
+                    >
+                      <option value="percentage" @if(old('coupon_type') == 'percentage') selected @endif>{{ __('Percentage') }}</option>
+                      <option value="amount" @if(old('coupon_type') == 'amount') selected @endif>{{ __('Amount') }}</option>
+                    </select>
+                    @error('coupon_type')
+                      <div class="text-danger">
+                        {{ $message }}
+                      </div>
+                    @enderror
+                  </div>
+                </div>
+                <div class="form-group">
+                  <div class="input-group d-flex flex-column">
+                    <label for="coupon_value">
+                      {{ __('Coupon Value') }}
+                      <i
+                        data-toggle="popover"
+                        data-trigger="hover"
+                        data-content="{{__('The value that the coupon will represent.')}}"
+                        class="fas fa-info-circle">
+                      </i>
+                    </label>
+                    <div class="d-flex">
+                      <input
+                        name="coupon_value"
+                        id="coupon_value"
+                        type="number"
+                        step="any"
+                        min="1"
+                        max="100"
+                        class="form-control @error('coupon_value') is-invalid @enderror"
+                        value="{{ old('coupon_value') }}"
+                      >
+                      <span id="input_percentage" class="input-group-text">%</span>
+                    </div>
+                    @error('coupon_value')
+                      <div class="text-danger">
+                        {{ $message }}
+                      </div>
+                    @enderror
+                  </div>
+                </div>
+                <div class="form-group">
+                  <label for="coupon_uses">
+                    {{ __('Max uses') }}
+                    <i
+                      data-toggle="popover"
+                      data-trigger="hover"
+                      data-content="{{__('The maximum number of times the coupon can be used.')}}"
+                      class="fas fa-info-circle">
+                    </i>
+                  </label>
+                  <input
+                    name="coupon_uses"
+                    id="coupon_uses"
+                    type="number"
+                    step="any"
+                    min="1"
+                    max="100"
+                    class="form-control @error('coupon_uses') is-invalid @enderror"
+                    value="{{ old('coupon_uses') }}"
+                  >
+                  @error('coupon_uses')
+                    <div class="text-danger">
+                      {{ $message }}
+                    </div>
+                  @enderror
+                </div>
+                <div class="d-flex flex-column input-group form-group date" id="datepicker" data-target-input="nearest">
+                  <label for="datepicker">
+                    {{ __('Expires at') }}
+                    <i
+                      data-toggle="popover"
+                      data-trigger="hover"
+                      data-content="{{__('The date when the coupon will expire.')}}"
+                      class="fas fa-info-circle">
+                    </i>
+                  </label>
+                  <div class="d-flex">
+                    <input
+                      value="{{old('datepicker')}}"
+                      name="datepicker"
+                      placeholder="yyyy-mm-dd hh:mm:ss"
+                      type="text"
+                      class="form-control @error('datepicker') is-invalid @enderror datetimepicker-input"
+                      data-target="#datepicker"
+                    />
+                    <div
+                      class="input-group-append"
+                      data-target="#datepicker"
+                      data-toggle="datetimepicker"
+                    >
+                      <div class="input-group-text">
+                        <i class="fa fa-calendar"></i>
+                      </div>
+                    </div>
+                  </div>
+                  @error('datepicker')
+                    <div class="text-danger">
+                      {{ $message }}
+                    </div>
+                  @enderror
+                </div>
+                <div class="form-group text-right mb-0">
+                  <button type="submit" class="btn btn-primary">
+                    {{__('Submit')}}
+                  </button>
+                </div>
+              </form>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </section>
+   <!-- END CONTENT -->
+
+  <script>
+    $(document).ready(function() {
+      $('#datepicker').datetimepicker({
+        format: 'Y-MM-DD HH:mm:ss',
+        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'
+        }
+      });
+      $('#random_codes').change(function() {
+        if ($(this).is(':checked')) {
+          $('#coupon_code_element').prop('disabled', true).hide()
+          $('#range_codes_element').prop('disabled', false).show()
+
+          if ($('#coupon_code').val()) {
+            $('#coupon_code').prop('value', null)
+          }
+
+        } else {
+          $('#coupon_code_element').prop('disabled', false).show()
+          $('#range_codes_element').prop('disabled', true).hide()
+
+          if ($('#range_codes').val()) {
+            $('#range_codes').prop('value', null)
+          }
+        }
+      })
+
+      $('#coupon_type').change(function() {
+        if ($(this).val() == 'percentage') {
+          $('#input_percentage').prop('disabled', false).show()
+        } else {
+          $('#input_percentage').prop('disabled', true).hide()
+        }
+      })
+    })
+  </script>
+@endsection

+ 65 - 0
themes/default/views/admin/coupons/index.blade.php

@@ -0,0 +1,65 @@
+@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>{{__('Coupons')}}</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.coupons.index')}}">{{__('Coupons')}}</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="nav-icon fas fa-ticket-alt"></i>
+                      {{__('Coupons')}}
+                    </h5>
+                    <a href="{{route('admin.coupons.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>{{__('Partner discount')}}</th>
+                        <th>{{__('Registered user discount')}}</th>
+                        <th>{{__('Referral system commission')}}</th>
+                        <th>{{__('Created')}}</th>
+                        <th>{{__('Actions')}}</th>
+                    </tr>
+                    </thead>
+                    <tbody>
+                    </tbody>
+                </table>
+
+            </div>
+        </div>
+
+
+    </div>
+    <!-- END CUSTOM CONTENT -->
+
+</section>
+<!-- END CONTENT -->
+@endsection

+ 12 - 2
themes/default/views/layouts/main.blade.php

@@ -424,7 +424,7 @@
                                 </a>
                             </li>
                         @endcanany
-                        @canany(["admin.voucher.read","admin.voucher.read"])
+                        @canany(["admin.voucher.read","admin.voucher.write"])
                             <li class="nav-item">
                                 <a href="{{ route('admin.vouchers.index') }}"
                                     class="nav-link @if (Request::routeIs('admin.vouchers.*')) active @endif">
@@ -433,7 +433,7 @@
                                 </a>
                             </li>
                         @endcanany
-                        @canany(["admin.partners.read","admin.partners.read"])
+                        @canany(["admin.partners.read","admin.partners.write"])
                             <li class="nav-item">
                                 <a href="{{ route('admin.partners.index') }}"
                                     class="nav-link @if (Request::routeIs('admin.partners.*')) active @endif">
@@ -443,6 +443,16 @@
                             </li>
                         @endcanany
 
+												@canany(["admin.coupons.read", "admin.coupons.write"])
+                            <li class="nav-item">
+                                <a href="{{ route('admin.coupons.index') }}"
+                                    class="nav-link @if (Request::routeIs('admin.coupons.*')) active @endif">
+                                    <i class="nav-icon fas fa-ticket-alt"></i>
+                                    <p>{{ __('Coupons') }}</p>
+                                </a>
+                            </li>
+                        @endcanany
+
                             @canany(["admin.useful_links.read","admin.legal.read"])
                                 <li class="nav-header">{{ __('Other') }}</li>
                             @endcanany

+ 125 - 3
themes/default/views/store/checkout.blade.php

@@ -24,7 +24,19 @@
     <!-- MAIN CONTENT -->
     <section class="content">
         <div class="container-fluid">
-            <form x-data="{ payment_method: '', clicked: false }" action="{{ route('payment.pay') }}" method="POST">
+            <form
+							id="payment_form"
+              action="{{ route('payment.pay') }}"
+              method="POST"
+              x-data="{
+                payment_method: '',
+                coupon_code: '',
+                clicked: false,
+                setCouponCode(event) {
+                  this.coupon_code = event.target.value
+                }
+              }"
+            >
                 @csrf
                 @method('post')
                 <div class="row d-flex justify-content-center flex-wrap">
@@ -67,6 +79,45 @@
                                 </div>
                             </div>
                         </div>
+												<div class="col-xl-4">
+													<div class="card">
+														<div class="card-header">
+															<h4 class="mb-0">
+																Coupon Code
+															</h4>
+														</div>
+														<div class="card-body">
+															<div class="d-flex">
+                                <input
+                                  type="text"
+                                  id="coupon_code"
+                                  name="coupon_code"
+                                  value="{{ old('coupon_code') }}"
+                                  :value="coupon_code"
+                                  class="form-control @error('coupon_code') is_invalid @enderror"
+                                  placeholder="SUMMER"
+                                  x-on:change.debounce="setCouponCode($event)"
+                                  x-model="coupon_code"
+                                />
+                              <button
+                                type="button"
+                                id="send_coupon_code"
+                                class="btn btn-success ml-3"
+                                :disabled="!coupon_code.length"
+                                :class="!coupon_code.length ? 'disabled' : ''"
+                                :value="coupon_code"
+                              >
+                                {{ __('Submit') }}
+                              </button>
+                              </div>
+                              @error('coupon_code')
+                                <div class="text-danger">
+                                  {{ $message }}
+                                </div>
+                              @enderror
+														</div>
+													</div>
+												</div>
                     @endif
                     <div class="col-xl-3">
                         <div class="card">
@@ -131,6 +182,12 @@
                                                 <span class="text-muted d-inline-block">
                                                     + {{ $product->formatToCurrency($taxvalue) }}</span>
                                             </div>
+                                            <div id="coupon_discount_details" class="d-flex justify-content-between" style="display: none !important;">
+                                              <span class="text-muted d-inline-block">
+                                                {{ __('Coupon Discount') }}
+
+                                              </span>
+                                            </div>
                                             @if ($discountpercent && $discountvalue)
                                                 <div class="d-flex justify-content-between">
                                                     <span class="text-muted d-inline-block">{{ __('Discount') }}
@@ -155,9 +212,12 @@
                                     </li>
                                 </ul>
 
-                                <button :disabled="(!payment_method || !clicked) && {{ !$productIsFree }}"
-                                    :class="(!payment_method || !clicked) && {{ !$productIsFree }} ? 'disabled' : ''"
+                                <button :disabled="(!payment_method || !clicked || coupon_code ? true : false) && {{ !$productIsFree }}"
+                                    id="submit_form_button"
+                                    :class="(!payment_method || !clicked || coupon_code ? true : false) && {{ !$productIsFree }} ? 'disabled' : ''"
+                                    :x-text="coupon_code"
                                     class="btn btn-success float-right w-100">
+
                                     <i class="far fa-credit-card mr-2" @click="clicked == true"></i>
                                     @if ($productIsFree)
                                         {{ __('Get for free') }}
@@ -166,6 +226,8 @@
                                     @endif
 
                                 </button>
+                                <script>
+                                </script>
                             </div>
                         </div>
                     </div>
@@ -175,4 +237,64 @@
 
     </section>
     <!-- END CONTENT -->
+
+    <script>
+      $(document).ready(function() {
+        let hasCouponCodeValue = $('#coupon_code').val().trim() !== ''
+
+        $('#coupon_code').on('change', function(e) {
+          hasCouponCodeValue = e.target.value !== ''
+        })
+
+				function checkCoupon() {
+					const couponCode = $('#coupon_code').val()
+
+					$.ajax({
+						url: "{{ route('admin.coupon.redeem') }}",
+						method: 'POST',
+						data: { coupon_code: couponCode },
+						success: function(response) {
+							if (response.isValid && response.couponCode) {
+                Swal.fire({
+                  icon: 'success',
+                  text: `The coupon '${response.couponCode}' was successfully inserted in your purchase.`,
+                }).then(function(isConfirmed) {
+                  console.log('confirmou')
+
+                  $('#submit_form_button').prop('disabled', false).removeClass('disabled')
+                  $('#send_coupon_code').prop('disabled', true)
+                  $('#coupon_discount_details').prop('disabled', false).show()
+                })
+
+							} else {
+								console.log('Invalid Coupon')
+							}
+						},
+						error: function(response) {
+              const responseJson = response.responseJSON
+
+              if (!responseJson.isValid) {
+                  Swal.fire({
+                  icon: 'error',
+                  title: 'Oops...',
+                  text: responseJson.error,
+                })
+              }
+						}
+					})
+				}
+
+				$('#payment_form').on('submit', function(e) {
+					if (hasCouponCodeValue) {
+						checkCoupon()
+					}
+				})
+
+        $('#send_coupon_code').click(function(e) {
+          if (hasCouponCodeValue) {
+						checkCoupon()
+					}
+        })
+      })
+    </script>
 @endsection