Feat: Let's implement the coupons now

This commit is contained in:
Ferks-FK 2023-05-17 16:17:51 +00:00 committed by IceToast
parent e03ac5dae0
commit 72f3decd3f
31 changed files with 1122 additions and 15 deletions

View file

@ -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
.gitignore vendored
View file

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

View file

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

View file

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

View file

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

View file

@ -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
app/Models/Coupon.php Normal file
View file

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

View file

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

View file

@ -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
app/Traits/Coupon.php Normal file
View file

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

0
bootstrap/cache/.gitignore vendored Normal file → Executable file
View file

View file

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

View file

@ -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,
],
/*

View file

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

View file

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

View file

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

2
package-lock.json generated
View file

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

View file

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

0
storage/app/.gitignore vendored Normal file → Executable file
View file

0
storage/app/public/.gitignore vendored Normal file → Executable file
View file

0
storage/debugbar/.gitignore vendored Normal file → Executable file
View file

0
storage/framework/.gitignore vendored Normal file → Executable file
View file

0
storage/framework/cache/.gitignore vendored Normal file → Executable file
View file

0
storage/framework/sessions/.gitignore vendored Normal file → Executable file
View file

0
storage/framework/testing/.gitignore vendored Normal file → Executable file
View file

0
storage/framework/views/.gitignore vendored Normal file → Executable file
View file

0
storage/logs/.gitignore vendored Normal file → Executable file
View file

View file

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

View file

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

View file

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

View file

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