From 9080cf866e7a13dc368b9c5855a40d4cc25bdc28 Mon Sep 17 00:00:00 2001 From: AmirmahdiNourkazemi Date: Sat, 6 Jun 2026 13:10:53 +0330 Subject: [PATCH] feat: add referral --- .../BreathingExerciseController.php | 4 +- app/Http/Controllers/MoodController.php | 2 +- app/Http/Controllers/UserController.php | 66 ++++++++++++++++++- app/Models/User.php | 32 +++++++++ ...00000_add_referral_code_to_users_table.php | 42 ++++++++++++ routes/api.php | 5 +- 6 files changed, 144 insertions(+), 7 deletions(-) create mode 100644 database/migrations/2026_06_06_000000_add_referral_code_to_users_table.php diff --git a/app/Http/Controllers/BreathingExerciseController.php b/app/Http/Controllers/BreathingExerciseController.php index 0739bf4..9792fd4 100644 --- a/app/Http/Controllers/BreathingExerciseController.php +++ b/app/Http/Controllers/BreathingExerciseController.php @@ -148,8 +148,8 @@ public function completeSession(Request $request) 'duration' => $data['duration'] ?? $template->duration, ]); - // Increase XP - auth()->user()->increment('xp', 10); + // Increase XP (also credits the referrer's 10% share) + auth()->user()->awardXp(10); return response()->json(['message' => 'Session completed', 'session' => $session]); } diff --git a/app/Http/Controllers/MoodController.php b/app/Http/Controllers/MoodController.php index 626fe75..2a718a2 100644 --- a/app/Http/Controllers/MoodController.php +++ b/app/Http/Controllers/MoodController.php @@ -35,7 +35,7 @@ public function storeUserMood(Request $request) ); } - $user->increment('xp', 10); + $user->awardXp(10); return response()->json([ 'message' => 'Mood saved successfully', diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 7eb6ce4..3394725 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -61,9 +61,10 @@ public function loginV2(Request $request) 'identifier' => $response['uuid'] ?? null, 'password' => config('app.default_password'), // 'name' => trim(($response['first_name'] ?? '') . ' ' . ($response['last_name'] ?? '')), - 'mobile' => $mobile, + 'mobile' => $mobile, 'first_name' => $response['first_name'] ?? null, - 'last_name' => $response['last_name'] ?? null + 'last_name' => $response['last_name'] ?? null, + 'referral_code' => $response['referral_code'] ?? null, ]); } else { // Only update name and mobile for existing user @@ -71,10 +72,13 @@ public function loginV2(Request $request) $user->last_name = $response['last_name'] ?? $user->last_name; // $user->name = trim(($response['first_name'] ?? '') . ' ' . ($response['last_name'] ?? '')); $user->mobile = $mobile ?? $user->mobile; + $user->referral_code = $response['referral_code'] ?? $user->referral_code; $user->save(); } - + // Link the referral relationship and reward the referrer (کد معرف) + $this->linkReferral($user, $response); + $access_token = $user->createToken('user')->plainTextToken; return [ @@ -111,6 +115,7 @@ private function syncUserFromStatus(array $response, ?User $user = null): User 'mobile' => $mobile, 'first_name' => $response['first_name'] ?? null, 'last_name' => $response['last_name'] ?? null, + 'referral_code' => $response['referral_code'] ?? null, 'email_verified_at' => $response['email_verified_at'] ?? null ]); } else { @@ -118,13 +123,68 @@ private function syncUserFromStatus(array $response, ?User $user = null): User $user->first_name = $response['first_name'] ?? $user->first_name; $user->last_name = $response['last_name'] ?? $user->last_name; $user->mobile = $mobile ?? $user->mobile; + $user->referral_code = $response['referral_code'] ?? $user->referral_code; $user->email_verified_at = $response['email_verified_at'] ?? $user->email_verified_at; $user->save(); } + // Link the referral relationship and reward the referrer (کد معرف) + $this->linkReferral($user, $response); + return $user; } + /** + * Resolve the approagency referrer (by uuid/identifier) to a local + * meditation user, record the relationship once, and grant the referrer + * their per-invite points. The free-month subscription milestone is + * handled in approagency, where products/plans/transactions are managed. + */ + private function linkReferral(User $user, array $response): void + { + if ($user->referred_by) { + return; // already linked — never reward twice + } + + $referrerUuid = $response['referrer_uuid'] ?? null; + if (!$referrerUuid) { + return; + } + + $referrer = User::where('identifier', $referrerUuid)->first(); + if (!$referrer || $referrer->id === $user->id) { + return; + } + + $user->referred_by = $referrer->id; + $user->save(); + + // 100 points per successful invite + $referrer->increment('referral_points', User::REFERRAL_POINTS_PER_INVITE); + } + + /** + * Referral dashboard data (دعوت دوستان page). + */ + public function referral(Request $request) + { + $user = auth()->user(); + $invites = $user->referrals()->count(); + $target = User::REFERRAL_SUBSCRIPTION_TARGET; + + return response()->json([ + 'referral_code' => $user->referral_code, + 'referral_points' => (int) $user->referral_points, + 'successful_invites' => $invites, + 'subscription_target' => $target, + 'remaining_to_subscription' => max(0, $target - $invites), + 'rewards' => [ + 'points_per_invite' => User::REFERRAL_POINTS_PER_INVITE, + 'friend_points_share_percent' => (int) (User::REFERRAL_SHARE * 100), + 'subscription_invite_target' => $target, + ], + ]); + } public function loginForeginer(Request $request) { diff --git a/app/Models/User.php b/app/Models/User.php index afc4ef5..b84d7c1 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -18,6 +18,12 @@ class User extends Authenticatable 'male' => 1, 'female' => 2, ]; + + // Referral program (کد معرف) + const REFERRAL_POINTS_PER_INVITE = 100; // points granted to the referrer per successful invite + const REFERRAL_SHARE = 0.10; // referrer keeps 10% of each friend's earned xp, forever + const REFERRAL_SUBSCRIPTION_TARGET = 10; // successful invites needed for the free-month milestone + /** * The attributes that are mass assignable. * @@ -69,6 +75,32 @@ public function otpTokens() return $this->hasMany(OtpTokens::class); } + public function referrer() + { + return $this->belongsTo(User::class, 'referred_by'); + } + + public function referrals() + { + return $this->hasMany(User::class, 'referred_by'); + } + + /** + * Award xp to this user and, if they were referred, credit the + * referrer their permanent 10% share of the earned points. + */ + public function awardXp(int $amount): void + { + $this->increment('xp', $amount); + + if ($this->referred_by) { + $share = (int) floor($amount * self::REFERRAL_SHARE); + if ($share > 0) { + self::where('id', $this->referred_by)->increment('referral_points', $share); + } + } + } + public function transactions() { return $this->hasMany(Transaction::class); diff --git a/database/migrations/2026_06_06_000000_add_referral_code_to_users_table.php b/database/migrations/2026_06_06_000000_add_referral_code_to_users_table.php new file mode 100644 index 0000000..9839267 --- /dev/null +++ b/database/migrations/2026_06_06_000000_add_referral_code_to_users_table.php @@ -0,0 +1,42 @@ +string('referral_code', 12)->nullable()->after('identifier'); + } + // Local meditation user who referred this user (resolved from approagency's referrer_uuid). + if (!Schema::hasColumn('users', 'referred_by')) { + $table->foreignId('referred_by')->nullable()->after('referral_code') + ->constrained('users')->nullOnDelete(); + } + // Points earned through referrals (100 per invite + 10% of friends' xp). + if (!Schema::hasColumn('users', 'referral_points')) { + $table->unsignedInteger('referral_points')->default(0)->after('referred_by'); + } + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + if (Schema::hasColumn('users', 'referred_by')) { + $table->dropForeign(['referred_by']); + $table->dropColumn('referred_by'); + } + foreach (['referral_code', 'referral_points'] as $column) { + if (Schema::hasColumn('users', $column)) { + $table->dropColumn($column); + } + } + }); + } +}; diff --git a/routes/api.php b/routes/api.php index e1360b6..af84e54 100644 --- a/routes/api.php +++ b/routes/api.php @@ -78,9 +78,12 @@ Route::post('/profile', [UserController::class, 'updateProfile']); Route::get('/profile', [UserController::class, 'profile']); - ////-- leader board + ////-- leader board Route::get('/leader-board', [UserController::class, 'leaderBoard']); + ////-- referral (دعوت دوستان) + Route::get('/referral', [UserController::class, 'referral']); + ///-- mood routes Route::post('/moods/today', [MoodController::class, 'storeUserMood']);