diff --git a/.github/workflows/run-linter.yml b/.github/workflows/run-linter.yml new file mode 100644 index 0000000..8285dfd --- /dev/null +++ b/.github/workflows/run-linter.yml @@ -0,0 +1,21 @@ +name: "Run Linter" + +on: pull_request + +jobs: + lint: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Lint with Pint + uses: aglipanci/laravel-pint-action@2.3.0 + + - name: Commit linted files + uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: "fix: Files linted with Pint" + diff --git a/README.md b/README.md index ec0c393..d2a5234 100644 --- a/README.md +++ b/README.md @@ -591,6 +591,59 @@ public Activity $activity, public Streak $streak, ``` +## 🥶 Streak Freezing + +Streaks can be frozen, which means they will not be broken if a day is skipped. This is useful for when you want to allow users to take a break from an activity without losing their streak. + +The freeze duration is a configurable option in the config file. + +```php +'freeze_duration' => env(key: 'STREAK_FREEZE_DURATION', default: 1), +``` + +### Freeze a Streak + +Fetch the activity you want to freeze and pass it to the `freezeStreak` method. A second parameter can be passed to set the duration of the freeze. The default is `1` day (as set in the config) + +A `StreakFrozen` Event is ran when a streak is frozen. + +```php +$user->freezeStreak(activity: $activity); + +$user->freezeStreak(activity: $activity, days: 5); // freeze for 5 days +``` + +### Unfreeze a Streak + +The opposite of freezing a streak is unfreezing it. This will allow the streak to be broken again. + +A `StreakUnfrozen` Event is run when a streak is unfrozen. + +```php +$user->unfreezeStreak($activity); +``` + +### Check if a Streak is Frozen + +```php +$user->isStreakFrozen($activity); +``` + +### Events + +**StreakFrozen** - When a streak is frozen. + +```php +public int $frozenStreakLength, +public Carbon $frozenUntil, +``` + +**StreakUnfrozen** - When a streak is unfrozen. + +``` +No data is sent with this event +``` + # Testing ``` diff --git a/config/level-up.php b/config/level-up.php index c87d296..fb251cd 100644 --- a/config/level-up.php +++ b/config/level-up.php @@ -86,4 +86,14 @@ 'archive_streak_history' => [ 'enabled' => env(key: 'ARCHIVE_STREAK_HISTORY_ENABLED', default: true), ], + + /* + | ------------------------------------------------------------------------- + | Default Streak Freeze Time + | ------------------------------------------------------------------------- + | + | Set the default time in days that a streak will be frozen for. + | + */ + 'freeze_duration' => env(key: 'STREAK_FREEZE_DURATION', default: 1), ]; diff --git a/database/migrations/add_streak_freeze_feature_columns_to_streaks_table.php.stub b/database/migrations/add_streak_freeze_feature_columns_to_streaks_table.php.stub new file mode 100644 index 0000000..d4aea0d --- /dev/null +++ b/database/migrations/add_streak_freeze_feature_columns_to_streaks_table.php.stub @@ -0,0 +1,23 @@ +after('activity_at', function (Blueprint $table) { + $table->timestamp('frozen_until')->nullable(); + }); + }); + } + + public function down(): void + { + Schema::table('streaks', function (Blueprint $table) { + $table->dropColumn('frozen_until'); + }); + } +}; diff --git a/src/Concerns/HasStreaks.php b/src/Concerns/HasStreaks.php index 721b63a..62b6ec6 100644 --- a/src/Concerns/HasStreaks.php +++ b/src/Concerns/HasStreaks.php @@ -4,9 +4,12 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Facades\Event; use LevelUp\Experience\Events\StreakBroken; +use LevelUp\Experience\Events\StreakFrozen; use LevelUp\Experience\Events\StreakIncreased; use LevelUp\Experience\Events\StreakStarted; +use LevelUp\Experience\Events\StreakUnfroze; use LevelUp\Experience\Models\Activity; use LevelUp\Experience\Models\Streak; use LevelUp\Experience\Models\StreakHistory; @@ -18,6 +21,8 @@ public function recordStreak(Activity $activity): void // If the user doesn't have a streak for this activity, start a new one if (! $this->hasStreakForActivity(activity: $activity)) { $this->startNewStreak($activity); + + return; } $diffInDays = $this->getStreakLastActivity($activity) @@ -25,6 +30,11 @@ public function recordStreak(Activity $activity): void ->startOfDay() ->diffInDays(now()->startOfDay()); + // Checking to see if the streak is frozen + if ($this->getStreakLastActivity($activity)->frozen_until && now()->lessThan($this->getStreakLastActivity($activity)->frozen_until)) { + return; + } + if ($diffInDays === 0) { return; } @@ -125,10 +135,29 @@ public function hasStreakToday(Activity $activity): bool ->isToday(); } - // public function getLongestStreak(Activity $activity): int - // { - // return $this->streaks() - // ->whereBelongsTo(related: $activity) - // ->max(column: 'count'); - // } + public function freezeStreak(Activity $activity, int $days = null): bool + { + $days = $days ?? config(key: 'level-up.freeze_duration'); + + Event::dispatch(new StreakFrozen( + frozenStreakLength: $days, + frozenUntil: now()->addDays(value: $days)->startOfDay() + )); + + return $this->getStreakLastActivity($activity) + ->update(['frozen_until' => now()->addDays(value: $days)->startOfDay()]); + } + + public function unFreezeStreak(Activity $activity): bool + { + Event::dispatch(new StreakUnfroze()); + + return $this->getStreakLastActivity($activity) + ->update(['frozen_until' => null]); + } + + public function isStreakFrozen(Activity $activity): bool + { + return ! is_null($this->getStreakLastActivity($activity)->frozen_until); + } } diff --git a/src/Events/StreakFrozen.php b/src/Events/StreakFrozen.php new file mode 100644 index 0000000..aa22cc5 --- /dev/null +++ b/src/Events/StreakFrozen.php @@ -0,0 +1,14 @@ + 'datetime', + 'frozen_until' => 'datetime', ]; public function user(): BelongsTo diff --git a/tests/Concerns/HasStreaksTest.php b/tests/Concerns/HasStreaksTest.php index 429e9a4..ad6bbce 100644 --- a/tests/Concerns/HasStreaksTest.php +++ b/tests/Concerns/HasStreaksTest.php @@ -2,8 +2,10 @@ use Illuminate\Support\Facades\Event; use LevelUp\Experience\Events\StreakBroken; +use LevelUp\Experience\Events\StreakFrozen; use LevelUp\Experience\Events\StreakIncreased; use LevelUp\Experience\Events\StreakStarted; +use LevelUp\Experience\Events\StreakUnfroze; use LevelUp\Experience\Models\Activity; use function Spatie\PestPluginTestTime\testTime; @@ -176,3 +178,67 @@ 'ended_at' => now()->subDays(value: 2), ]); }); + +test(description: 'a streak can be frozen', closure: function () { + Event::fake(); + + $this->user->recordStreak($this->activity); + + $this->user->freezeStreak($this->activity); + + expect($this->user->streaks->first()->frozen_until)->toBeCarbon(now()->addDays()->startOfDay()); + + Event::assertDispatched( + event: StreakFrozen::class, + callback: fn (StreakFrozen $event): bool => $event->frozenStreakLength === config(key: 'level-up.freeze_duration') + && $event->frozenUntil->isTomorrow(), + ); +}); + +test(description: 'a streak can be unfrozen', closure: function () { + Event::fake(); + + $this->user->recordStreak($this->activity); + + $this->user->freezeStreak($this->activity); + + expect($this->user->streaks->first()->frozen_until)->toBeCarbon(now()->addDays()->startOfDay()); + + $this->user->unFreezeStreak($this->activity); + + expect($this->user->isStreakFrozen($this->activity))->toBeFalse(); + + Event::assertDispatched(event: StreakUnfroze::class); +}); + +test(description: 'when a streak is frozen, it does not break', closure: function () { + $this->user->recordStreak($this->activity); + + testTime()->addDays(); + $this->user->recordStreak($this->activity); + + expect($this->user->getCurrentStreakCount($this->activity))->toBe(expected: 2); + + $this->user->freezeStreak($this->activity); + + testTime()->addDays(); + $this->user->recordStreak($this->activity); + + expect($this->user->getCurrentStreakCount($this->activity))->toBe(expected: 3); +}); + +test('when a streak is frozen and freeze duration has passed, streak count will reset', function () { + $this->user->recordStreak($this->activity); + + testTime()->addDays(); + $this->user->recordStreak($this->activity); + + expect($this->user->getCurrentStreakCount($this->activity))->toBe(expected: 2); + + $this->user->freezeStreak($this->activity); + + testTime()->addDays(2); + $this->user->recordStreak($this->activity); + + expect($this->user->getCurrentStreakCount($this->activity))->toBe(expected: 1); +}); diff --git a/tests/TestCase.php b/tests/TestCase.php index aff49c4..d97e6fa 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -38,31 +38,23 @@ protected function getEnvironmentSetUp($app): void $table->timestamps(); }); - $migration = include __DIR__.'/../database/migrations/create_levels_table.php.stub'; - $migration->up(); - - $migration = include __DIR__.'/../database/migrations/create_experiences_table.php.stub'; - $migration->up(); - - $migration = include __DIR__.'/../database/migrations/add_level_relationship_to_users_table.php.stub'; - $migration->up(); - - $migration = include __DIR__.'/../database/migrations/create_experience_audits_table.php.stub'; - $migration->up(); - - $migration = include __DIR__.'/../database/migrations/create_achievements_table.php.stub'; - $migration->up(); - - $migration = include __DIR__.'/../database/migrations/create_achievement_user_pivot_table.php.stub'; - $migration->up(); - - $migration = include __DIR__.'/../database/migrations/create_streak_activities_table.php.stub'; - $migration->up(); + $migrationFiles = [ + 'create_levels_table', + 'create_experiences_table', + 'add_level_relationship_to_users_table', + 'create_experience_audits_table', + 'create_achievements_table', + 'create_achievement_user_pivot_table', + 'create_streak_activities_table', + 'create_streaks_table', + 'create_streak_histories_table', + 'add_streak_freeze_feature_columns_to_streaks_table', + ]; - $migration = include __DIR__.'/../database/migrations/create_streaks_table.php.stub'; - $migration->up(); + foreach ($migrationFiles as $migrationFile) { + $migration = include __DIR__."/../database/migrations/$migrationFile.php.stub"; - $migration = include __DIR__.'/../database/migrations/create_streak_histories_table.php.stub'; - $migration->up(); + $migration->up(); + } } }