Page MenuHomePhorge

D1162.1775307638.diff
No OneTemporary

Authored By
Unknown
Size
24 KB
Referenced Files
None
Subscribers
None

D1162.1775307638.diff

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
@@ -5,6 +5,7 @@
use App\Http\Controllers\Controller;
use App\Jobs\SignupVerificationEmail;
use App\Jobs\SignupVerificationSMS;
+use App\Discount;
use App\Domain;
use App\Plan;
use App\Rules\ExternalEmail;
@@ -40,7 +41,8 @@
{
$plans = [];
- Plan::all()->map(function ($plan) use (&$plans) {
+ // Use reverse order just to have individual on left, group on right ;)
+ Plan::select()->orderByDesc('title')->get()->map(function ($plan) use (&$plans) {
$plans[] = [
'title' => $plan->title,
'name' => $plan->name,
@@ -71,6 +73,7 @@
'email' => 'required',
'name' => 'required|max:512',
'plan' => 'nullable|alpha_num|max:128',
+ 'voucher' => 'max:32',
]
);
@@ -89,6 +92,7 @@
'email' => $request->email,
'name' => $request->name,
'plan' => $request->plan,
+ 'voucher' => $request->voucher,
]
]);
@@ -142,12 +146,13 @@
$has_domain = $this->getPlan()->hasDomain();
- // Return user name and email/phone from the codes database,
+ // Return user name and email/phone/voucher from the codes database,
// domains list for selection and "plan type" flag
return response()->json([
'status' => 'success',
'email' => $code->data['email'],
'name' => $code->data['name'],
+ 'voucher' => $code->data['voucher'],
'is_domain' => $has_domain,
'domains' => $has_domain ? [] : Domain::getPublicDomains(),
]);
@@ -169,6 +174,7 @@
'login' => 'required|min:2',
'password' => 'required|min:4|confirmed',
'domain' => 'required',
+ 'voucher' => 'max:32',
]
);
@@ -182,6 +188,17 @@
return $v;
}
+ // Find the voucher discount
+ if ($request->voucher) {
+ $discount = Discount::where('code', \strtoupper($request->voucher))
+ ->where('active', true)->first();
+
+ if (!$discount) {
+ $errors = ['voucher' => \trans('validation.voucherinvalid')];
+ return response()->json(['status' => 'error', 'errors' => $errors], 422);
+ }
+ }
+
// Get the plan
$plan = $this->getPlan();
$is_domain = $plan->hasDomain();
@@ -222,6 +239,12 @@
]);
}
+ if (!empty($discount)) {
+ $wallet = $user->wallets()->first();
+ $wallet->discount()->associate($discount);
+ $wallet->save();
+ }
+
$user->assignPlan($plan, $domain);
// Save the external email and plan in user settings
diff --git a/src/resources/js/routes-user.js b/src/resources/js/routes-user.js
--- a/src/resources/js/routes-user.js
+++ b/src/resources/js/routes-user.js
@@ -71,6 +71,7 @@
},
{
path: '/signup/:param?',
+ alias: '/signup/voucher/:param',
name: 'signup',
component: SignupComponent
},
diff --git a/src/resources/lang/en/validation.php b/src/resources/lang/en/validation.php
--- a/src/resources/lang/en/validation.php
+++ b/src/resources/lang/en/validation.php
@@ -129,6 +129,7 @@
'packageinvalid' => 'Invalid package selected.',
'packagerequired' => 'Package is required.',
'usernotexists' => 'Unable to find user.',
+ 'voucherinvalid' => 'The voucher code is invalid or expired.',
'noextemail' => 'This user has no external email address.',
'entryinvalid' => 'The specified :attribute is invalid.',
'entryexists' => 'The specified :attribute is not available.',
diff --git a/src/resources/sass/app.scss b/src/resources/sass/app.scss
--- a/src/resources/sass/app.scss
+++ b/src/resources/sass/app.scss
@@ -200,3 +200,19 @@
height: 6rem;
}
}
+
+.plan-selector {
+ .plan-ico {
+ width: 100px;
+ font-size: 4em;
+ color: #f1a539;
+ border: 3px solid #f1a539;
+ width: 100px;
+ margin-bottom: 1rem;
+ border-radius: 50%;
+ }
+
+ ul:last-child {
+ margin-bottom: 0;
+ }
+}
diff --git a/src/resources/vue/Signup.vue b/src/resources/vue/Signup.vue
--- a/src/resources/vue/Signup.vue
+++ b/src/resources/vue/Signup.vue
@@ -1,7 +1,15 @@
<template>
<div class="container">
<div id="step0">
- <div class="plan-selector d-flex justify-content-around align-items-stretch mb-3"></div>
+ <div class="plan-selector d-flex justify-content-around align-items-stretch mb-3">
+ <div v-for="plan in plans" :key="plan.id" :class="'p-3 m-1 text-center bg-light flex-fill plan-box d-flex flex-column align-items-center plan-' + plan.title">
+ <div class="plan-ico">
+ <svg-icon :icon="plan_icons[plan.title]"></svg-icon>
+ </div>
+ <button class="btn btn-primary" :data-title="plan.title" @click="selectPlan(plan.title)" v-html="plan.button"></button>
+ <div class="plan-description text-left mt-3" v-html="plan.description"></div>
+ </div>
+ </div>
<h3>FAQs</h3>
<ul>
<li><a href="https://kolabnow.com/tos">What are your terms of service?</a></li>
@@ -76,6 +84,10 @@
<label for="signup_confirm" class="sr-only">Confirm Password</label>
<input type="password" class="form-control" id="signup_confirm" placeholder="Confirm Password" required v-model="password_confirmation">
</div>
+ <div class="form-group pt-2 pb-2">
+ <label for="signup_voucher" class="sr-only">Voucher code</label>
+ <input type="text" class="form-control" id="signup_voucher" placeholder="Voucher code" v-model="voucher">
+ </div>
<button class="btn btn-secondary" type="button" @click="stepBack">Back</button>
<button class="btn btn-primary" type="submit"><svg-icon icon="check"></svg-icon> Submit</button>
</form>
@@ -98,13 +110,23 @@
domain: '',
plan: null,
is_domain: false,
- plans: null
+ plan_icons: {
+ individual: 'user',
+ group: 'users'
+ },
+ plans: [],
+ voucher: ''
}
},
mounted() {
let param = this.$route.params.param;
+
if (param) {
- if (/^([A-Z0-9]+)-([a-zA-Z0-9]+)$/.test(param)) {
+ if (this.$route.path.indexOf('/signup/voucher/') === 0) {
+ // Voucher (discount) code
+ this.voucher = param
+ this.displayForm(0)
+ } else if (/^([A-Z0-9]+)-([a-zA-Z0-9]+)$/.test(param)) {
// Verification code provided, auto-submit Step 2
this.short_code = RegExp.$1
this.code = RegExp.$2
@@ -121,31 +143,16 @@
}
},
methods: {
+ selectPlan(plan) {
+ this.$router.push({path: '/signup/' + plan})
+ this.plan = plan
+ this.displayForm(1, true)
+ },
// Composes plan selection page
step0() {
- if (!this.plans) {
+ if (!this.plans.length) {
axios.get('/api/auth/signup/plans', {}).then(response => {
- let boxes = []
-
this.plans = response.data.plans
-
- this.plans.forEach(plan => {
- boxes.push($(
- `<div class="p-3 m-1 text-center bg-light flex-fill plan-box plan-${plan.title}">
- <button class="btn btn-primary" data-title="${plan.title}">${plan.button}</button>
- <div class="plan-description text-left mt-3">${plan.description}</div>
- </div>`
- ))
- })
-
- $('#step0').find('.plan-selector')
- .append(boxes)
- .find('button').on('click', event => {
- let plan = $(event.target).data('title')
- this.$router.push({path: '/signup/' + plan})
- this.plan = plan
- this.displayForm(1, true)
- })
})
}
},
@@ -156,7 +163,8 @@
axios.post('/api/auth/signup/init', {
email: this.email,
name: this.name,
- plan: this.plan
+ plan: this.plan,
+ voucher: this.voucher
}).then(response => {
this.displayForm(2, true)
this.code = response.data.code
@@ -179,6 +187,7 @@
this.name = response.data.name
this.email = response.data.email
this.is_domain = response.data.is_domain
+ this.voucher = response.data.voucher
// Fill the domain selector with available domains
if (!this.is_domain) {
@@ -208,7 +217,8 @@
login: this.login,
domain: this.domain,
password: this.password,
- password_confirmation: this.password_confirmation
+ password_confirmation: this.password_confirmation,
+ voucher: this.voucher
}).then(response => {
// auto-login and goto dashboard
this.$root.loginUser(response.data.access_token)
diff --git a/src/tests/Browser/SignupTest.php b/src/tests/Browser/SignupTest.php
--- a/src/tests/Browser/SignupTest.php
+++ b/src/tests/Browser/SignupTest.php
@@ -2,6 +2,7 @@
namespace Tests\Browser;
+use App\Discount;
use App\Domain;
use App\SignupCode;
use App\User;
@@ -65,11 +66,7 @@
// FIXME: User will not be able to continue anyway, so we should
// either display 1st step or 404 error page
$browser->waitFor('@step1')
- ->with(new Toast(Toast::TYPE_ERROR), function (Browser $browser) {
- $browser->assertToastTitle('Error')
- ->assertToastMessage('Form validation error')
- ->closeToast();
- });
+ ->assertToast(Toast::TYPE_ERROR, 'Error', 'Form validation error');
});
// Test valid code
@@ -79,14 +76,14 @@
'email' => 'User@example.org',
'name' => 'User Name',
'plan' => 'individual',
+ 'voucher' => '',
]
]);
- $browser->visit('/signup/' . $code->short_code . '-' . $code->code);
-
- $browser->waitFor('@step3');
- $browser->assertMissing('@step1');
- $browser->assertMissing('@step2');
+ $browser->visit('/signup/' . $code->short_code . '-' . $code->code)
+ ->waitFor('@step3')
+ ->assertMissing('@step1')
+ ->assertMissing('@step2');
// FIXME: Find a nice way to read javascript data without using hidden inputs
$this->assertSame($code->code, $browser->value('@step2 #signup_code'));
@@ -190,13 +187,8 @@
->type('#signup_email', '@test')
->click('[type=submit]')
->waitFor('#signup_email.is-invalid')
- ->assertVisible('#signup_email + .invalid-feedback');
-
- $browser->with(new Toast(Toast::TYPE_ERROR), function (Browser $browser) {
- $browser->assertToastTitle('Error')
- ->assertToastMessage('Form validation error')
- ->closeToast();
- });
+ ->assertVisible('#signup_email + .invalid-feedback')
+ ->assertToast(Toast::TYPE_ERROR, 'Error', 'Form validation error');
});
// Submit valid data
@@ -261,13 +253,8 @@
$step->waitFor('#signup_short_code.is-invalid')
->assertVisible('#signup_short_code + .invalid-feedback')
- ->assertFocused('#signup_short_code');
-
- $browser->with(new Toast(Toast::TYPE_ERROR), function (Browser $browser) {
- $browser->assertToastTitle('Error')
- ->assertToastMessage('Form validation error')
- ->closeToast();
- });
+ ->assertFocused('#signup_short_code')
+ ->assertToast(Toast::TYPE_ERROR, 'Error', 'Form validation error');
});
// Submit valid code
@@ -353,13 +340,8 @@
->assertVisible('#signup_domain + .invalid-feedback')
->assertVisible('#signup_password.is-invalid')
->assertVisible('#signup_password + .invalid-feedback')
- ->assertFocused('#signup_login');
-
- $browser->with(new Toast(Toast::TYPE_ERROR), function (Browser $browser) {
- $browser->assertToastTitle('Error')
- ->assertToastMessage('Form validation error')
- ->closeToast();
- });
+ ->assertFocused('#signup_login')
+ ->assertToast(Toast::TYPE_ERROR, 'Error', 'Form validation error');
});
// Submit invalid data (valid login, invalid password)
@@ -370,13 +352,8 @@
->assertVisible('#signup_password + .invalid-feedback')
->assertMissing('#signup_login.is-invalid')
->assertMissing('#signup_domain + .invalid-feedback')
- ->assertFocused('#signup_password');
-
- $browser->with(new Toast(Toast::TYPE_ERROR), function (Browser $browser) {
- $browser->assertToastTitle('Error')
- ->assertToastMessage('Form validation error')
- ->closeToast();
- });
+ ->assertFocused('#signup_password')
+ ->assertToast(Toast::TYPE_ERROR, 'Error', 'Form validation error');
});
// Submit valid data
@@ -455,13 +432,8 @@
->assertVisible('#signup_domain + .invalid-feedback')
->assertVisible('#signup_password.is-invalid')
->assertVisible('#signup_password + .invalid-feedback')
- ->assertFocused('#signup_login');
-
- $browser->with(new Toast(Toast::TYPE_ERROR), function (Browser $browser) {
- $browser->assertToastTitle('Error')
- ->assertToastMessage('Form validation error')
- ->closeToast();
- });
+ ->assertFocused('#signup_login')
+ ->assertToast(Toast::TYPE_ERROR, 'Error', 'Form validation error');
});
// Submit invalid domain
@@ -475,13 +447,8 @@
->assertVisible('#signup_domain.is-invalid + .invalid-feedback')
->assertMissing('#signup_password.is-invalid')
->assertMissing('#signup_password + .invalid-feedback')
- ->assertFocused('#signup_domain');
-
- $browser->with(new Toast(Toast::TYPE_ERROR), function (Browser $browser) {
- $browser->assertToastTitle('Error')
- ->assertToastMessage('Form validation error')
- ->closeToast();
- });
+ ->assertFocused('#signup_domain')
+ ->assertToast(Toast::TYPE_ERROR, 'Error', 'Form validation error');
});
// Submit invalid domain
@@ -499,4 +466,61 @@
$browser->click('a.link-logout');
});
}
+
+ /**
+ * Test signup with voucher
+ */
+ public function testSignupVoucherLink(): void
+ {
+ $this->browse(function (Browser $browser) {
+ $browser->visit('/signup/voucher/TEST')
+ ->onWithoutAssert(new Signup())
+ ->waitFor('@step0')
+ ->click('.plan-individual button')
+ ->whenAvailable('@step1', function (Browser $browser) {
+ $browser->type('#signup_name', 'Test User')
+ ->type('#signup_email', 'BrowserSignupTestUser1@kolab.org')
+ ->click('[type=submit]');
+ })
+ ->whenAvailable('@step2', function (Browser $browser) {
+ // Get the code and short_code from database
+ // FIXME: Find a nice way to read javascript data without using hidden inputs
+ $code = $browser->value('#signup_code');
+
+ $this->assertNotEmpty($code);
+
+ $code = SignupCode::find($code);
+
+ $browser->type('#signup_short_code', $code->short_code)
+ ->click('[type=submit]');
+ })
+ ->whenAvailable('@step3', function (Browser $browser) {
+ // Assert that the code is filled in the input
+ // Change it and test error handling
+ $browser->assertValue('#signup_voucher', 'TEST')
+ ->type('#signup_voucher', 'TESTXX')
+ ->type('#signup_login', 'signuptestdusk')
+ ->type('#signup_password', '123456789')
+ ->type('#signup_confirm', '123456789')
+ ->click('[type=submit]')
+ ->waitFor('#signup_voucher.is-invalid')
+ ->assertVisible('#signup_voucher + .invalid-feedback')
+ ->assertFocused('#signup_voucher')
+ ->assertToast(Toast::TYPE_ERROR, 'Error', 'Form validation error')
+ // Submit the correct code
+ ->type('#signup_voucher', 'TEST')
+ ->click('[type=submit]');
+ })
+ ->waitUntilMissing('@step3')
+ ->waitUntilMissing('.app-loader')
+ ->on(new Dashboard())
+ ->assertUser('signuptestdusk@' . \config('app.domain'))
+ // Logout the user
+ ->click('a.link-logout');
+ });
+
+ $user = $this->getTestUser('signuptestdusk@' . \config('app.domain'));
+ $discount = Discount::where('code', 'TEST')->first();
+ $this->assertSame($discount->id, $user->wallets()->first()->discount_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
@@ -3,6 +3,7 @@
namespace Tests\Feature\Controller;
use App\Http\Controllers\API\SignupController;
+use App\Discount;
use App\Domain;
use App\SignupCode;
use App\User;
@@ -77,8 +78,6 @@
*/
public function testSignupPlans()
{
- // Note: this uses plans that already have been seeded into the DB
-
$response = $this->get('/api/auth/signup/plans');
$json = $response->json();
@@ -141,6 +140,22 @@
$this->assertCount(1, $json['errors']);
$this->assertArrayHasKey('email', $json['errors']);
+ // Sanity check on voucher code
+ $data = [
+ 'voucher' => '123456789012345678901234567890123',
+ 'name' => 'Signup User',
+ 'email' => 'valid@email.com',
+ ];
+
+ $response = $this->post('/api/auth/signup/init', $data);
+ $json = $response->json();
+
+ $response->assertStatus(422);
+
+ $this->assertSame('error', $json['status']);
+ $this->assertCount(1, $json['errors']);
+ $this->assertArrayHasKey('voucher', $json['errors']);
+
// TODO: Test phone validation
}
@@ -183,11 +198,34 @@
&& $code->data['name'] === $data['name'];
});
+ // Try the same with voucher
+ $data['voucher'] = 'TEST';
+
+ $response = $this->post('/api/auth/signup/init', $data);
+ $json = $response->json();
+
+ $response->assertStatus(200);
+ $this->assertCount(2, $json);
+ $this->assertSame('success', $json['status']);
+ $this->assertNotEmpty($json['code']);
+
+ // Assert the job has proper data assigned
+ Queue::assertPushed(\App\Jobs\SignupVerificationEmail::class, function ($job) use ($data, $json) {
+ $code = TestCase::getObjectProperty($job, 'code');
+
+ return $code->code === $json['code']
+ && $code->data['plan'] === $data['plan']
+ && $code->data['email'] === $data['email']
+ && $code->data['voucher'] === $data['voucher']
+ && $code->data['name'] === $data['name'];
+ });
+
return [
'code' => $json['code'],
'email' => $data['email'],
'name' => $data['name'],
'plan' => $data['plan'],
+ 'voucher' => $data['voucher']
];
}
@@ -260,10 +298,11 @@
$json = $response->json();
$response->assertStatus(200);
- $this->assertCount(5, $json);
+ $this->assertCount(6, $json);
$this->assertSame('success', $json['status']);
$this->assertSame($result['email'], $json['email']);
$this->assertSame($result['name'], $json['name']);
+ $this->assertSame($result['voucher'], $json['voucher']);
$this->assertSame(false, $json['is_domain']);
$this->assertTrue(is_array($json['domains']) && !empty($json['domains']));
@@ -362,8 +401,28 @@
$this->assertCount(1, $json['errors']);
$this->assertArrayHasKey('short_code', $json['errors']);
- // Valid code, invalid login
$code = SignupCode::find($result['code']);
+
+ // Data with invalid voucher
+ $data = [
+ 'login' => 'TestLogin',
+ 'domain' => $domain,
+ 'password' => 'test',
+ 'password_confirmation' => 'test',
+ 'code' => $result['code'],
+ 'short_code' => $code->short_code,
+ 'voucher' => 'XXX',
+ ];
+
+ $response = $this->post('/api/auth/signup', $data);
+ $json = $response->json();
+
+ $response->assertStatus(422);
+ $this->assertSame('error', $json['status']);
+ $this->assertCount(1, $json['errors']);
+ $this->assertArrayHasKey('voucher', $json['errors']);
+
+ // Valid code, invalid login
$data = [
'login' => 'żżżżżż',
'domain' => $domain,
@@ -402,6 +461,7 @@
'password_confirmation' => 'test',
'code' => $code->code,
'short_code' => $code->short_code,
+ 'voucher' => 'TEST',
];
$response = $this->post('/api/auth/signup', $data);
@@ -434,6 +494,10 @@
// Check external email in user settings
$this->assertSame($result['email'], $user->getSetting('external_email'));
+ // Discount
+ $discount = Discount::where('code', 'TEST')->first();
+ $this->assertSame($discount->id, $user->wallets()->first()->discount_id);
+
// TODO: Check SKUs/Plan
// TODO: Check if the access token works
@@ -487,10 +551,11 @@
$result = $response->json();
$response->assertStatus(200);
- $this->assertCount(5, $result);
+ $this->assertCount(6, $result);
$this->assertSame('success', $result['status']);
$this->assertSame($user_data['email'], $result['email']);
$this->assertSame($user_data['name'], $result['name']);
+ $this->assertSame(null, $result['voucher']);
$this->assertSame(true, $result['is_domain']);
$this->assertSame([], $result['domains']);

File Metadata

Mime Type
text/plain
Expires
Sat, Apr 4, 1:00 PM (15 h, 56 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18789954
Default Alt Text
D1162.1775307638.diff (24 KB)

Event Timeline