diff --git a/app/Console/Commands/DeleteExpiredCoupons.php b/app/Console/Commands/DeleteExpiredCoupons.php new file mode 100644 index 00000000..9d3ded87 --- /dev/null +++ b/app/Console/Commands/DeleteExpiredCoupons.php @@ -0,0 +1,42 @@ +delete_coupon_on_expires) { + $expired_coupons = Coupon::where('expires_at', '<=', Carbon::now(config('app.timezone')))->get(); + + foreach ($expired_coupons as $expired_coupon) { + $expired_coupon->delete(); + } + } + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 64af565f..a1bd37d2 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -16,6 +16,7 @@ class Kernel extends ConsoleKernel protected $commands = [ Commands\ChargeCreditsCommand::class, Commands\ChargeServers::class, + Commands\DeleteExpiredCoupons::class, ]; /** @@ -29,6 +30,7 @@ class Kernel extends ConsoleKernel $schedule->command('servers:charge')->everyMinute(); $schedule->command('cp:versioncheck:get')->daily(); $schedule->command('payments:open:clear')->daily(); + $schedule->command('coupons:delete')->daily(); //log cronjob activity $schedule->call(function () { diff --git a/app/Events/CouponUsedEvent.php b/app/Events/CouponUsedEvent.php new file mode 100644 index 00000000..49168b60 --- /dev/null +++ b/app/Events/CouponUsedEvent.php @@ -0,0 +1,25 @@ +coupon = $coupon; + } +} diff --git a/app/Extensions/PaymentGateways/PayPal/PayPalExtension.php b/app/Extensions/PaymentGateways/PayPal/PayPalExtension.php index 7667c08d..07771e48 100644 --- a/app/Extensions/PaymentGateways/PayPal/PayPalExtension.php +++ b/app/Extensions/PaymentGateways/PayPal/PayPalExtension.php @@ -2,6 +2,7 @@ namespace App\Extensions\PaymentGateways\PayPal; +use App\Events\CouponUsedEvent; use App\Helpers\AbstractExtension; use App\Events\PaymentEvent; use App\Events\UserUpdateCreditsEvent; @@ -56,6 +57,7 @@ class PayPalExtension extends AbstractExtension // Partner Discount. $price = $price - ($price * $discount / 100); + $price = number_format($price, 2); // create a new payment $payment = Payment::create([ @@ -82,12 +84,12 @@ class PayPalExtension extends AbstractExtension "reference_id" => uniqid(), "description" => $shopProduct->display . ($discount ? (" (" . __('Discount') . " " . $discount . '%)') : ""), "amount" => [ - "value" => $shopProduct->getTotalPrice(), + "value" => $price, 'currency_code' => strtoupper($shopProduct->currency_code), 'breakdown' => [ 'item_total' => [ 'currency_code' => strtoupper($shopProduct->currency_code), - 'value' => number_format($price, 2), + 'value' => $price ], 'tax_total' => [ 'currency_code' => strtoupper($shopProduct->currency_code), @@ -158,6 +160,8 @@ class PayPalExtension extends AbstractExtension if ($coupon_code) { $coupon = new Coupon; $coupon->incrementUses($coupon_code); + + event(new CouponUsedEvent($coupon)); } event(new UserUpdateCreditsEvent($user)); diff --git a/app/Http/Controllers/Admin/CouponController.php b/app/Http/Controllers/Admin/CouponController.php index 396e2936..1287a57c 100644 --- a/app/Http/Controllers/Admin/CouponController.php +++ b/app/Http/Controllers/Admin/CouponController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use App\Models\Coupon; +use App\Settings\LocaleSettings; use App\Traits\Coupon as CouponTrait; use Illuminate\Http\Request; use Carbon\Carbon; @@ -20,11 +21,13 @@ class CouponController extends Controller * * @return \Illuminate\Http\Response */ - public function index() + public function index(LocaleSettings $localeSettings) { $this->checkPermission(self::READ_PERMISSION); - return view('admin.coupons.index'); + return view('admin.coupons.index', [ + 'locale_datatables' => $localeSettings->datatables + ]); } /** @@ -47,21 +50,12 @@ class CouponController extends Controller */ 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'); + $coupon_code = $request->input('code'); $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()) - ]; + $rules = $this->requestRules($request); - // If for some reason you pass both fields at once. - if ($coupon_code && $random_codes_amount) { + // 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()); } @@ -69,12 +63,6 @@ class CouponController extends Controller 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)) { @@ -85,23 +73,17 @@ class CouponController extends Controller foreach ($coupons as $coupon) { $data[] = [ 'code' => $coupon, - 'type' => $coupon_type, - 'value' => $coupon_value, - 'max_uses' => $coupon_max_uses, - 'expires_at' => $coupon_datepicker, + 'type' => $request->input('type'), + 'value' => $request->input('value'), + 'max_uses' => $request->input('max_uses'), + 'expires_at' => $request->input('expires_at'), '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, - ]); + Coupon::create($request->except('_token')); } return redirect()->route('admin.coupons.index')->with('success', __("The coupon's was registered successfully.")); @@ -126,7 +108,12 @@ class CouponController extends Controller */ public function edit(Coupon $coupon) { - // + $this->checkPermission(self::WRITE_PERMISSION); + + return view('admin.coupons.edit', [ + 'coupon' => $coupon, + 'expired_at' => $coupon->expires_at ? Carbon::createFromTimestamp($coupon->expires_at) : null + ]); } /** @@ -138,7 +125,23 @@ class CouponController extends Controller */ public function update(Request $request, Coupon $coupon) { - // + $coupon_code = $request->input('code'); + $random_codes_amount = $request->input('range_codes'); + $rules = $this->requestRules($request); + + // 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()); + } + + $request->validate($rules); + $coupon->update($request->except('_token')); + + return redirect()->route('admin.coupons.index')->with('success', __('coupon has been updated!')); } /** @@ -149,11 +152,87 @@ class CouponController extends Controller */ public function destroy(Coupon $coupon) { - // + $this->checkPermission(self::WRITE_PERMISSION); + $coupon->delete(); + + return redirect()->back()->with('success', __('coupon has been removed!')); + } + + private function requestRules(Request $request) + { + $coupon_code = $request->input('code'); + $random_codes_amount = $request->input('range_codes'); + $rules = [ + "type" => "required|string|in:percentage,amount", + "max_uses" => "required|integer|digits_between:1,100", + "value" => "required|numeric|between:0,100", + "expires_at" => "nullable|date|after:" . Carbon::now()->format(Coupon::formatDate()) + ]; + + if ($coupon_code) { + $rules['code'] = "required|string|min:4"; + } elseif ($random_codes_amount) { + $rules['range_codes'] = 'required|integer|digits_between:1,100'; + } + + return $rules; } public function redeem(Request $request) { return $this->validateCoupon($request->user(), $request->input('couponCode'), $request->input('productId')); } + + public function dataTable() + { + $query = Coupon::query(); + + return datatables($query) + ->addColumn('actions', function(Coupon $coupon) { + return ' + + +
+ '.csrf_field().' + '.method_field('DELETE').' + +
+ '; + }) + ->addColumn('status', function(Coupon $coupon) { + $color = 'success'; + $status = $coupon->getStatus(); + + if ($status != __('VALID')) { + $color = 'danger'; + } + + return ''.str_replace('_', ' ', $status).''; + }) + ->editColumn('uses', function (Coupon $coupon) { + return "{$coupon->uses} / {$coupon->max_uses}"; + }) + ->editColumn('value', function (Coupon $coupon) { + if ($coupon->type === 'percentage') { + return $coupon->value . "%"; + } + + return number_format($coupon->value, 2, '.', ''); + }) + ->editColumn('expires_at', function (Coupon $coupon) { + if (!$coupon->expires_at) { + return __('Never'); + } + + return Carbon::createFromTimestamp($coupon->expires_at); + }) + ->editColumn('created_at', function(Coupon $coupon) { + return Carbon::createFromTimeString($coupon->created_at); + }) + ->editColumn('code', function (Coupon $coupon) { + return "{$coupon->code}"; + }) + ->rawColumns(['actions', 'code', 'status']) + ->make(); + } } diff --git a/app/Listeners/CouponUsed.php b/app/Listeners/CouponUsed.php new file mode 100644 index 00000000..75ced4eb --- /dev/null +++ b/app/Listeners/CouponUsed.php @@ -0,0 +1,47 @@ +delete_coupon_on_expires = $couponSettings->delete_coupon_on_expires; + $this->delete_coupon_on_uses_reached = $couponSettings->delete_coupon_on_uses_reached; + } + + /** + * Handle the event. + * + * @param \App\Events\CouponUsedEvent $event + * @return void + */ + public function handle(CouponUsedEvent $event) + { + if ($this->delete_coupon_on_expires) { + if (!is_null($event->coupon->expired_at)) { + if ($event->coupon->expires_at <= Carbon::now()->timestamp) { + $event->coupon->delete(); + } + } + } + + if ($this->delete_coupon_on_uses_reached) { + if ($event->coupon->uses >= $event->coupon->max_uses) { + $event->coupon->delete(); + } + } + } +} diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index bdd71a3a..2ac9182d 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -4,6 +4,8 @@ namespace App\Providers; use App\Events\PaymentEvent; use App\Events\UserUpdateCreditsEvent; +use App\Events\CouponUsedEvent; +use App\Listeners\CouponUsed; use App\Listeners\CreateInvoice; use App\Listeners\UnsuspendServers; use App\Listeners\UserPayment; @@ -31,6 +33,9 @@ class EventServiceProvider extends ServiceProvider CreateInvoice::class, UserPayment::class, ], + CouponUsedEvent::class => [ + CouponUsed::class + ], SocialiteWasCalled::class => [ // ... other providers 'SocialiteProviders\\Discord\\DiscordExtendSocialite@handle', diff --git a/app/Settings/CouponSettings.php b/app/Settings/CouponSettings.php index 45f9c25b..29ec3332 100644 --- a/app/Settings/CouponSettings.php +++ b/app/Settings/CouponSettings.php @@ -7,6 +7,8 @@ use Spatie\LaravelSettings\Settings; class CouponSettings extends Settings { public ?int $max_uses_per_user; + public ?bool $delete_coupon_on_expires; + public ?bool $delete_coupon_on_uses_reached; public static function group(): string { @@ -20,7 +22,9 @@ class CouponSettings extends Settings public static function getValidations() { return [ - 'max_uses_per_user' => 'required|integer' + 'max_uses_per_user' => 'required|integer', + 'delete_coupon_on_expires' => 'required|boolean', + 'delete_coupon_on_uses_reached' => 'required|boolean', ]; } @@ -36,7 +40,17 @@ class CouponSettings extends Settings '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.', + 'description' => 'Maximum number of uses that a user can make of the same coupon.' + ], + 'delete_coupon_on_expires' => [ + 'label' => 'Delete Coupon On Expires', + 'type' => 'boolean', + 'description' => 'Automatically deletes the coupon if it expires.' + ], + 'delete_coupon_on_uses_reached' => [ + 'label' => 'Delete Coupon When Max Uses Reached', + 'type' => 'boolean', + 'description' => 'Delete a coupon as soon as its maximum usage is reached.' ] ]; } diff --git a/database/settings/2023_05_12_170041_create_coupon_settings.php b/database/settings/2023_05_12_170041_create_coupon_settings.php index 77505cea..9ff792ce 100644 --- a/database/settings/2023_05_12_170041_create_coupon_settings.php +++ b/database/settings/2023_05_12_170041_create_coupon_settings.php @@ -7,10 +7,14 @@ return new class extends SettingsMigration public function up(): void { $this->migrator->add('coupon.max_uses_per_user', 1); + $this->migrator->add('coupon.delete_coupon_on_expires', false); + $this->migrator->add('coupon.delete_coupon_on_uses_reached', false); } public function down(): void { $this->migrator->delete('coupon.max_uses_per_user'); + $this->migrator->delete('coupon.delete_coupon_on_expires'); + $this->migrator->delete('coupon.delete_coupon_on_uses_reached'); } }; diff --git a/routes/web.php b/routes/web.php index bdfd06f5..a6a2ab3c 100644 --- a/routes/web.php +++ b/routes/web.php @@ -201,6 +201,7 @@ Route::middleware(['auth', 'checkSuspended'])->group(function () { Route::resource('partners', PartnerController::class); //coupons + Route::get('coupons/datatable', [CouponController::class, 'dataTable'])->name('coupons.datatable'); Route::post('coupons/redeem', [CouponController::class, 'redeem'])->name('coupon.redeem'); Route::resource('coupons', CouponController::class); diff --git a/themes/default/views/admin/coupons/create.blade.php b/themes/default/views/admin/coupons/create.blade.php index 2b19d6a7..7cef744e 100644 --- a/themes/default/views/admin/coupons/create.blade.php +++ b/themes/default/views/admin/coupons/create.blade.php @@ -84,7 +84,7 @@ @enderror
-
-
-
-
-
-