Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117838813
D1162.1775307638.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
24 KB
Referenced Files
None
Subscribers
None
D1162.1775307638.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D1162: Signup: Vouchers
Attached
Detach File
Event Timeline