├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── config └── referrals.php ├── database └── migrations │ ├── 2017_09_23_110000_create_referral_programs_table.php │ ├── 2017_09_23_110001_create_referral_links_table.php │ ├── 2017_09_23_110002_create_referral_relationships_table.php │ ├── 2017_09_23_110003_add_allowed_ref_program_to_users.php │ └── 2023_08_30_230804_add_clicks_to_links.php ├── phpunit.xml ├── src ├── Contracts │ └── ProgramInterface.php ├── Events │ ├── ReferralCase.php │ └── UserReferred.php ├── Http │ └── Middleware │ │ └── StoreReferralCode.php ├── Listeners │ ├── ReferUser.php │ └── RewardUser.php ├── Models │ ├── ReferralLink.php │ ├── ReferralProgram.php │ └── ReferralRelationship.php ├── Programs │ ├── AbstractProgram.php │ └── ExampleProgram.php ├── Providers │ └── ReferralsServiceProvider.php └── Traits │ └── ReferralsMember.php ├── test.sh └── tests ├── TestCase.php ├── TestUser.php ├── Unit ├── Events │ └── UserReferredTest.php ├── Listeners │ ├── ReferUserTest.php │ └── RewardUserTest.php ├── MiddlewareTest.php └── Models │ └── ReferralLinkTest.php ├── WithLoadMigrations.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | node_modules/ 3 | .idea 4 | .phpunit.* 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jefferson Ochoa and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple Referrals system for Laravel 2 | 3 | [![Latest Version on Packagist][ico-version]][link-packagist] 4 | [![Software License][ico-license]](LICENSE) 5 | [![Total Downloads][ico-downloads]][link-downloads] 6 | 7 | A simple system of referrals with the ability to assign different programs for different users. 8 | 9 | This package was created based on the [lesson](https://blog.damirmiladinov.com/laravel/building-laravel-referral-system.html#.Wc4eA6xJaHo) 10 | author is Damir Miladinov, with some minor changes, for which I express my gratitude to him. 11 | 12 | - [Installation](#installation) 13 | - [Usage](#usage) 14 | - [Bonus](#bonus-content) 15 | 16 | ## Installation 17 | ### Add dependency 18 | Via Composer 19 | 20 | ``` bash 21 | $ composer require pdazcom/laravel-referrals 22 | ``` 23 | 24 | Then in config/app.php add service-provider and facade alias: 25 | 26 | ``` 27 | 'providers' => [ 28 | ... 29 | Pdazcom\Referrals\Providers\ReferralsServiceProvider::class, 30 | ... 31 | ]; 32 | ``` 33 | 34 | ### Configuration 35 | First of all you need to run: 36 | ``` 37 | php artisan vendor:publish --tag=referrals-config 38 | ``` 39 | to make `referrals.php` file in your `config` folder. 40 | 41 | ### Migrations 42 | >**OPTIONAL:** If you want to make changes to the migration files, you also need to run: 43 | >``` 44 | >php artisan vendor:publish --tag=referrals-migrations 45 | >``` 46 | > Then change new migrations. 47 | 48 | Run `php artisan migrate` to make tables in database. 49 | 50 | ### Middleware 51 | Add middleware to your `web` group in `Http/Kernel.php`: 52 | 53 | ``` 54 | 'web' => [ 55 | ... 56 | \Pdazcom\Referrals\Http\Middleware\StoreReferralCode::class, 57 | ], 58 | ``` 59 | This intermediary stores referral links applied to the user in cookies. 60 | 61 | 62 | >#### Note 63 | >Starting from v2.0, several referral programs can be applied to same user. 64 | >They will be stored in cookies as a JSON-object, and in the request instance, 65 | >an array will be available in the `_referrals` property: 66 | >``` 67 | >[ 68 | > 'ref_id_1' => 'expires_timestamp', 69 | > 'ref_id_2' => 'expires_timestamp', 70 | > ... 71 | > 'ref_id_n' => 'expires_timestamp' 72 | >] 73 | >``` 74 | >where `ref_id_n` - ID of referral link, `expires_timestamp` - storage expire timestamp for links in cookies. 75 | > 76 | > Expired links are automatically deleted. 77 | > 78 | 79 | Add `Pdazcom\Referrals\Traits\ReferralsMember` trait to your `Users` model: 80 | 81 | 82 | ``` 83 | class User extends Authenticatable { 84 | use ReferralsMember; 85 | ... 86 | } 87 | ``` 88 | ## Usage 89 | ### Add new referrer event 90 | Then in `Http/Controllers/Auth/RegisterController.php` add event dispatcher: 91 | 92 | ``` 93 | ... 94 | use Pdazcom\Referrals\Events\UserReferred; 95 | use Pdazcom\Referrals\Middlewares\StoreReferralCode; 96 | 97 | ... 98 | // overwrite registered function 99 | public function registered(Request $request) 100 | { 101 | // dispatch user referred event here 102 | UserReferred::dispatch(request()->input(StoreReferralCode::REFERRALS), $user); 103 | } 104 | ``` 105 | 106 | From this point all referral links would be attached new users as referrals to users owners of these links. 107 | ### Create referral program 108 | And then you need to create a referral program in database and attach it to users by `referral_program_id` field: 109 | 110 | ``` 111 | php artisan tinker 112 | 113 | Pdazcom\Referrals\Models\ReferralProgram::create(['name'=>'example', 'title' => 'Example Program', 'description' => 'Laravel Referrals made easy thanks to laravel-referrals package based on an article by Damir Miladinov,', 'uri' => 'register']); 114 | ``` 115 | 116 | add association to config `referrals.programs`: 117 | ``` 118 | ... 119 | 'example' => App\ReferralPrograms\ExampleProgram.php 120 | ``` 121 | and create the reward class `App\ReferralPrograms\ExampleProgram.php` for referral program: 122 | 123 | ``` 124 | recruitUser->balance = $this->recruitUser->balance + $rewardObject * (self::ROYALTY_PERCENT/100); 142 | $this->recruitUser->save(); 143 | } 144 | 145 | } 146 | ``` 147 | create referral link: 148 | ``` 149 | php artisan tinker 150 | 151 | Pdazcom\Referrals\Models\ReferralLink::create(['user_id' => 1, 'referral_program_id' => 1]); 152 | ``` 153 | 154 | and finally dispatch reward event in any place of your code: 155 | 156 | ``` 157 | use Pdazcom\Referrals\Events\ReferralCase; 158 | ... 159 | 160 | ReferralCase::dispatch('example', $referralUser, $rewardObject); 161 | ``` 162 | 163 | From this point all referrals action you need would be reward recruit users by code logic in your reward classes. 164 | 165 | Create many programs and their reward classes. Enjoy! 166 | 167 | ### Bonus Content 168 | 169 | If you want to list all the users for a given Referral Link, simply use 170 | 171 | ```php 172 | $referralLink->referredUsers() 173 | ``` 174 | 175 | ## Security 176 | 177 | If you discover any security related issues, please email kostya.dn@gmail.com instead of using the issue tracker. 178 | 179 | ## Credits 180 | 181 | - [Konstantin A.][link-author] 182 | - [All Contributors][link-contributors] 183 | 184 | ## License 185 | 186 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 187 | 188 | [ico-version]: https://img.shields.io/packagist/v/pdazcom/laravel-referrals.svg?style=flat-square 189 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square 190 | [ico-travis]: https://img.shields.io/travis/pdazcom/laravel-referrals/master.svg?style=flat-square 191 | [ico-scrutinizer]: https://img.shields.io/scrutinizer/coverage/g/pdazcom/laravel-referrals.svg?style=flat-square 192 | [ico-code-quality]: https://img.shields.io/scrutinizer/g/pdazcom/laravel-referrals.svg?style=flat-square 193 | [ico-downloads]: https://img.shields.io/packagist/dt/pdazcom/laravel-referrals.svg?style=flat-square 194 | 195 | [link-packagist]: https://packagist.org/packages/pdazcom/laravel-referrals 196 | [link-travis]: https://travis-ci.org/pdazcom/laravel-referrals 197 | [link-scrutinizer]: https://scrutinizer-ci.com/g/pdazcom/laravel-referrals/code-structure 198 | [link-code-quality]: https://scrutinizer-ci.com/g/pdazcom/laravel-referrals 199 | [link-downloads]: https://packagist.org/packages/pdazcom/laravel-referrals 200 | [link-author]: https://github.com/pdazcom 201 | [link-contributors]: https://github.com/pdazcom/laravel-referrals/graphs/contributors 202 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pdazcom/laravel-referrals", 3 | "description": "A referrals system for a laravel projects.", 4 | "homepage": "https://github.com/pdazcom/laravel-referrals", 5 | "keywords": [ 6 | "laravel", 7 | "referrals", 8 | "package", 9 | "laravel-referrals", 10 | "referral-system", 11 | "user-referral", 12 | "affiliate", 13 | "referral-program", 14 | "referals" 15 | ], 16 | "type": "library", 17 | "version": "2.0.0", 18 | "require": { 19 | "php": "^8.2", 20 | "laravel/framework": "^9.52.18|^10", 21 | "ext-json": "*" 22 | }, 23 | "require-dev": { 24 | "orchestra/testbench":"^7.48" 25 | }, 26 | "license": "MIT", 27 | "authors": [ 28 | { 29 | "name": "Konstantin A.", 30 | "email": "kostya.dn@gmail.com" 31 | } 32 | ], 33 | "autoload": { 34 | "psr-4": { 35 | "Pdazcom\\Referrals\\": "src/" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "Pdazcom\\Referrals\\Tests\\": "tests/" 41 | } 42 | }, 43 | "extra": { 44 | "laravel": { 45 | "providers": [ 46 | "Pdazcom\\Referrals\\Providers\\ReferralsServiceProvider" 47 | ] 48 | } 49 | }, 50 | "scripts": { 51 | "test": "./test.sh" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /config/referrals.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'example' => \Pdazcom\Referrals\Programs\ExampleProgram::class, 6 | ], 7 | 'cookie_name' => 'ref', 8 | ]; 9 | -------------------------------------------------------------------------------- /database/migrations/2017_09_23_110000_create_referral_programs_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 20 | $table->string('name')->unique(); 21 | $table->string('uri'); 22 | $table->string('title'); 23 | $table->text('description'); 24 | $table->integer('lifetime_minutes')->default(self::DEFAULT_LIFETIME); 25 | $table->timestamps(); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | * 32 | * @return void 33 | */ 34 | public function down() 35 | { 36 | Schema::drop('referral_programs'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /database/migrations/2017_09_23_110001_create_referral_links_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->integer('user_id')->unsigned(); 19 | $table->integer('referral_program_id')->unsigned(); 20 | $table->string('code', 36)->index(); 21 | $table->unique(['referral_program_id', 'user_id']); 22 | $table->timestamps(); 23 | 24 | $table->foreign('referral_program_id')->references('id')->on('referral_programs')->onDelete('cascade'); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::drop('referral_links'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /database/migrations/2017_09_23_110002_create_referral_relationships_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->integer('referral_link_id')->unsigned(); 19 | $table->integer('user_id')->unsigned(); 20 | $table->timestamps(); 21 | 22 | $table->foreign('referral_link_id')->references('id')->on('referral_links')->onDelete('cascade'); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::drop('referral_relationships'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /database/migrations/2017_09_23_110003_add_allowed_ref_program_to_users.php: -------------------------------------------------------------------------------- 1 | environment() === 'testing') { 17 | return; 18 | } 19 | 20 | Schema::table($this->getUsersTable(), function (Blueprint $table) { 21 | $table->integer('referral_program_id')->unsigned()->nullable()->dafault(null); 22 | 23 | $table->foreign('referral_program_id') 24 | ->references('id') 25 | ->on('referral_programs') 26 | ->onDelete('set null'); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | * 33 | * @return void 34 | */ 35 | public function down() 36 | { 37 | if (app()->environment() === 'testing') { 38 | return; 39 | } 40 | 41 | Schema::table($this->getUsersTable(), function (Blueprint $table) { 42 | $table->dropForeign(['referral_program_id']); 43 | $table->dropColumn('referral_program_id'); 44 | }); 45 | } 46 | 47 | private function getUsersTable() 48 | { 49 | $userModel = config('auth.providers.users.model'); 50 | return (new $userModel)->getTable(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /database/migrations/2023_08_30_230804_add_clicks_to_links.php: -------------------------------------------------------------------------------- 1 | unsignedBigInteger('clicks')->default(0); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('referral_links', function (Blueprint $table) { 29 | $table->dropColumn('clicks'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./Tests/Unit/ 15 | 16 | 17 | 18 | 19 | ./src/ 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/Contracts/ProgramInterface.php: -------------------------------------------------------------------------------- 1 | user = $user; 23 | $this->programName = is_array($programName) ? $programName : [ $programName ]; 24 | $this->rewardObject = $rewardObject; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Events/UserReferred.php: -------------------------------------------------------------------------------- 1 | referralIDs = array_keys($referralId); 22 | $this->user = $user; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Http/Middleware/StoreReferralCode.php: -------------------------------------------------------------------------------- 1 | expires_timestamp, 20 | * link_id2 => expires_timestamp, 21 | * ... 22 | * link_idN => expires_timestamp 23 | * ] 24 | */ 25 | class StoreReferralCode { 26 | 27 | protected string $cookieName = 'ref'; 28 | const REFERRALS = '_referrals'; 29 | protected Request $request; 30 | 31 | public function handle(Request $request, Closure $next) 32 | { 33 | $this->request = $request; 34 | $this->cookieName = config('referrals.cookie_name'); 35 | $request->merge([static::REFERRALS => $this->parseRefCookie()]); 36 | 37 | if ($request->query($this->cookieName)) { 38 | /** @var ReferralLink $referral */ 39 | $referral = ReferralLink::whereCode($request->get($this->cookieName))->first(); 40 | 41 | if (!empty($referral)) { 42 | 43 | $referral->addClick(); 44 | 45 | return redirect($request->url()) 46 | ->cookie($this->prepareCookie($referral->id, $referral->program->lifetime_minutes)); 47 | } 48 | 49 | Log::warning('Referral Ref code not found where request.ref equals ' . $request->get($this->cookieName)); 50 | } 51 | 52 | return $next($request); 53 | } 54 | 55 | /** 56 | * Parse referrals from cookie and returns as array like ['ref_id' => 'ref_expires'] 57 | * where 'ref_id' is a referral link `id` 58 | * and 'ref_expires' is timestamp when referral expires 59 | * 60 | * @return array 61 | */ 62 | public function parseRefCookie (): array 63 | { 64 | // get referral program cookie if exist 65 | $refCookie = $this->request->cookie($this->cookieName, '[]'); 66 | try { 67 | $programCookie = json_decode($refCookie, true, flags: JSON_THROW_ON_ERROR); 68 | 69 | // remove all expired referrals 70 | return array_filter($programCookie, fn ($expires) => now()->timestamp < $expires); 71 | 72 | } catch (\Exception) { 73 | 74 | // return empty can't unknown format 75 | return []; 76 | } 77 | } 78 | 79 | private function prepareCookie(int $referralId, int $lifetimeMinutes): Cookie 80 | { 81 | $programCookie = $this->request->input(static::REFERRALS); 82 | 83 | // add referral ID to array 84 | // set cookie for current referral 85 | $programCookie[$referralId] = now()->addMinutes($lifetimeMinutes)->timestamp; 86 | 87 | // save to request new referrals 88 | $this->request->replace([static::REFERRALS => $programCookie]); 89 | 90 | // set cookie with max ref program lifetime 91 | $cookieExpires = max(array_values($programCookie)); 92 | 93 | // add a minute for inclusive difference 94 | $lifetimeMinutes = Carbon::createFromTimestamp($cookieExpires)->diffInMinutes() + 1; 95 | 96 | return cookie( 97 | $this->cookieName, 98 | json_encode($programCookie), 99 | $lifetimeMinutes 100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Listeners/ReferUser.php: -------------------------------------------------------------------------------- 1 | referralIDs)) { 18 | Log::debug('ReferralIDs not provided so skipping logic [' . implode(", ", $event->referralIDs) . ']'); 19 | return; 20 | } 21 | 22 | foreach ($event->referralIDs as $referralID) { 23 | 24 | /** @var ReferralLink $referralLink */ 25 | $referralLink = ReferralLink::find($referralID); 26 | 27 | if (empty($referralLink)) { 28 | Log::warning('Referral Link not found for referralId '. $referralID); 29 | continue; 30 | } 31 | 32 | $referralLink->relationships()->firstOrCreate(['user_id' => $event->user->id]); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Listeners/RewardUser.php: -------------------------------------------------------------------------------- 1 | programName)->get(); 21 | 22 | // if it not exists, then nothing to do 23 | if (empty($referralPrograms)) { 24 | Log::warning("Program(s) named '" . implode(", ", $event->programName) . "' not found"); 25 | return; 26 | } 27 | 28 | foreach ($referralPrograms as $referralProgram) { 29 | $referralLink = $this->getReferralLink($referralProgram, $event->user->id); 30 | 31 | // if user is not refer for this referral program then nothing to do 32 | if (empty($referralLink)) { 33 | continue; 34 | } 35 | 36 | $recruitUser = $referralLink->user; 37 | $referralUser = $event->user; 38 | $rewardClass = config('referrals.programs.' . $referralProgram->name); 39 | 40 | if (!class_exists($rewardClass)) { 41 | Log::warning("Not configured program reward class for '$referralProgram->name' referral program"); 42 | continue; 43 | } 44 | 45 | (new $rewardClass($referralProgram, $recruitUser, $referralUser))->reward($event->rewardObject); 46 | } 47 | } 48 | 49 | /** 50 | * Find referral link where current user is refer for 51 | * 52 | * @param $userId 53 | * @param ReferralProgram $program 54 | * @return Builder|Model|HasMany|null 55 | */ 56 | protected function getReferralLink(ReferralProgram $program, $userId): Builder|Model|HasMany|null 57 | { 58 | return $program->links()->whereHas('relationships', function ($query) use ($userId) { 59 | $query->where('user_id', $userId); 60 | })->first(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Models/ReferralLink.php: -------------------------------------------------------------------------------- 1 | generateCode(); 31 | }); 32 | } 33 | 34 | private function generateCode(): void 35 | { 36 | $this->code = (string) Uuid::uuid1(); 37 | } 38 | 39 | public static function getReferral($user, $program) 40 | { 41 | return static::where([ 42 | 'user_id' => $user->id, 43 | 'referral_program_id' => $program->id 44 | ])->first(); 45 | } 46 | 47 | public function link(): Attribute 48 | { 49 | return Attribute::get( fn () => url($this->program->uri) . '?ref=' . $this->code); 50 | } 51 | 52 | public function user(): BelongsTo 53 | { 54 | $usersModel = config('auth.providers.users.model'); 55 | return $this->belongsTo($usersModel); 56 | } 57 | 58 | public function program(): BelongsTo 59 | { 60 | return $this->belongsTo(ReferralProgram::class, 'referral_program_id'); 61 | } 62 | 63 | public function relationships(): HasMany 64 | { 65 | return $this->hasMany(ReferralRelationship::class); 66 | } 67 | 68 | public function referredUsers() 69 | { 70 | $usersModel = config('auth.providers.users.model'); 71 | return $usersModel::whereIn('id', $this->relationships->pluck('user_id')->all())->get(); 72 | } 73 | 74 | public function addClick(): static 75 | { 76 | $this->increment('clicks'); 77 | return $this; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Models/ReferralProgram.php: -------------------------------------------------------------------------------- 1 | hasMany(ReferralLink::class, 'referral_program_id', 'id'); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/Models/ReferralRelationship.php: -------------------------------------------------------------------------------- 1 | recruitUser->balance = $this->recruitUser->balance + $rewardObject * (self::ROYALTY_PERCENT/100); 15 | $this->recruitUser->save(); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/Providers/ReferralsServiceProvider.php: -------------------------------------------------------------------------------- 1 | [ 13 | 'Pdazcom\Referrals\Listeners\ReferUser', 14 | ], 15 | 'Pdazcom\Referrals\Events\ReferralCase' => [ 16 | 'Pdazcom\Referrals\Listeners\RewardUser', 17 | ], 18 | ]; 19 | 20 | /** 21 | * Register bindings in the container. 22 | */ 23 | public function register() 24 | { 25 | $this->mergeConfigFrom(__DIR__ . '/../../config/referrals.php', 'referrals'); 26 | } 27 | 28 | /** 29 | * Perform post-registration booting of services. 30 | */ 31 | public function boot() 32 | { 33 | parent::boot(); 34 | 35 | // publish config 36 | $this->publishes([__DIR__ . '/../../config/referrals.php' => config_path('referrals.php')], 'referrals-config'); 37 | 38 | $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); 39 | 40 | // publish migrations 41 | $migrationsPath = __DIR__ . '/../../database/migrations/2017_09_23_1100'; 42 | $this->publishes([ 43 | "{$migrationsPath}00_create_referral_programs_table.php" => database_path('migrations/' . date("Y_m_d_Hi") . "00_create_referral_programs_table.php"), 44 | "{$migrationsPath}01_create_referral_links_table.php" => database_path('migrations/' . date("Y_m_d_Hi") . "01_create_referral_links_table.php"), 45 | "{$migrationsPath}02_create_referral_relationships_table.php" => database_path('migrations/' . date("Y_m_d_Hi") . "02_create_referral_relationships_table.php"), 46 | "{$migrationsPath}03_add_allowed_ref_program_to_users.php" => database_path('migrations/' . date("Y_m_d_Hi") . "03_add_allowed_ref_program_to_users.php"), 47 | ], 'referrals-migrations'); 48 | 49 | AboutCommand::add('Laravel Referrals', fn () => [ 50 | 'Version' => '2.0.0', 51 | 'Description' => 'A simple system of referrals with the ability to assign different programs for different users.', 52 | 'Url' => 'https://github.com/pdazcom/laravel-referrals' 53 | ]); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Traits/ReferralsMember.php: -------------------------------------------------------------------------------- 1 | map(function ($program) { 19 | return ReferralLink::getReferral($this, $program); 20 | })->filter(); 21 | } 22 | 23 | public function referralProgram(): HasOne 24 | { 25 | return $this->hasOne(ReferralProgram::class, 'id', 'referral_program_id'); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ./vendor/bin/phpunit --configuration phpunit.xml ./tests/Unit/ -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | set('database.default', 'testbench'); 20 | $app['config']->set('database.connections.testbench', [ 21 | 'driver' => 'sqlite', 22 | 'database' => ':memory:', 23 | 'prefix' => '', 24 | ]); 25 | } 26 | 27 | protected function getPackageProviders($app): array 28 | { 29 | return ['Pdazcom\Referrals\Providers\ReferralsServiceProvider']; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/TestUser.php: -------------------------------------------------------------------------------- 1 | randomNumber(); 14 | $user = new TestUser(); 15 | $event = new UserReferred([ 16 | 1 => now()->addDay()->timestamp, 17 | 2 => now()->addMinute()->timestamp, 18 | $link_id => now()->timestamp 19 | ], $user); 20 | 21 | $this->assertTrue(in_array($link_id, $event->referralIDs)); 22 | $this->assertEquals($user, $event->user); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Unit/Listeners/ReferUserTest.php: -------------------------------------------------------------------------------- 1 | user(); 18 | $referrerUser = $this->user(); 19 | 20 | $program = ReferralProgram::create([ 21 | 'name' => 'test', 22 | 'title' => 'Test', 23 | 'description' => 'Test description', 24 | 'uri' => 'test', 25 | ]); 26 | 27 | $refLink = $program->links()->create([ 28 | 'user_id' => $recruitUser->id, 29 | ]); 30 | 31 | $event = new UserReferred([$refLink->id => now()->timestamp], $referrerUser); 32 | (new ReferUser())->handle($event); 33 | 34 | $relationships = $refLink->relationships; 35 | 36 | $this->assertCount(1, $relationships); 37 | $this->assertEquals($referrerUser->id, $relationships->first()->user_id); 38 | 39 | (new ReferUser())->handle($event); 40 | 41 | $this->assertEquals(1, $refLink->relationships()->count()); 42 | $this->assertEquals($referrerUser->id, $relationships->first()->user_id); 43 | } 44 | 45 | public function testCreatingMultiplyRelationship() 46 | { 47 | $recruitUser = $this->user(); 48 | $recruitUser2 = $this->user(); 49 | $referrerUser = $this->user(); 50 | 51 | 52 | $program = ReferralProgram::create([ 53 | 'name' => 'test', 54 | 'title' => 'Test', 55 | 'description' => 'Test description', 56 | 'uri' => 'test', 57 | ]); 58 | 59 | $refLink = $program->links()->create([ 60 | 'user_id' => $recruitUser->id, 61 | ]); 62 | 63 | $refLink2 = $program->links()->create([ 64 | 'user_id' => $recruitUser2->id, 65 | ]); 66 | 67 | $event = new UserReferred([$refLink->id => now()->timestamp], $referrerUser); 68 | (new ReferUser())->handle($event); 69 | 70 | $relationships = $refLink->relationships; 71 | 72 | $this->assertCount(1, $relationships); 73 | $this->assertEquals($referrerUser->id, $relationships->first()->user_id); 74 | 75 | $event = new UserReferred([$refLink2->id => now()->timestamp], $referrerUser); 76 | (new ReferUser())->handle($event); 77 | 78 | $relationships2 = $refLink2->relationships; 79 | 80 | $this->assertCount(1, $relationships2); 81 | $this->assertEquals($referrerUser->id, $relationships2->first()->user_id); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/Unit/Listeners/RewardUserTest.php: -------------------------------------------------------------------------------- 1 | user()); 20 | $recruitUser->balance = 100; 21 | 22 | $referralUser = $this->user(); 23 | 24 | $program = ReferralProgram::create([ 25 | 'name' => 'example', 26 | 'title' => 'Test', 27 | 'description' => 'Test description', 28 | 'uri' => 'test', 29 | ]); 30 | 31 | $refLink = $program->links()->create([ 32 | 'user_id' => $recruitUser->id, 33 | ]); 34 | 35 | $refLink->relationships()->create([ 36 | 'user_id' => $referralUser->id, 37 | ]); 38 | $refLink->setRelation('user', $recruitUser); 39 | 40 | $event = new ReferralCase('example', $referralUser, 350); 41 | 42 | $recruitUser->shouldReceive('save')->once(); 43 | $mockRewardUser = m::mock(RewardUser::class)->makePartial(); 44 | 45 | $mockRewardUser->shouldAllowMockingProtectedMethods(); 46 | $mockRewardUser->shouldReceive('getReferralLink')->once()->andReturn($refLink); 47 | $mockRewardUser->handle($event); 48 | 49 | $this->assertEquals(100 + (350 * ExampleProgram::ROYALTY_PERCENT / 100), $recruitUser->balance); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Unit/MiddlewareTest.php: -------------------------------------------------------------------------------- 1 | 'test', 23 | 'title' => 'Test', 24 | 'description' => 'Test description', 25 | 'uri' => 'test', 26 | ]); 27 | 28 | $refLink = $program->links()->create([ 29 | 'user_id' => 1, 30 | ]); 31 | 32 | $this->assertEquals(0, $refLink->clicks); 33 | 34 | $request = Request::create($program->uri, parameters: [ 35 | 'ref' => $refLink->code, 36 | ]); 37 | 38 | $middleware = new StoreReferralCode(); 39 | $response = $middleware->handle($request, function ($request) { 40 | return response(''); 41 | }); 42 | 43 | // is this redirect? 44 | $this->assertEquals(302, $response->getStatusCode()); 45 | 46 | // is cookie was set 47 | $this->assertCount(1, $response->headers->getCookies()); 48 | $cookie = $response->headers->getCookies()[0]; 49 | 50 | // checking name and value of cookie 51 | $this->assertEquals('ref', $cookie->getName()); 52 | 53 | // parse value as json and check it 54 | $refCookieLinks = json_decode($cookie->getValue(), true); 55 | $this->assertArrayHasKey($refLink->id, $refCookieLinks); 56 | $this->assertEquals($cookie->getExpiresTime(), $refCookieLinks[$refLink->id]); 57 | 58 | // check if click was incremented 59 | $refLink->refresh(); 60 | $this->assertEquals(1, $refLink->clicks); 61 | 62 | 63 | // then test multiply referrals 64 | /** @var ReferralProgram $program2 */ 65 | $program2 = ReferralProgram::create([ 66 | 'name' => 'test2', 67 | 'title' => 'Test2', 68 | 'description' => 'Test description of program 2', 69 | 'uri' => 'test2', 70 | 'lifetime_minutes' => 14 * 24 * 60 // set longer lifetime 71 | ]); 72 | 73 | $refLink2 = $program2->links()->create([ 74 | 'user_id' => 1, // same user 75 | ]); 76 | 77 | $this->assertEquals(0, $refLink2->clicks); 78 | 79 | $request = Request::create($program2->uri, parameters: [ 80 | 'ref' => $refLink2->code, 81 | ], cookies: ['ref' => $cookie->getValue()]); 82 | 83 | $middleware = new StoreReferralCode(); 84 | $response = $middleware->handle($request, function ($request) { 85 | return response(''); 86 | }); 87 | 88 | // is this redirect? 89 | $this->assertEquals(302, $response->getStatusCode()); 90 | 91 | // is cookie was set 92 | $this->assertCount(1, $response->headers->getCookies()); 93 | $cookie = $response->headers->getCookies()[0]; 94 | 95 | // checking name and value of cookie 96 | $this->assertEquals('ref', $cookie->getName()); 97 | 98 | // parse value as json and check it 99 | $refCookieLinks = json_decode($cookie->getValue(), true); 100 | $this->assertCount(2, $refCookieLinks); 101 | $this->assertArrayHasKey($refLink->id, $refCookieLinks); 102 | $this->assertArrayHasKey($refLink2->id, $refCookieLinks); 103 | $this->assertEquals($cookie->getExpiresTime(), $refCookieLinks[$refLink2->id]); 104 | $this->assertTrue($refCookieLinks[$refLink2->id] > $refCookieLinks[$refLink->id]); 105 | 106 | // check if click was incremented 107 | $refLink->refresh(); 108 | $refLink2->refresh(); 109 | $this->assertEquals(1, $refLink->clicks); 110 | $this->assertEquals(1, $refLink2->clicks); 111 | 112 | $this->assertEquals($refCookieLinks, $request->get("_referrals")); 113 | } 114 | 115 | public function testUnknownReferralCode() 116 | { 117 | $program = ReferralProgram::create([ 118 | 'name' => 'test', 119 | 'title' => 'Test', 120 | 'description' => 'Test description', 121 | 'uri' => 'test', 122 | ]); 123 | 124 | $program->links()->create([ 125 | 'user_id' => 1, 126 | ]); 127 | 128 | $request = Request::create($program->uri, parameters: [ 129 | 'ref' => 'unknown', 130 | ]); 131 | 132 | Log::partialMock(); 133 | Log::shouldReceive('warning')->once(); 134 | 135 | $middleware = new StoreReferralCode(); 136 | $response = $middleware->handle($request, function () { 137 | return response(''); 138 | }); 139 | 140 | // is this redirect? 141 | $this->assertEquals(200, $response->getStatusCode()); 142 | 143 | // is cookie was set 144 | $this->assertCount(0, $response->headers->getCookies()); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /tests/Unit/Models/ReferralLinkTest.php: -------------------------------------------------------------------------------- 1 | user(); 16 | 17 | $recruitUser1 = $this->user(); 18 | 19 | $this->user(); 20 | 21 | $recruitUser2 = $this->user(); 22 | 23 | 24 | $program = ReferralProgram::create([ 25 | 'name' => 'example', 26 | 'title' => 'Test', 27 | 'description' => 'Test description', 28 | 'uri' => 'test', 29 | ]); 30 | 31 | /** @var ReferralLink $refLink */ 32 | $refLink = $program->links()->create([ 33 | 'user_id' => $referralUser->id, 34 | ]); 35 | 36 | $refLink->relationships()->create([ 37 | 'user_id' => $recruitUser1->id, 38 | ]); 39 | 40 | $refLink->relationships()->create([ 41 | 'user_id' => $recruitUser2->id, 42 | ]); 43 | 44 | $this->assertCount(2, $refLink->referredUsers()); 45 | $user_ids = $refLink->referredUsers()->map(function($user) { 46 | return $user->id; 47 | }); 48 | $this->assertEquals(2, $user_ids[0]); 49 | $this->assertEquals(4, $user_ids[1]); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/WithLoadMigrations.php: -------------------------------------------------------------------------------- 1 | app['config']->set('auth.providers.users.model', User::class); 15 | } 16 | 17 | protected function defineDatabaseMigrations(): void 18 | { 19 | $this->loadLaravelMigrations(); 20 | $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); 21 | 22 | 23 | } 24 | 25 | protected function user(array $attributes = []): User { 26 | return User::create(array_merge([ 27 | 'name' => fake()->userName, 28 | 'email' => fake()->email, 29 | 'password' => Hash::make(fake()->password), 30 | ], $attributes)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |