Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117781104
D5655.1775248053.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
15 KB
Referenced Files
None
Subscribers
None
D5655.1775248053.diff
View Options
diff --git a/src/app/Device.php b/src/app/Device.php
--- a/src/app/Device.php
+++ b/src/app/Device.php
@@ -58,93 +58,95 @@
// Before we do anything let's make sure that a device can be assigned only
// to a plan with a device SKU in a package
if ($device_packages->count() != 1) {
- throw new \Exception("A device requires a plan with a device SKU");
+ throw new \Exception("A plan with a device SKU is required");
}
foreach ($device_packages as $package) {
$this->assignPackageAndWallet($package, $wallet);
}
+
+ // Push entitlements.updated_at to one year in the future
+ $threshold = (clone $this->created_at)->addYearWithoutOverflow();
+ if ($threshold > \now()) {
+ $this->entitlements()->each(static function ($entitlement) use ($threshold) {
+ $entitlement->updated_at = $threshold;
+ $entitlement->save();
+ });
+ }
}
/**
* Assign device to (another) real user account
+ *
+ * @param string $token Device token
+ * @param User $user User claiming the device ownership
*/
- public function bindTo(User $user): void
+ public static function claim(string $token, User $user): void
{
- $wallet = $user->wallets()->first();
-
- // TODO: What if the device is already used by another (real) user?
- DB::beginTransaction();
-
- // Remove existing user association
- $this->entitlements()->delete();
-
- $device_packages = [];
+ $plan = null;
// Existing user's plan
if ($plan_id = $user->getSetting('plan_id')) {
$plan = Plan::withObjectTenantContext($user)->find($plan_id);
-
- // Find packages with a device SKU in this plan
- $device_packages = !$plan ? collect([]) : $plan->packages->filter(static function ($package) {
- foreach ($package->skus as $sku) {
- if ($sku->handler_class::entitleableClass() == self::class) {
- return true;
- }
- }
-
- return false;
- });
-
- if ($device_packages->count() != 1) {
- throw new \Exception("A device requires a plan with a device SKU");
- }
}
// TODO: Get "default" device package and assign if none found above, or just use the device SKU?
- foreach ($device_packages as $package) {
- $this->assignPackageAndWallet($package, $wallet);
+ if (!$plan || !$plan->hasSku(self::class)) {
+ throw new \Exception("A plan with a device SKU is required");
}
- // Push entitlements.updated_at to one year from the first registration
- $threshold = (clone $this->created_at)->addYearWithoutOverflow();
- if ($threshold > \now()) {
- $this->entitlements()->each(static function ($entitlement) use ($threshold) {
- $entitlement->updated_at = $threshold;
- $entitlement->save();
- });
- }
+ DB::beginTransaction();
+
+ $device = self::initDevice($token);
+ $device->assignPlan($plan, $user->wallets()->first());
DB::commit();
}
/**
- * Signup a device
+ * Setup a device record for the token. Create one if needed.
+ * Warning: It also removes any existing association with a user.
+ *
+ * @param string $token Device token
*/
- public static function signup(string $token, Plan $plan, string $password): self
+ public static function initDevice(string $token): self
{
- DB::beginTransaction();
-
// Check if a device already exists
- $device = Device::withTrashed()->where('hash', $token)->first();
+ $device = self::withTrashed()->where('hash', $token)->first();
if ($device) {
// FIXME: Should we remove the user (if it's a role=device user)?
- // FIXME: Should we bail out if the device is used by a normal user?
- // Remove the device-to-wallet connection
- $device->entitlements()->delete();
+ // Remove the existing device-to-wallet connection
+ $device->entitlements()->each(function ($entitlement) {
+ $entitlement->delete();
+ });
// Undelete the device if needed
if ($device->trashed()) {
- $device->restore();
+ $device->withoutEvents(function () use ($device) {
+ $device->restore();
+ });
}
} else {
// Create a device
$device = self::create(['hash' => $token]);
}
+ return $device;
+ }
+
+ /**
+ * Signup a device
+ */
+ public static function signup(string $token, Plan $plan, string $password): self
+ {
+ DB::beginTransaction();
+
+ // Check if a device already exists
+ $device = self::initDevice($token);
+
// Create a special account
while (true) {
$user_id = Utils::uuidInt();
@@ -166,21 +168,10 @@
]);
// Assign the device via an entitlement to the user's wallet
- $device->assignPlan($plan, $wallet = $user->wallets()->first());
-
- // Push entitlements.updated_at to one year in the future
- $threshold = (clone $device->created_at)->addYearWithoutOverflow();
- if ($threshold > \now()) {
- $device->entitlements()->each(static function ($entitlement) use ($threshold) {
- $entitlement->updated_at = $threshold;
- $entitlement->save();
- });
- }
+ $device->assignPlan($plan, $user->wallets()->first());
// FIXME: Should this bump signup_tokens.counter, as it does for user signup?
- // TODO: Trigger payment mandate creation?
-
DB::commit();
return $device;
diff --git a/src/app/Http/Controllers/API/SignupController.php b/src/app/Http/Controllers/API/SignupController.php
--- a/src/app/Http/Controllers/API/SignupController.php
+++ b/src/app/Http/Controllers/API/SignupController.php
@@ -2,6 +2,7 @@
namespace App\Http\Controllers\API;
+use App\Device;
use App\Discount;
use App\Domain;
use App\Group;
@@ -467,9 +468,17 @@
}
}
- // Bump up counter on the signup token
if (!empty($request->settings['signup_token'])) {
- SignupToken::where('id', $request->settings['signup_token'])->increment('counter');
+ $token = $request->settings['signup_token'];
+
+ // Bump up counter on the signup token
+ SignupToken::where('id', $token)->increment('counter');
+
+ // Bind the device with the user
+ if ($request->plan->hasSku(Device::class)) {
+ $device = Device::initDevice($token);
+ $device->assignPlan($request->plan, $user->wallets()->first());
+ }
}
DB::commit();
diff --git a/src/app/Http/Controllers/API/V4/DeviceController.php b/src/app/Http/Controllers/API/V4/DeviceController.php
--- a/src/app/Http/Controllers/API/V4/DeviceController.php
+++ b/src/app/Http/Controllers/API/V4/DeviceController.php
@@ -30,12 +30,26 @@
}
$device = Device::where('hash', strtoupper($token))->first();
-
- if (empty($device)) {
- return $this->errorResponse(404);
+ $user = $this->guard()->user();
+
+ if ($device) {
+ $device_wallet = $device->wallet();
+ $user_wallet = $user->wallets()->first();
+
+ // Does the existing device already belong to this user?
+ if ($device_wallet && $device_wallet->id == $user_wallet->id) {
+ response()->json([
+ 'status' => 'success',
+ 'message' => self::trans('app.device-claim-success'),
+ ]);
+ }
+ } else {
+ if (!SignupToken::where('id', strtoupper($token))->exists()) {
+ return $this->errorResponse(404);
+ }
}
- $device->bindTo($this->guard()->user());
+ Device::claim($token, $user);
return response()->json([
'status' => 'success',
diff --git a/src/app/Plan.php b/src/app/Plan.php
--- a/src/app/Plan.php
+++ b/src/app/Plan.php
@@ -134,4 +134,20 @@
return false;
}
+
+ /**
+ * Checks if the plan has a SKU assigned.
+ */
+ public function hasSku(string $class): bool
+ {
+ foreach ($this->packages as $package) {
+ foreach ($package->skus as $sku) {
+ if ($sku->handler_class::entitleableClass() == $class) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
}
diff --git a/src/tests/Feature/Controller/DeviceTest.php b/src/tests/Feature/Controller/DeviceTest.php
--- a/src/tests/Feature/Controller/DeviceTest.php
+++ b/src/tests/Feature/Controller/DeviceTest.php
@@ -95,6 +95,38 @@
$this->assertSame('success', $json['status']);
$this->assertSame('The device has been claimed successfully.', $json['message']);
+ $device = Device::where('hash', $this->hash)->first();
+ $this->assertCount(1, $device->entitlements);
+ $this->assertSame($user->wallets()->first()->id, $device->entitlements[0]->wallet_id);
+
+ // Claim a soft-deleted device, no token registered
+ $device->delete();
+ $response = $this->actingAs($user)->post('api/v4/device/' . $this->hash . '/claim', []);
+ $response->assertStatus(404);
+
+ // Claim a soft-deleted device, token registered
+ SignupToken::create(['id' => $this->hash, 'plans' => [$plan->id]]);
+ $response = $this->actingAs($user)->post('api/v4/device/' . $this->hash . '/claim', []);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('success', $json['status']);
+ $this->assertSame('The device has been claimed successfully.', $json['message']);
+ $device = Device::where('hash', $this->hash)->first();
+ $this->assertCount(1, $device->entitlements);
+ $this->assertSame($user->wallets()->first()->id, $device->entitlements[0]->wallet_id);
+
+ // Claim a non-existing device
+ $device->forceDelete();
+ $response = $this->actingAs($user)->post('api/v4/device/' . $this->hash . '/claim', []);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('success', $json['status']);
+ $this->assertSame('The device has been claimed successfully.', $json['message']);
+ $device = Device::where('hash', $this->hash)->first();
$this->assertCount(1, $device->entitlements);
$this->assertSame($user->wallets()->first()->id, $device->entitlements[0]->wallet_id);
diff --git a/src/tests/Feature/Controller/SignupTest.php b/src/tests/Feature/Controller/SignupTest.php
--- a/src/tests/Feature/Controller/SignupTest.php
+++ b/src/tests/Feature/Controller/SignupTest.php
@@ -2,6 +2,7 @@
namespace Tests\Feature\Controller;
+use App\Device;
use App\Discount;
use App\Domain;
use App\Http\Controllers\API\SignupController;
@@ -14,8 +15,10 @@
use App\SignupCode;
use App\SignupInvitation as SI;
use App\SignupToken;
+use App\Sku;
use App\User;
use App\VatRate;
+use Carbon\Carbon;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
@@ -44,6 +47,8 @@
SI::truncate();
SignupToken::truncate();
Plan::where('title', 'test')->delete();
+ Package::where('title', 'test')->delete();
+ Device::query()->forceDelete();
IP4Net::where('net_number', inet_pton('128.0.0.0'))->delete();
VatRate::query()->delete();
ReferralProgram::query()->delete();
@@ -64,6 +69,8 @@
SI::truncate();
SignupToken::truncate();
Plan::where('title', 'test')->delete();
+ Package::where('title', 'test')->delete();
+ Device::query()->forceDelete();
IP4Net::where('net_number', inet_pton('128.0.0.0'))->delete();
VatRate::query()->delete();
ReferralProgram::query()->delete();
@@ -994,6 +1001,7 @@
{
Queue::fake();
+ $sku = Sku::withEnvTenantContext()->where('title', 'device')->first();
$plan = Plan::create([
'title' => 'test',
'name' => 'Test Account',
@@ -1003,6 +1011,16 @@
'discount_rate' => 0,
'mode' => Plan::MODE_TOKEN,
]);
+ $package = Package::create([
+ 'title' => 'test',
+ 'name' => 'Device Account',
+ 'description' => 'A device account.',
+ 'discount_rate' => 0,
+ ]);
+ $plan->packages()->saveMany([$package]);
+ $package->skus()->saveMany([$sku]);
+
+ Carbon::setTestNow(Carbon::createFromDate(2025, 2, 2));
$post = [
'plan' => $plan->title,
@@ -1023,7 +1041,6 @@
// Test valid token
$token = SignupToken::create(['id' => 'abc', 'plans' => [$plan->id]]);
- $post['plan'] = $plan->title;
$response = $this->post('/api/auth/signup', $post);
$response->assertStatus(200);
@@ -1041,6 +1058,44 @@
// Token's counter bumped up
$this->assertSame(1, $token->fresh()->counter);
+
+ // Device registered
+ $device = Device::where('hash', $token->id)->first();
+ $this->assertSame($device->account->email, $user->email);
+ $entitlements = $device->wallet()->entitlements()->get();
+ $this->assertCount(1, $entitlements);
+ $this->assertSame($sku->id, $entitlements[0]->sku_id);
+ $this->assertStringContainsString('2026-02-02', $entitlements[0]->updated_at->toDateString());
+
+ // Signup a new user with the same token/device
+ Carbon::setTestNow(Carbon::createFromDate(2025, 4, 2));
+ $post = [
+ 'plan' => $plan->title,
+ 'token' => 'abc',
+ 'login' => 'signuplogin',
+ 'domain' => $this->domain,
+ 'password' => 'testtest',
+ 'password_confirmation' => 'testtest',
+ ];
+
+ $response = $this->post('/api/auth/signup', $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $user2 = User::where('email', $post['login'] . '@' . $post['domain'])->first();
+ $this->assertSame($plan->id, $user->getSetting('plan_id'));
+ $this->assertSame($token->id, $user->getSetting('signup_token'));
+ $this->assertSame(2, $token->fresh()->counter);
+
+ $device->refresh();
+ $this->assertSame($device->account->email, $user2->email);
+ $entitlements = $device->entitlements()->get();
+ $this->assertCount(1, $entitlements);
+ $this->assertSame($sku->id, $entitlements[0]->sku_id);
+ $this->assertStringContainsString('2026-02-02', $entitlements[0]->updated_at->toDateString());
+ $this->assertCount(1, $user->wallets()->first()->entitlements()->withTrashed()
+ ->where('sku_id', $sku->id)->whereNotNull('deleted_at')->get());
}
/**
diff --git a/src/tests/Feature/PlanTest.php b/src/tests/Feature/PlanTest.php
--- a/src/tests/Feature/PlanTest.php
+++ b/src/tests/Feature/PlanTest.php
@@ -2,8 +2,10 @@
namespace Tests\Feature;
+use App\Domain;
use App\Plan;
use App\Tenant;
+use App\User;
use Tests\TestCase;
class PlanTest extends TestCase
@@ -74,6 +76,22 @@
$this->assertTrue($plan->hasDomain() === true);
}
+ /**
+ * Tests for Plan::hasSku()
+ */
+ public function testHasSku(): void
+ {
+ $plan = Plan::where('title', 'individual')->first();
+
+ $this->assertFalse($plan->hasSku(Domain::class));
+ $this->assertTrue($plan->hasSku(User::class));
+
+ $plan = Plan::where('title', 'group')->first();
+
+ $this->assertTrue($plan->hasSku(Domain::class));
+ $this->assertTrue($plan->hasSku(User::class));
+ }
+
/**
* Test for a plan's cost.
*/
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Fri, Apr 3, 8:27 PM (3 h, 6 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18826340
Default Alt Text
D5655.1775248053.diff (15 KB)
Attached To
Mode
D5655: Device signup improvements and fixes
Attached
Detach File
Event Timeline