Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117811279
D5655.1775277271.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
28 KB
Referenced Files
None
Subscribers
None
D5655.1775277271.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,136 @@
// 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): self
{
- $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;
- }
- }
+ // TODO: Get "default" device package and assign if none found above, or just use the device SKU?
- return false;
- });
+ if (!$plan || !$plan->hasSku(self::class)) {
+ throw new \Exception("A plan with a device SKU is required");
+ }
- if ($device_packages->count() != 1) {
- throw new \Exception("A device requires a plan with a device SKU");
- }
+ DB::beginTransaction();
+
+ $device = self::initDevice($token);
+ $device->assignPlan($plan, $user->wallets()->first());
+
+ DB::commit();
+
+ return $device;
+ }
+
+ /**
+ * Get a default plan for a device signup
+ *
+ * @param string|SignupToken $token Device token
+ * @param bool $isDevice Get plan for a device, rather than a user
+ */
+ public static function defaultPlan($token, bool $isDevice): ?Plan
+ {
+ if (is_string($token)) {
+ $token = SignupToken::find(strtoupper($token));
}
- // TODO: Get "default" device package and assign if none found above, or just use the device SKU?
+ if (empty($token)) {
+ return null;
+ }
- foreach ($device_packages as $package) {
- $this->assignPackageAndWallet($package, $wallet);
+ $plans = Plan::withEnvTenantContext()
+ ->where('mode', Plan::MODE_TOKEN)
+ ->whereIn('id', $token->plans)
+ // TODO: Instead of using a prefix match, we should check the plan SKUs
+ ->whereLike('title', $isDevice ? 'device-%' : 'user-%')
+ ->get()
+ ->keyBy('title');
+
+ if (!$plans->count()) {
+ return null;
}
- // 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();
- });
+ $defaults = $plans->filter(function ($plan, $key) {
+ return str_contains($key, 'default');
+ });
+
+ if ($defaults->count()) {
+ return $defaults->first();
}
- DB::commit();
+ return $plans->first();
}
/**
- * 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 +209,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,10 +2,12 @@
namespace App\Http\Controllers\API;
+use App\Device;
use App\Discount;
use App\Domain;
use App\Group;
use App\Http\Controllers\Controller;
+use App\Http\Resources\DeviceInfoResource;
use App\Http\Resources\PlanResource;
use App\Jobs\Mail\SignupVerificationJob;
use App\Payment;
@@ -252,7 +254,7 @@
#[BodyParameter('domain', description: 'User domain namespace', type: 'string', required: true)]
#[BodyParameter('password', description: 'User password', type: 'string', required: true)]
#[BodyParameter('voucher', description: 'Voucher code', type: 'string')]
- #[BodyParameter('plan', description: 'Plan identifier (required when not using a verification code)', type: 'string')]
+ #[BodyParameter('plan', description: 'Plan identifier (required when not using a verification code or token)', type: 'string')]
#[BodyParameter('invitation', description: 'Signup invitation identifier', type: 'string')]
#[BodyParameter('token', description: 'Signup token (required for mode=token plans)', type: 'string')]
#[BodyParameter('first_name', description: 'First name', type: 'string')]
@@ -287,6 +289,14 @@
// Direct signup by token
if ($request->token) {
+ if (!$request->plan) {
+ $plan = Device::defaultPlan($request->token, false);
+ if ($plan) {
+ $request->merge(['plan' => $plan]);
+ unset($rules['plan']);
+ }
+ }
+
// This will validate the token and the plan mode
$rules['token'] = ['required', 'string', new SignupTokenRule($plan)];
}
@@ -377,7 +387,7 @@
#[BodyParameter('domain', description: 'User domain namespace', type: 'string', required: true)]
#[BodyParameter('password', description: 'User password', type: 'string', required: true)]
#[BodyParameter('voucher', description: 'Voucher code', type: 'string')]
- #[BodyParameter('plan', description: 'Plan identifier (required when not using a verification code)', type: 'string')]
+ #[BodyParameter('plan', description: 'Plan identifier (required when not using a verification code or token)', type: 'string')]
#[BodyParameter('invitation', description: 'Signup invitation identifier', type: 'string')]
#[BodyParameter('token', description: 'Signup token (required for mode=token plans)', type: 'string')]
#[BodyParameter('first_name', description: 'First name', type: 'string')]
@@ -467,9 +477,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();
@@ -480,6 +498,10 @@
$response->checkout = $this->mandateForPlan($request->plan, $request->discount, $user);
}
+ if (!empty($device)) {
+ $response->device = new DeviceInfoResource($device);
+ }
+
return $response;
}
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,16 +30,31 @@
}
$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 = Device::claim($token, $user);
return response()->json([
'status' => 'success',
'message' => self::trans('app.device-claim-success'),
+ 'device' => new DeviceInfoResource($device),
]);
}
@@ -106,38 +121,39 @@
*
* @unauthenticated
*/
- #[BodyParameter('plan', description: 'Plan title', type: 'string', required: true)]
+ #[BodyParameter('plan', description: 'Plan title', type: 'string')]
public function signup(Request $request, string $token)
{
- $v = Validator::make($request->all(), ['plan' => ['required', 'string']]);
-
- if ($v->fails()) {
- return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
- }
-
// Signup plan
- $plan = Plan::withEnvTenantContext()->where('title', $request->plan)->first();
+ if ($request->plan) {
+ $plan = Plan::withEnvTenantContext()->where('title', $request->plan)->first();
+ } else {
+ $plan = Device::defaultPlan($token, true);
+ }
if (!$plan) {
$errors = ['plan' => self::trans('validation.invalidvalue')];
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
- $request->merge([
- 'plan' => $plan,
- 'token' => \strtoupper($token),
- ]);
-
- // Validate input
- $v = Validator::make(
- $request->all(),
- [
- 'token' => ['required', 'string', new SignupTokenRule($plan)],
- ]
- );
-
- if ($v->fails()) {
- return response()->json(['status' => 'error', /* @var array */ 'errors' => $v->errors()], 422);
+ // Validate token (needed when using non-default plan)
+ if ($request->plan) {
+ $request->merge([
+ 'plan' => $plan,
+ 'token' => \strtoupper($token),
+ ]);
+
+ // Validate input
+ $v = Validator::make(
+ $request->all(),
+ [
+ 'token' => ['required', 'string', new SignupTokenRule($plan)],
+ ]
+ );
+
+ if ($v->fails()) {
+ return response()->json(['status' => 'error', /* @var array */ 'errors' => $v->errors()], 422);
+ }
}
// TODO: Validate that the plan is device-only, don't accept a user plan here
@@ -157,6 +173,8 @@
'password' => $password,
];
+ $response->device = new DeviceInfoResource($device);
+
return $response;
}
}
diff --git a/src/app/Http/Resources/AuthResource.php b/src/app/Http/Resources/AuthResource.php
--- a/src/app/Http/Resources/AuthResource.php
+++ b/src/app/Http/Resources/AuthResource.php
@@ -14,6 +14,7 @@
public ?int $user_id = null;
public ?array $checkout = null;
public ?array $credentials = null;
+ public ?DeviceInfoResource $device = null;
private ?UserInfoResource $userinfo = null;
@@ -50,6 +51,8 @@
'checkout' => $this->when(isset($this->checkout), $this->checkout),
// @var array New user credentials (on device signup)
'credentials' => $this->when(isset($this->credentials), $this->credentials),
+ // @var DeviceInfoResource Device information (on device signup)
+ 'device' => $this->when(isset($this->device), $this->device),
// @var int User identifier
'id' => $this->user_id,
// @var UserInfoResource User information
diff --git a/src/app/Http/Resources/DeviceInfoResource.php b/src/app/Http/Resources/DeviceInfoResource.php
--- a/src/app/Http/Resources/DeviceInfoResource.php
+++ b/src/app/Http/Resources/DeviceInfoResource.php
@@ -19,8 +19,27 @@
public function toArray(Request $request): array
{
return [
- // Device registration date-time
+ /*
+ * @var string Device registration date
+ * @format date-time
+ */
'created_at' => (string) $this->resource->created_at,
+
+ // Number of free months left
+ 'freeMonths' => $this->freeMonths(),
];
}
+
+ /**
+ * Calculate number of free months left
+ */
+ private function freeMonths(): int
+ {
+ $until = (clone $this->created_at)->addYearWithoutOverflow()->floorMonth();
+ $now = (clone \now())->floorMonth();
+
+ $months = $now->diffInMonths($until);
+
+ return max(0, $months); // No negative values
+ }
}
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
@@ -24,7 +24,7 @@
$this->hash = str_repeat('1', 64);
Device::query()->forceDelete();
- Plan::where('title', 'test')->delete();
+ Plan::whereIn('title', ['device-test', 'device-test-default'])->delete();
Package::where('title', 'test')->delete();
User::where('role', User::ROLE_DEVICE)->forceDelete();
$this->deleteTestUser('jane@kolabnow.com');
@@ -34,7 +34,7 @@
protected function tearDown(): void
{
SignupToken::query()->delete();
- Plan::where('title', 'test')->delete();
+ Plan::whereIn('title', ['device-test', 'device-test-default'])->delete();
Package::where('title', 'test')->delete();
$this->deleteTestUser('jane@kolabnow.com');
User::where('role', User::ROLE_DEVICE)->forceDelete();
@@ -65,6 +65,28 @@
$json = $response->json();
$this->assertStringContainsString('2025-02-02', $json['created_at']);
+ $this->assertSame(12, $json['freeMonths']);
+
+ // Assert freeMonths after 2 months
+ Carbon::setTestNow(Carbon::createFromDate(2025, 4, 2));
+ $response = $this->get('api/v4/device/' . $this->hash);
+ $json = $response->json();
+
+ $this->assertSame(10, $json['freeMonths']);
+
+ // Assert freeMonths after 12 months
+ Carbon::setTestNow(Carbon::createFromDate(2026, 2, 2));
+ $response = $this->get('api/v4/device/' . $this->hash);
+ $json = $response->json();
+
+ $this->assertSame(0, $json['freeMonths']);
+
+ // Assert freeMonths after 13 months
+ Carbon::setTestNow(Carbon::createFromDate(2026, 3, 2));
+ $response = $this->get('api/v4/device/' . $this->hash);
+ $json = $response->json();
+
+ $this->assertSame(0, $json['freeMonths']);
}
/**
@@ -95,6 +117,40 @@
$this->assertSame('success', $json['status']);
$this->assertSame('The device has been claimed successfully.', $json['message']);
+ $this->assertStringContainsString('2025-02-02', $json['device']['created_at']);
+ $this->assertSame(12, $json['device']['freeMonths']);
+ $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);
@@ -111,7 +167,7 @@
$response->assertStatus(404);
$plan = Plan::create([
- 'title' => 'test',
+ 'title' => 'device-test',
'name' => 'Test',
'description' => 'Test',
'mode' => Plan::MODE_TOKEN,
@@ -138,7 +194,7 @@
{
$sku = Sku::withEnvTenantContext()->where('title', 'device')->first();
$plan = Plan::create([
- 'title' => 'test',
+ 'title' => 'device-test-default',
'name' => 'Test',
'description' => 'Test',
'mode' => Plan::MODE_TOKEN,
@@ -152,16 +208,6 @@
$plan->packages()->saveMany([$package]);
$package->skus()->saveMany([$sku]);
- // Signup, missing plan
- $post = [];
- $response = $this->post("api/v4/device/{$this->hash}/signup", $post);
- $response->assertStatus(422);
-
- $json = $response->json();
-
- $this->assertSame('error', $json['status']);
- $this->assertSame(['plan' => ['The plan field is required.']], $json['errors']);
-
// Signup, invalid plan
$post = ['plan' => 'invalid'];
$response = $this->post("api/v4/device/{$this->hash}/signup", $post);
@@ -194,6 +240,8 @@
$this->assertSame('bearer', $json['token_type']);
$this->assertTrue(!empty($json['expires_in']) && is_int($json['expires_in']) && $json['expires_in'] > 0);
$this->assertNotEmpty($json['access_token']);
+ $this->assertStringContainsString('2025-02-02', $json['device']['created_at']);
+ $this->assertSame(12, $json['device']['freeMonths']);
$device = Device::where('hash', $this->hash)->first();
$account = $device->account;
@@ -215,7 +263,8 @@
// Note: without this finding the proper wallet may not work because of how Device::wallet() works
Carbon::setTestNow(Carbon::createFromDate(2025, 3, 4));
- // Signup again
+ // Signup again (w/o a plan now)
+ unset($post['plan']);
$response = $this->post("api/v4/device/{$this->hash}/signup", $post);
$response->assertStatus(200);
@@ -245,7 +294,7 @@
{
$sku = Sku::withEnvTenantContext()->where('title', 'device')->first();
$plan = Plan::create([
- 'title' => 'test',
+ 'title' => 'device-test',
'name' => 'Test',
'description' => 'Test',
'mode' => Plan::MODE_TOKEN,
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;
@@ -43,7 +46,9 @@
SI::truncate();
SignupToken::truncate();
- Plan::where('title', 'test')->delete();
+ Plan::where('title', ['test', 'user-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();
@@ -63,7 +68,9 @@
SI::truncate();
SignupToken::truncate();
- Plan::where('title', 'test')->delete();
+ Plan::where('title', ['test', 'user-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,8 +1001,9 @@
{
Queue::fake();
+ $sku = Sku::withEnvTenantContext()->where('title', 'device')->first();
$plan = Plan::create([
- 'title' => 'test',
+ 'title' => 'user-test',
'name' => 'Test Account',
'description' => 'Test',
'free_months' => 1,
@@ -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,45 @@
// 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());
+ $this->assertStringContainsString('2025-02-02', $json['device']['created_at']);
+ $this->assertSame(12, $json['device']['freeMonths']);
+
+ // Signup a new user with the same token/device (use default plan)
+ Carbon::setTestNow(Carbon::createFromDate(2025, 4, 2));
+ $post = [
+ '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
Sat, Apr 4, 4:34 AM (12 h, 49 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18828117
Default Alt Text
D5655.1775277271.diff (28 KB)
Attached To
Mode
D5655: Device signup improvements and fixes
Attached
Detach File
Event Timeline