diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bd454f5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,30 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.0.0] - 2017-08-29 +### Added +- Ability to set expiration date of promocode while creating. +- Ability to remove all redundant (expired or used) promocodes from database. +- Invalid Promocode Exception, Unauthenticated Exception, Already Used Exception. +- Ability to disable promocode using code string (Promocode will be expired). +- Support for Laravel 5.5 Package Auto-Discovery. + +### Changed +- Migration & config file. Now promocode & user will be related through pivot table. [#12] + +### Fixed +- Migration problem where database couldn't support json type. [#13] + +### Removed +- Ability of user, that they could create promocodes assigned to them. + +[Unreleased]: https://github.com/olivierlacan/keep-a-changelog/compare/v1.0.0...HEAD +[1.0.0]: https://github.com/zgabievi/laravel-promocodes/compare/v0.5.4...v1.0.0 + +[#12]: https://github.com/zgabievi/laravel-promocodes/issues/12 +[#13]: https://github.com/zgabievi/laravel-promocodes/issues/13 diff --git a/README.md b/README.md index 33c5f6c..fe534cd 100644 --- a/README.md +++ b/README.md @@ -9,14 +9,15 @@ [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/zgabievi/laravel-promocodes/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/zgabievi/laravel-promocodes/?branch=master) [![Build Status](https://scrutinizer-ci.com/g/zgabievi/laravel-promocodes/badges/build.png?b=master)](https://scrutinizer-ci.com/g/zgabievi/laravel-promocodes/build-status/master) -| PR0M0C0D35 | | -|:----------:|:----| -| [![PR0M0C0D35](https://i.imgsafe.org/ff13c6de54.png)](https://github.com/zgabievi/promocodes) | Promocodes generator for [Laravel 5.*](http://laravel.com/). Trying to make the best package in this category. You are welcome to join the party, give me some advices :tada: and make pull requests. | +> Promocodes generator for [Laravel 5.*](http://laravel.com/). Trying to make the best package in this category. You are welcome to join the party, give me some advices :tada: and make pull requests. ## Table of Contents - [Installation](#installation) - [Configuration](#configuration) - [Usage](#usage) + - [Basic Methods](#usage) + - [User Trait](#promocodes-can-be-related-to-users) + - [Additional Data](#how-to-use-additional-data) - [License](#license) ### What's new? @@ -29,7 +30,9 @@ Install this package via Composer: $ composer require zgabievi/promocodes ``` -### Open `config/app.php` and follow steps below: +> If you are using Laravel 5.5, than installation is done. Otherwise follow next steps. + +#### Open `config/app.php` and follow steps below: Find the `providers` array and add our service provider. @@ -67,32 +70,73 @@ $ php artisan migrate ## Usage -Generate as many codes as you wish and output them without saving. +Generate as many codes as you wish and output them without saving to database. + You will get array of codes in return: ```php Promocodes::output($amount = 1) ``` -Create as many codes as you wish, with same reward for each one. +--- + +Create as many codes as you wish. Set reward (amount). + +Attach additional data as array. Specify for how many days should this codes stay alive. + They will be saved in database and you will get collection of them in return: ```php -Promocodes::create($amount = 1, $reward = null, array $data = []) +Promocodes::create($amount = 1, $reward = null, array $data = [], $expires_in = null) ``` -Check if given code exists and isn't used at all: +--- + +Check if given code exists, is usable and not yet expired. + +This code may throw `\Gabievi\Promocodes\Exceptions\InvalidPromocodeExceprion` if there is not such promocode in database, with give code. + +Returns `Promocode` object if valid, or `false` if not. ```php Promocodes::check($code) ``` -Apply, that given code is used. Update database record. You will get promocode record back or true/false: +--- + +Redeem or apply code. Redeem is alias for apply method. + +User should be authenticated to redeem code or this method will thow an exception (`\Gabievi\Promocodes\Exceptions\UnauthenticatedExceprion`). + +Also if authenticated user will try to apply code twice, it will throw an exception (`\Gabievi\Promocodes\Exceptions\AlreadyUsedExceprion`) + +Returns `Promocode` object if applied, or `false` if not. ```php +Promocodes::redeem($code) Promocodes::apply($code) ``` +--- + +You can imediately expire code by calling *disable* function. Returning boolean status of update. + +```php +Promocodes::disable($code) +``` + +--- + +And if you want to delete expired, or non-usable codes you can erase them. + +This method will remove redundant codes from database and their relations to users. + +```php +Promocodes::clearRedundant() +``` + +--- + ### Promocodes can be related to users If you want to use user relation open `app/User.php` and make it `Rewardable` as in example: @@ -111,38 +155,25 @@ class User extends Authenticatable // ... } ``` +--- -Get all promocodes of current user: - -```php -User::promocodes() -``` - -> There is query scopes for promocodes: `fresh()`, `byCode($code)`: -> - `User::promocodes()->fresh()` - all not used codes of user -> - `User::promocodes()->byCode($code)` - record which matches given code - -Create promocode(s) for current user. Works exactly same like `create` method of `Promocodes`: - -```php -User::createCode($amount = 1, $reward = null, array $data = []) -``` +Redeem or apply code are same. *redeemCode* is alias of *applyCode* -Apply, that given code is used by current user. -Second argument is optional, if null, it will return promocode record or boolean, or you can pass callback function, which gives you reward or boolean value as argument: +Pass promotion code you want to be applied by current user. ```php +User::redeemCode($code, $callback = null) User::applyCode($code, $callback = null) ``` -Example: +Example (usage of callback): ```php -$user = Auth::user(); - -$user->applyCode('ABCD-DCBA', function ($promocode) use ($user) { - return 'Congratulations, ' . $user->name . '! We have added ' . $promocode->reward . ' points on your account'. +$redeemMessage = $user->redeemCode('ABCD-DCBA', function ($promocode) use ($user) { + return 'Congratulations, ' . $user->name . '! We have added ' . $promocode->reward . ' points on your account'; }); + +// Congratulations, Zura! We have added 10 points on your account ``` ### How to use additional data? @@ -153,31 +184,26 @@ $user->applyCode('ABCD-DCBA', function ($promocode) use ($user) { Promocodes::create(1, 25, ['foo' => 'bar', 'baz' => 'qux']); ``` -or - -```php -User::createCode(1, 25, ['foo' => 'bar', 'baz' => 'qux']); -``` - 2. Getting data back: ```php -Promocodes::apply('ABC-DEF', function($promocode) { +Promocodes::redeem('ABC-DEF', function($promocode) { echo $pomocode->data['foo']; }); + +// bar ``` or ```php -User::applyCode('ABC-DEF', function($promocode) { +User::redeemCode('ABC-DEF', function($promocode) { echo $pomocode->data['foo']; }); + +// bar ``` ## License -laravel-promocodes is licensed under a [MIT License](https://github.com/zgabievi/laravel-promocodes/blob/master/LICENSE). - -## TODO -- [x] Create tests to check funtionality +laravel-promocodes is licensed under a [MIT License](https://github.com/zgabievi/laravel-promocodes/blob/master/LICENSE). diff --git a/composer.json b/composer.json index 57281e1..4ee19a4 100644 --- a/composer.json +++ b/composer.json @@ -25,5 +25,15 @@ "psr-4": { "Gabievi\\Promocodes\\": "src/" } + }, + "extra": { + "laravel": { + "providers": [ + "Gabievi\\Promocodes\\PromocodesServiceProvider" + ], + "aliases": { + "Promocodes": "Gabievi\\Promocodes\\Facades\\Promocodes" + } + } } } diff --git a/src/Exceptions/AlreadyUsedExceprion.php b/src/Exceptions/AlreadyUsedExceprion.php new file mode 100644 index 0000000..ad9ff84 --- /dev/null +++ b/src/Exceptions/AlreadyUsedExceprion.php @@ -0,0 +1,18 @@ + 'boolean', - 'data' => 'array', + 'is_disposable' => 'boolean', + 'data' => 'array', ]; + /** + * The attributes that should be mutated to dates. + * + * @var array + */ + protected $dates = ['expires_at']; + /** * Promocode constructor. * @@ -48,13 +51,14 @@ public function __construct(array $attributes = []) } /** - * Get the user who owns the promocode. + * Get the users who is related promocode. * - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ - public function user() + public function users() { - return $this->belongsTo(User::class); + return $this->belongsToMany(config('promocodes.user_model'), config('promocodes.relation_table')) + ->withPivot('used_at'); } /** @@ -71,14 +75,55 @@ public function scopeByCode($query, $code) } /** - * Query builder to find all not used promocodes. + * Query builder to get disposable codes. * * @param $query + * @return mixed + */ + public function scopeIsDisposable($query) + { + return $query->where('is_disposable', true); + } + + /** + * Query builder to get non-disposable codes. * + * @param $query * @return mixed */ - public function scopeFresh($query) + public function scopeIsNotDisposable($query) + { + return $query->where('is_disposable', false); + } + + /** + * Query builder to get expired promotion codes. + * + * @param $query + * @return mixed + */ + public function scopeExpired($query) + { + return $query->whereNotNull('expires_at')->whereDate('expires_at', '<=', Carbon::now()); + } + + /** + * Check if code is disposable (ont-time). + * + * @return bool + */ + public function isDisposable() + { + return $this->is_disposable; + } + + /** + * Check if code is expired. + * + * @return bool + */ + public function isExpired() { - return $query->where('is_used', false); + return $this->expires_at ? Carbon::now()->gte($this->expires_at) : false; } } diff --git a/src/Promocodes.php b/src/Promocodes.php index a69fcbe..564295a 100644 --- a/src/Promocodes.php +++ b/src/Promocodes.php @@ -2,7 +2,11 @@ namespace Gabievi\Promocodes; +use Carbon\Carbon; use Gabievi\Promocodes\Model\Promocode; +use Gabievi\Promocodes\Exceptions\AlreadyUsedExceprion; +use Gabievi\Promocodes\Exceptions\UnauthenticatedExceprion; +use Gabievi\Promocodes\Exceptions\InvalidPromocodeExceprion; class Promocodes { @@ -28,9 +32,154 @@ class Promocodes public function __construct() { $this->codes = Promocode::pluck('code')->toArray(); + $this->length = substr_count(config('promocodes.mask'), '*'); } + /** + * Generates promocodes as many as you wish. + * + * @param int $amount + * + * @return array + */ + public function output($amount = 1) + { + $collection = []; + + for ($i = 1; $i <= $amount; $i++) { + $random = $this->generate(); + + while (!$this->validate($collection, $random)) { + $random = $this->generate(); + } + + array_push($collection, $random); + } + + return $collection; + } + + /** + * Save promocodes into database + * Successful insert returns generated promocodes + * Fail will return NULL. + * + * @param int $amount + * @param null $reward + * @param array $data + * @param int|null $expires_in + * + * @return \Illuminate\Support\Collection + */ + public function create($amount = 1, $reward = null, array $data = [], $expires_in = null) + { + $records = []; + + foreach ($this->output($amount) as $code) { + $records[] = [ + 'code' => $code, + 'reward' => $reward, + 'data' => json_encode($data), + 'expires_at' => $expires_in ? Carbon::now()->addDays($expires_in) : null, + ]; + } + + if (Promocode::insert($records)) { + return collect($records); + } + + return collect([]); + } + + /** + * Check promocode in database if it is valid. + * + * @param string $code + * + * @return bool|\Gabievi\Promocodes\Model\Promocode + * @throws \Gabievi\Promocodes\Exceptions\InvalidPromocodeExceprion + */ + public function check($code) + { + $promocode = Promocode::byCode($code)->first(); + + if ($promocode === null) { + throw new InvalidPromocodeExceprion; + } + + if ($promocode->isExpired() || ($promocode->isDisposable() && $promocode->users()->exists())) { + return false; + } + + return $promocode; + } + + /** + * Apply promocode to user that it's used from now. + * + * @param string $code + * + * @return bool|\Gabievi\Promocodes\Model\Promocode + * @throws \Gabievi\Promocodes\Exceptions\UnauthenticatedExceprion|\Gabievi\Promocodes\Exceptions\AlreadyUsedExceprion + */ + public function apply($code) + { + if (!auth()->check()) { + throw new UnauthenticatedExceprion; + } + + if ($promocode = $this->check($code)) { + if ($this->isSecondUsageAttempt($promocode)) { + throw new AlreadyUsedExceprion; + } + + $promocode->users()->attach(auth()->user()->id, [ + 'used_at' => Carbon::now(), + ]); + + return $promocode->load('users'); + } + + return false; + } + + /** + * Expire code as it won't usable anymore. + * + * @param string $code + * @return bool + * @throws \Gabievi\Promocodes\Exceptions\InvalidPromocodeExceprion + */ + public function disable($code) + { + $promocode = Promocode::byCode($code)->first(); + + if ($promocode === null) { + throw new InvalidPromocodeExceprion; + } + + $promocode->expires_at = Carbon::now(); + + return $promocode->save(); + } + + /** + * Clear all expired and used promotion codes + * that can not be used anymore. + * + * @return void + */ + public function clearRedundant() + { + Promocode::all()->each(function ($promocode) { + if ($promocode->isExpired() || ($promocode->isDisposable() && $promocode->users()->exists())) { + $promocode->users()->detach(); + $promocode->delete(); + } + }); + } + /** * Here will be generated single code using your parameters from config. * @@ -76,7 +225,9 @@ private function generate() */ private function getPrefix() { - return (bool) config('promocodes.prefix') ? config('promocodes.prefix').config('promocodes.separator') : ''; + return (bool)config('promocodes.prefix') + ? config('promocodes.prefix') . config('promocodes.separator') + : ''; } /** @@ -86,7 +237,9 @@ private function getPrefix() */ private function getSuffix() { - return (bool) config('promocodes.suffix') ? config('promocodes.separator').config('promocodes.suffix') : ''; + return (bool)config('promocodes.suffix') + ? config('promocodes.separator') . config('promocodes.suffix') + : ''; } /** @@ -103,95 +256,13 @@ private function validate($collection, $new) } /** - * Generates promocodes as many as you wish. - * - * @param int $amount - * - * @return array - */ - public function output($amount = 1) - { - $collection = []; - - for ($i = 1; $i <= $amount; $i++) { - $random = $this->generate(); - - while (!$this->validate($collection, $random)) { - $random = $this->generate(); - } - - $collection[] = $random; - } - - return $collection; - } - - /** - * Save promocodes into database - * Successful insert returns generated promocodes - * Fail will return NULL. - * - * @param int $amount - * @param null $reward - * @param array $data - * - * @return static - */ - public function create($amount = 1, $reward = null, array $data = []) - { - $records = []; - - // loop though each promocodes required - foreach ($this->output($amount) as $code) { - $records[] = [ - 'code' => $code, - 'reward' => $reward, - 'data' => json_encode($data), - ]; - } - - // check for insertion of record - if (Promocode::insert($records)) { - return collect($records); - } - - return collect([]); - } - - /** - * Check promocode in database if it is valid. + * Check if user is trying to apply code again. * - * @param $code + * @param $promocode * * @return bool */ - public function check($code) - { - return Promocode::byCode($code)->fresh()->exists(); - } - - /** - * Apply promocode to user that it's used from now. - * - * @param $code - * - * @return mixed - */ - public function apply($code) - { - $promocode = Promocode::byCode($code)->fresh(); - - // check if exists not used code - if ($promocode->exists()) { - $record = $promocode->first(); - $record->is_used = true; - - // update promocode as it is used - if ($record->save()) { - return $record ?: true; - } - } - - return false; + private function isSecondUsageAttempt($promocode) { + return $promocode->users()->wherePivot('user_id', auth()->user()->id)->exists(); } } diff --git a/src/Traits/Rewardable.php b/src/Traits/Rewardable.php index 7bd887a..725eeb0 100644 --- a/src/Traits/Rewardable.php +++ b/src/Traits/Rewardable.php @@ -2,84 +2,55 @@ namespace Gabievi\Promocodes\Traits; -use Gabievi\Promocodes\Facades\Promocodes; +use Carbon\Carbon; use Gabievi\Promocodes\Model\Promocode; +use Gabievi\Promocodes\Facades\Promocodes; trait Rewardable { /** - * Create promocodes for current model. + * Get the promocodes that are related to user. * - * @param int $amount - * @param null $reward - * @param array $data - * - * @return mixed + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ - public function createCode($amount = 1, $reward = null, array $data = []) + public function promocodes() { - $records = []; - - // loop though each promocodes required - foreach (Promocodes::output($amount) as $code) { - $records[] = new Promocode([ - 'code' => $code, - 'reward' => $reward, - 'data' => json_encode($data), - ]); - } - - // check for insertion of record - if ($this->promocodes()->saveMany($records)) { - return collect($records); - } - - return collect([]); + return $this->belongsToMany(Promocode::class, config('promocodes.relation_table')); } /** - * Apply promocode for user and get callback. + * Apply promocode to user and get callback. * - * @param $code - * @param $callback + * @param string $code + * @param null|\Closure $callback * - * @return bool|float + * @return null|\Gabievi\Promocodes\Model\Promocode + * @throws \Gabievi\Promocodes\Exceptions\AlreadyUsedExceprion */ public function applyCode($code, $callback = null) { - $promocode = Promocode::byCode($code)->fresh()->first(); - - // check if exists not used code - if (!is_null($promocode)) { - - // - if (!is_null($promocode->user) && $promocode->user->id !== $this->attributes['id']) { - - // callback function with false value - if (is_callable($callback)) { - $callback(false); - } - - return false; + if ($promocode = Promocodes::check($code)) { + if ($promocode->users()->wherePivot('user_id', $this->id)->exists()) { + throw new AlreadyUsedExceprion; } - // update promocode as it is used - if ($promocode->update(['is_used' => true])) { + $promocode->users()->attach($this->id, [ + 'used_at' => Carbon::now(), + ]); - // callback function with promocode model - if (is_callable($callback)) { - $callback($promocode ?: true); - } + $promocode->load('users'); - return $promocode ?: true; + if (is_callable($callback)) { + $callback($promocode); } + + return $promocode; } - // callback function with false value if (is_callable($callback)) { - $callback(false); + $callback(null); } - return false; + return null; } } diff --git a/src/config/promocodes.php b/src/config/promocodes.php index 36f83ea..c844e7c 100644 --- a/src/config/promocodes.php +++ b/src/config/promocodes.php @@ -9,6 +9,12 @@ */ 'table' => 'promocodes', + /* + * Database pivot table name for promocodes and users relation + * use default database name: 'promocode_user' + */ + 'relation_table' => 'promocode_user', + /* * List of characters, promo code generated from. * We have removed 1 (one) and I because with some @@ -50,4 +56,9 @@ * Can be set any thing you wish */ 'separator' => '-', + + /** + * User model + */ + 'user_model' => \App\User::class, ];