Page MenuHomePhorge

D5655.1775248053.diff
No OneTemporary

Authored By
Unknown
Size
15 KB
Referenced Files
None
Subscribers
None

D5655.1775248053.diff

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

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)

Event Timeline