Conversation
Tighten version constraints for v2.0: - PHP ^8.3 (minimum for Laravel 13) - Laravel ^12.0|^13.0 (only actively supported versions) - Update dev dependencies to drop legacy major versions - Simplify CI matrix to PHP 8.3/8.4 with Laravel 12/13
- Rewrite rector.php to fluent RectorConfig API with PHP 8.3 sets - Add pint.json from approval package with strict rules - Add rector/rector and driftingly/rector-laravel as dev deps - Disable mb_str_functions (requires PHP 8.4) and protected_to_private (breaks Eloquent scopes)
- Add declare(strict_types=1) to all PHP files - Apply Rector PHP 8.3 rules (closure return types, arrow functions, etc.) - Apply Pint Laravel preset (class element ordering, PHPDoc to native types) - Convert fake() property access to method calls in factories - Add @extends annotations to factory classes
Breaking changes: - levelUp() throws InvalidArgumentException when level doesn't exist - deductPoints() throws when user has no experience record - Level::add() only accepts array arguments (scalar form removed) - incrementAchievementProgress() throws when user lacks achievement - AchievementUser::scopeWithProgress() removed (dead code) - getStreakLastActivity() returns ?Streak (was Streak) - grantAchievement($progress) typed as ?int (was untyped) Bug fixes: - levelUp() event dispatch: capture previous level before associate() - StreakBroken event now correctly filters by activity - nextLevelAt() null guard and division-by-zero guard - freezeStreak() casts config to int for strict_types - Level::add() catches UniqueConstraintViolationException (was Throwable) - PointsIncreasedListener null guard on highestAchievableLevel - Config key corrected from level-up.streaks.foreign_key to level-up.user.foreign_key Improvements: - Cache getStreakLastActivity() result in recordStreak() - Simplify getCurrentStreakCount() to nullsafe operator - Nullsafe operators on all getStreakLastActivity() callers - Standardise on event() helper (remove Event facade usage) - Remove empty register()/boot() from EventServiceProvider - Type LeaderboardService::$userModel as string - Narrow MultiplierService callback return type to int - nextLevelAt() treats null XP threshold as 0 baseline
New tests: - levelUp() throws on non-existent level - levelUp() associates correct level (happy path) - levelUp() respects level cap - deductPoints() throws when no experience record - nextLevelAt() returns 0 when current level missing - incrementAchievementProgress() throws when user lacks achievement - hasStreakToday() returns false when no streak exists - freezeStreak() returns false when no streak exists Modernisation: - Apply Rector/Pint code style across all test files - Fix LevelTest to use array form for Level::add() - Add Pest IDE helper for Intelephense support - Use Schema facade in TestCase - Import Date facade in GiveExperienceTest
- PHP 8.3 and 8.4 only (drop 8.1, 8.2) - Laravel 12 and 13 only (drop 10, 11) - Remove exclusion rules (no longer needed) - Fix YAML block scalar from | to >
- README: update Laravel version badge to ^12|^13 - README: add note about deductPoints/setPoints throwing on missing experience - UPGRADE.md: add comprehensive v1→v2 migration guide covering all breaking changes
Add migrations for tiers table, tier_id foreign keys on experiences and achievements tables, and alter experience_audits type column from enum to string for easier future extensibility. Register TierDirection and AuditType enum extensions (TierUp, TierDown), tier model in config, migrations in service provider, and UserTierUpdated event in EventServiceProvider.
Tier model with Tier::add() factory method, HasTiers trait providing getTier(), getNextTier(), tierProgress(), nextTierAt(), isAtTier(), and isAtOrAboveTier() methods. Supports configurable demotion with stored high-water mark when demotion is disabled. Includes UserTierUpdated event (readonly, typed TierDirection enum), UserTierUpdatedListener for audit trail, TierExistsException and TierRequirementNotMet exceptions, and tier relationships on Experience and Achievement models.
…aderboard Tier-based multipliers in GiveExperience::applyTierMultiplier(), tier-gated achievements in HasAchievements::grantAchievement(), tier-scaled streak freeze in HasStreaks::getFreezeDurationForTier(), tier-scoped leaderboard via LeaderboardService::forTier(), and automatic tier promotion/demotion detection in PointsIncreased and PointsDecreased listeners with ID short-circuit optimization.
TierTest (model creation, metadata, duplicate handling), HasTiersTest (getTier, getNextTier, tierProgress, nextTierAt, isAtTier, isAtOrAboveTier, promotion events, demotion events, high-water mark, tier_id storage), HasTiersIntegrationTest (tier-gated achievements, tier multipliers, tier-scaled streak freeze, tier-scoped leaderboard), and UserTierUpdatedListenerTest (audit trail for promotions and demotions).
Wrap Tier::add() in a DB transaction with UniqueConstraintViolation catch to prevent partial writes on concurrent inserts. Guard getTier() calls in GiveExperience, HasAchievements, and HasStreaks with method_exists() so models without HasTiers don't crash on upgrade.
README: add Tiers section with usage examples for tier definition, querying, demotion, multipliers, gated achievements, scaled streak freezes, scoped leaderboards, and events. Update config example to include tiers config. UPGRADE: add v2 tiers migration requirements including the alter_experience_audits_type_to_string migration and HasTiers trait instructions.
Add three Boost-compatible files for AI-assisted development: - guidelines/core.blade.php: package awareness guideline - skills/level-up-development: comprehensive usage guide - skills/level-up-upgrade-v2: automated v1→v2 upgrade workflow
feat: add tier system with cross-feature integrations
Add migrations for DB-backed multiplier system: - multipliers table (name, multiplier, is_active, starts_at, expires_at) - multiplier_scopes polymorphic table with composite index - multipliers JSON column on experience_audits for audit trail
Multiplier model with query scopes (active, forUser, scheduled, expired), polymorphic scoping via MultiplierScope, and validation (multiplier > 0).
Replace path/namespace config with stack_strategy (compound/additive/highest). Add Multiplier and MultiplierScope to configurable models array. Remove tiers.multipliers config key (replaced by DB-scoped multipliers).
Rewrite multiplier logic in GiveExperience trait: - Query active DB multipliers scoped to user/tier - Apply configurable stacking strategy (compound/additive/highest) - Inline multiplier param changed from ?int to int|float|null - Fire MultiplierApplied event when multipliers are applied - Record applied multipliers in audit trail - Remove withMultiplierData(), getMultipliers(), applyTierMultiplier()
- Expired/future challenge enrollment throws - Unenroll from active/completed/not-enrolled challenges - Repeatable re-enrollment resets progress - Completion percentage calculation - ChallengeEnrolled/ChallengeUnenrolled events - Auto-enroll baseline regression test (existing points) - Re-entrancy guard direct test - Validation rejects invalid condition/reward types and keys - Custom condition class validation at creation time - Listener try/catch isolation - Tier: isAtTier/isAtOrAboveTier with non-existent name - Tier: demotion below all tiers sets tier_id to null
- Add Challenges section to README with usage, conditions, rewards, events - Add Challenges section to UPGRADE.md for v2.0 migration guide - Add challenges config to README config block - Add .env to .gitignore - Fix stale search-docs reference in Boost guidelines
Replace app(abstract:) with resolve() (positional arg) — resolve() names its first parameter $name, not $abstract, causing silent failures in challenge evaluation via catch(Throwable). Also: throw_unless/throw_if helpers, type hints, method reordering, instanceof check in leaderboard filtering.
Match source changes: resolve() positional args, unqualified class references, explicit query builder calls.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…factor # Conflicts: # config/level-up.php # src/Concerns/GiveExperience.php # src/Models/Level.php # src/Services/MultiplierService.php # tests/Models/LevelTestWithCustomModel.php
…actor # Conflicts: # .gitignore # UPGRADE.md # config/level-up.php # src/Concerns/GiveExperience.php # src/LevelUpServiceProvider.php # tests/Models/LevelTestWithCustomModel.php # tests/TestCase.php
…strategy Adds tests for scopeTo(), tiers(), users() relationships, starts_at/expires_at validation, and unknown stacking strategy error path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
scopeTo() now uses firstOrCreate instead of create, so calling it twice with the same model no longer throws a unique constraint error. Also adds a test verifying a multiplier scoped to both a user and their tier is only applied once.
Replace app() with direct instantiation when resolving the Tier morph class. Avoids container overhead on every addPoints() call.
- Cache single now() call in active() scope instead of creating three separate Carbon instances per query - Remove readonly from MultiplierApplied event properties to match the convention used by all other events in the package
Extract resolveMultipliers() and buildMultiplierAuditData() from addPoints() and dispatchEvent() for clearer separation of concerns. Rename nested closure params in forUser() scope from $q to descriptive names ($outer, $scopeQuery, $match, $tierMatch).
refactor: replace class-based multipliers with DB-backed system
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The #[Scope] attribute was added in v12.4.0 and void-returning scopes were fixed in v12.6.0, causing prefer-lowest CI to fail on v12.1.1.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
v2.0 major release bringing three new systems and a platform upgrade:
Tier System
UserTierUpdatedevent with promotion/demotion directionChallenge System
ChallengeConditioncontractDB-Backed Multipliers
MultiplierandMultiplierScopemodels replacing class-based systemscopeTo()MultiplierAppliedevent with full audit trailPlatform
Bug Fixes
resolve()named parameter bug that silently broke challenge evaluationscopeTo()now idempotent — no duplicate scopes on repeated callsTest Coverage
Test Coverage Audit: 99.7% — all code paths covered across 9 key source files.
Tests: 18 test files, 216 tests, 417 assertions — all passing.
Pre-Landing Review
No issues found.
Adversarial Review
Claude adversarial subagent: 23 findings reviewed, all false positives (missed existing demotion listener, misunderstood intentional design).
Codex: unavailable (auth failure).
Scope Drift
Scope Check: CLEAN — all changes align with v2.0 stated intent.
Test plan
composer lint)🤖 Generated with Claude Code