I context logic & tests
This commit is contained in:
parent
2f19523dcf
commit
b780d154f2
|
|
@ -4,4 +4,4 @@ vendor
|
||||||
composer.lock
|
composer.lock
|
||||||
.idea/
|
.idea/
|
||||||
workbench
|
workbench
|
||||||
.phpunit.result.cache
|
.phpunit.*
|
||||||
|
|
@ -21,7 +21,7 @@ trait HasRoles
|
||||||
config('roles.models.role', \Blax\Roles\Models\Role::class),
|
config('roles.models.role', \Blax\Roles\Models\Role::class),
|
||||||
'member',
|
'member',
|
||||||
$pivotTable
|
$pivotTable
|
||||||
)->withPivot('expires_at', 'created_at', 'updated_at')
|
)->withPivot('expires_at', 'context', 'created_at', 'updated_at')
|
||||||
->withTimestamps()
|
->withTimestamps()
|
||||||
->where(function ($q) use ($pivotTable) {
|
->where(function ($q) use ($pivotTable) {
|
||||||
$q->where($pivotTable . '.expires_at', '>', now())
|
$q->where($pivotTable . '.expires_at', '>', now())
|
||||||
|
|
@ -201,6 +201,71 @@ trait HasRoles
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extend or create a role membership, scoped by an origin identifier stored in `context`.
|
||||||
|
*
|
||||||
|
* This allows multiple independent role_member records for the same role+user (e.g.,
|
||||||
|
* one from a subscription and one from a day-pass purchase). Each origin tracks its
|
||||||
|
* own expiry independently.
|
||||||
|
*
|
||||||
|
* - If an active (non-expired) record with the same origin exists → extend it
|
||||||
|
* - If only expired records exist for this origin, or no record exists → create new
|
||||||
|
*
|
||||||
|
* @param int|string|Role $role The role to assign/extend
|
||||||
|
* @param int $hours Duration in hours
|
||||||
|
* @param string $originName Human-readable label (e.g., product name)
|
||||||
|
* @param string $originValue Lookup key (e.g., "ProductPrice:uuid")
|
||||||
|
* @param bool $forceExpiry If true, set expiration even on records with null expires_at
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function extendOrAddRoleByOrigin(int|string|Role $role, int $hours, string $originName, string $originValue, bool $forceExpiry = false)
|
||||||
|
{
|
||||||
|
$hours = (int) $hours;
|
||||||
|
if ($hours <= 0) {
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve role
|
||||||
|
if (is_string($role) && !is_numeric($role)) {
|
||||||
|
$role = config('roles.models.role', \Blax\Roles\Models\Role::class)::firstOrCreate([
|
||||||
|
'name' => $role,
|
||||||
|
], [
|
||||||
|
'slug' => str()->slug($role)
|
||||||
|
]);
|
||||||
|
} elseif (is_numeric($role)) {
|
||||||
|
$role = config('roles.models.role', \Blax\Roles\Models\Role::class)::find($role);
|
||||||
|
} elseif (!$role instanceof Role) {
|
||||||
|
throw new \InvalidArgumentException('Role must be a string, numeric ID, or an instance of Role.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$role) {
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
$roleMemberModel = config('roles.models.role_member', \Blax\Roles\Models\RoleMember::class);
|
||||||
|
|
||||||
|
// Look for an active (non-expired) record from the same origin
|
||||||
|
$existing = $roleMemberModel::where('role_id', $role->id)
|
||||||
|
->where('member_id', $this->getKey())
|
||||||
|
->where('member_type', $this->getMorphClass())
|
||||||
|
->whereJsonContains('context->origin_value', $originValue)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
$existing->extendByHours($hours, $forceExpiry);
|
||||||
|
} else {
|
||||||
|
$this->roles()->attach($role->id, [
|
||||||
|
'expires_at' => now()->addHours($hours),
|
||||||
|
'context' => json_encode([
|
||||||
|
'origin_name' => $originName,
|
||||||
|
'origin_value' => $originValue,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the memberable has any of the given roles
|
* Checks if the memberable has any of the given roles
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -490,4 +490,148 @@ class HasRolesTest extends TestCase
|
||||||
$this->assertEquals('admin', $role1->slug);
|
$this->assertEquals('admin', $role1->slug);
|
||||||
$this->assertEquals('admin-1', $role2->slug);
|
$this->assertEquals('admin-1', $role2->slug);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── extendOrAddRoleByOrigin ────────────────────────────────
|
||||||
|
|
||||||
|
public function test_extend_by_origin_creates_new_record_with_context(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$role = Role::create(['name' => 'Premium', 'slug' => 'premium']);
|
||||||
|
|
||||||
|
$user->extendOrAddRoleByOrigin($role, 24, 'Monthly Sub', 'ProductPrice:abc-123');
|
||||||
|
|
||||||
|
$membership = DB::table(config('roles.table_names.role_member'))
|
||||||
|
->where('role_id', $role->id)
|
||||||
|
->where('member_id', $user->id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$this->assertNotNull($membership);
|
||||||
|
$context = json_decode($membership->context, true);
|
||||||
|
$this->assertEquals('Monthly Sub', $context['origin_name']);
|
||||||
|
$this->assertEquals('ProductPrice:abc-123', $context['origin_value']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_extend_by_origin_extends_active_record_from_same_origin(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$role = Role::create(['name' => 'Premium', 'slug' => 'premium']);
|
||||||
|
|
||||||
|
// First purchase — creates record
|
||||||
|
$user->extendOrAddRoleByOrigin($role, 24, 'Monthly Sub', 'ProductPrice:abc-123');
|
||||||
|
|
||||||
|
// Second purchase from same price — should extend, not create new
|
||||||
|
$user->extendOrAddRoleByOrigin($role, 24, 'Monthly Sub', 'ProductPrice:abc-123');
|
||||||
|
|
||||||
|
$count = DB::table(config('roles.table_names.role_member'))
|
||||||
|
->where('role_id', $role->id)
|
||||||
|
->where('member_id', $user->id)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$this->assertEquals(1, $count);
|
||||||
|
|
||||||
|
// Should be ~48 hours from now
|
||||||
|
$membership = DB::table(config('roles.table_names.role_member'))
|
||||||
|
->where('role_id', $role->id)
|
||||||
|
->where('member_id', $user->id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$expiresAt = \Carbon\Carbon::parse($membership->expires_at);
|
||||||
|
$this->assertTrue($expiresAt->gt(now()->addHours(40)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_extend_by_origin_creates_separate_records_for_different_origins(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$role = Role::create(['name' => 'Premium', 'slug' => 'premium']);
|
||||||
|
|
||||||
|
// Subscription creates one record
|
||||||
|
$user->extendOrAddRoleByOrigin($role, 720, 'Monthly Sub', 'ProductPrice:sub-monthly');
|
||||||
|
|
||||||
|
// Day pass creates a separate record
|
||||||
|
$user->extendOrAddRoleByOrigin($role, 24, 'Day Pass', 'ProductPrice:day-pass');
|
||||||
|
|
||||||
|
$count = DB::table(config('roles.table_names.role_member'))
|
||||||
|
->where('role_id', $role->id)
|
||||||
|
->where('member_id', $user->id)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
// Both should coexist
|
||||||
|
$this->assertEquals(2, $count);
|
||||||
|
|
||||||
|
// User should have the role
|
||||||
|
$this->assertTrue($user->hasRole($role));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_extend_by_origin_creates_new_record_when_previous_expired(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$role = Role::create(['name' => 'Premium', 'slug' => 'premium']);
|
||||||
|
|
||||||
|
// Insert an expired record from a subscription
|
||||||
|
DB::table(config('roles.table_names.role_member'))->insert([
|
||||||
|
'role_id' => $role->id,
|
||||||
|
'member_id' => $user->id,
|
||||||
|
'member_type' => $user->getMorphClass(),
|
||||||
|
'expires_at' => now()->subDay(),
|
||||||
|
'context' => json_encode(['origin_name' => 'Monthly Sub', 'origin_value' => 'ProductPrice:sub-monthly']),
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Same price triggers again — expired record should NOT be extended, new one created
|
||||||
|
$user->extendOrAddRoleByOrigin($role, 720, 'Monthly Sub', 'ProductPrice:sub-monthly');
|
||||||
|
|
||||||
|
$count = DB::table(config('roles.table_names.role_member'))
|
||||||
|
->where('role_id', $role->id)
|
||||||
|
->where('member_id', $user->id)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
// Old expired + new active = 2 records
|
||||||
|
$this->assertEquals(2, $count);
|
||||||
|
|
||||||
|
// User should have the role (active one)
|
||||||
|
$this->assertTrue($user->hasRole($role));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_extend_by_origin_with_zero_hours_does_nothing(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$role = Role::create(['name' => 'Zero', 'slug' => 'zero']);
|
||||||
|
|
||||||
|
$user->extendOrAddRoleByOrigin($role, 0, 'Test', 'ProductPrice:test');
|
||||||
|
|
||||||
|
$count = DB::table(config('roles.table_names.role_member'))
|
||||||
|
->where('role_id', $role->id)
|
||||||
|
->where('member_id', $user->id)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$this->assertEquals(0, $count);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_extend_by_origin_force_expiry_sets_expiration_on_null(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$role = Role::create(['name' => 'Perm', 'slug' => 'perm']);
|
||||||
|
|
||||||
|
// Insert a permanent (null expires_at) record with matching origin
|
||||||
|
DB::table(config('roles.table_names.role_member'))->insert([
|
||||||
|
'role_id' => $role->id,
|
||||||
|
'member_id' => $user->id,
|
||||||
|
'member_type' => $user->getMorphClass(),
|
||||||
|
'expires_at' => null,
|
||||||
|
'context' => json_encode(['origin_name' => 'Sub', 'origin_value' => 'ProductPrice:sub']),
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// With forceExpiry=true, should set expiration even on null expires_at
|
||||||
|
$user->extendOrAddRoleByOrigin($role, 24, 'Sub', 'ProductPrice:sub', true);
|
||||||
|
|
||||||
|
$membership = DB::table(config('roles.table_names.role_member'))
|
||||||
|
->where('role_id', $role->id)
|
||||||
|
->where('member_id', $user->id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$this->assertNotNull($membership->expires_at);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue