Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117909070
D4136.1775394458.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
63 KB
Referenced Files
None
Subscribers
None
D4136.1775394458.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
@@ -37,7 +37,8 @@
$plans = [];
// Use reverse order just to have individual on left, group on right ;)
- Plan::withEnvTenantContext()->orderByDesc('title')->get()
+ // But prefer monthly on left, yearly on right
+ Plan::withEnvTenantContext()->orderBy('months')->orderByDesc('title')->get()
->map(function ($plan) use (&$plans) {
// Allow themes to set custom button label
$button = \trans('theme::app.planbutton-' . $plan->title);
@@ -51,6 +52,7 @@
'button' => $button,
'description' => $plan->description,
'mode' => $plan->mode ?: 'email',
+ 'isDomain' => $plan->hasDomain(),
];
});
@@ -58,6 +60,18 @@
}
/**
+ * Returns list of public domains for signup.
+ *
+ * @param \Illuminate\Http\Request $request HTTP request
+ *
+ * @return \Illuminate\Http\JsonResponse JSON response
+ */
+ public function domains(Request $request)
+ {
+ return response()->json(['status' => 'success', 'domains' => Domain::getPublicDomains()]);
+ }
+
+ /**
* Starts signup process.
*
* Verifies user name and email/phone, sends verification email/sms message.
@@ -230,8 +244,19 @@
return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
}
- // Signup via invitation
- if ($request->invitation) {
+
+ $settings = [];
+
+ // Plan parameter is required/allowed in mandate mode
+ if (!empty($request->plan) && empty($request->code) && empty($request->invitation)) {
+ $plan = Plan::withEnvTenantContext()->where('title', $request->plan)->first();
+
+ if (!$plan || $plan->mode != 'mandate') {
+ $msg = \trans('validation.exists', ['attribute' => 'plan']);
+ return response()->json(['status' => 'error', 'errors' => ['plan' => $msg]], 422);
+ }
+ } elseif ($request->invitation) {
+ // Signup via invitation
$invitation = SignupInvitation::withEnvTenantContext()->find($request->invitation);
if (empty($invitation) || $invitation->isCompleted()) {
@@ -244,7 +269,6 @@
[
'first_name' => 'max:128',
'last_name' => 'max:128',
- 'voucher' => 'max:32',
]
);
@@ -266,6 +290,8 @@
return $v;
}
+ $plan = $this->getPlan();
+
// Get user name/email from the verification code database
$code_data = $v->getData();
@@ -274,7 +300,7 @@
'last_name' => $code_data->last_name,
];
- if ($this->getPlan()->mode == 'token') {
+ if ($plan->mode == 'token') {
$settings['signup_token'] = $code_data->email;
} else {
$settings['external_email'] = $code_data->email;
@@ -292,10 +318,11 @@
}
}
- // Get the plan
- $plan = $this->getPlan();
- $is_domain = $plan->hasDomain();
+ if (empty($plan)) {
+ $plan = $this->getPlan();
+ }
+ $is_domain = $plan->hasDomain();
$login = $request->login;
$domain_name = $request->domain;
@@ -355,7 +382,14 @@
DB::commit();
- return AuthController::logonResponse($user, $request->password);
+ $response = AuthController::logonResponse($user, $request->password);
+
+ // Redirect the user to the specified page
+ // $data = $response->getData(true);
+ // $data['redirect'] = 'wallet';
+ // $response->setData($data);
+
+ return $response;
}
/**
@@ -369,7 +403,7 @@
if (!$request->plan || !$request->plan instanceof Plan) {
// Get the plan if specified and exists...
- if ($request->code && $request->code->plan) {
+ if (($request->code instanceof SignupCode) && $request->code->plan) {
$plan = Plan::withEnvTenantContext()->where('title', $request->code->plan)->first();
} elseif ($request->plan) {
$plan = Plan::withEnvTenantContext()->where('title', $request->plan)->first();
diff --git a/src/app/Http/Controllers/API/V4/PaymentsController.php b/src/app/Http/Controllers/API/V4/PaymentsController.php
--- a/src/app/Http/Controllers/API/V4/PaymentsController.php
+++ b/src/app/Http/Controllers/API/V4/PaymentsController.php
@@ -166,18 +166,25 @@
$amount = (int) ($request->amount * 100);
// Validate the minimum value
- // It has to be at least minimum payment amount and must cover current debt
- if (
- $wallet->balance < 0
- && $wallet->balance <= Payment::MIN_AMOUNT * -1
- && $wallet->balance + $amount < 0
- ) {
- return ['amount' => \trans('validation.minamountdebt')];
+ // It has to be at least minimum payment amount and must cover current debt,
+ // and must be more than a yearly/monthly payment (according to the plan)
+ $min = Payment::MIN_AMOUNT;
+ $label = 'minamount';
+
+ if (($plan = $wallet->plan()) && $plan->months >= 1) {
+ $planCost = (int) ceil($plan->cost() * $plan->months);
+ if ($planCost > $min) {
+ $min = $planCost;
+ }
}
- if ($amount < Payment::MIN_AMOUNT) {
- $min = $wallet->money(Payment::MIN_AMOUNT);
- return ['amount' => \trans('validation.minamount', ['amount' => $min])];
+ if ($wallet->balance < 0 && $wallet->balance < $min * -1) {
+ $min = $wallet->balance * -1;
+ $label = 'minamountdebt';
+ }
+
+ if ($amount < $min) {
+ return ['amount' => \trans("validation.{$label}", ['amount' => $wallet->money($min)])];
}
return null;
@@ -366,9 +373,10 @@
// Get the Mandate info
$mandate = (array) $provider->getMandate($wallet);
- $mandate['amount'] = (int) (Payment::MIN_AMOUNT / 100);
+ $mandate['amount'] = $mandate['minAmount'] = (int) ceil(Payment::MIN_AMOUNT / 100);
$mandate['balance'] = 0;
$mandate['isDisabled'] = !empty($mandate['id']) && $settings['mandate_disabled'];
+ $mandate['isValid'] = !empty($mandate['isValid']);
foreach (['amount', 'balance'] as $key) {
if (($value = $settings["mandate_{$key}"]) !== null) {
@@ -376,6 +384,19 @@
}
}
+ // If this is a multi-month plan, we calculate the expected amount to be payed.
+ if (($plan = $wallet->plan()) && $plan->months >= 1) {
+ $planCost = (int) ceil(($plan->cost() * $plan->months) / 100);
+ if ($planCost > $mandate['minAmount']) {
+ $mandate['minAmount'] = $planCost;
+ }
+ }
+
+ // Unrestrict the wallet owner if mandate is valid
+ if (!empty($mandate['isValid']) && $wallet->owner->isRestricted()) {
+ $wallet->owner->unrestrict();
+ }
+
return $mandate;
}
diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php
--- a/src/app/Http/Controllers/API/V4/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/UsersController.php
@@ -184,6 +184,8 @@
$hasBeta = in_array('beta', $skus);
+ $plan = $isController ? $user->wallet()->plan() : null;
+
$result = [
'skus' => $skus,
'enableBeta' => in_array('beta', $skus),
@@ -200,6 +202,8 @@
'enableSettings' => $isController,
'enableUsers' => $isController,
'enableWallets' => $isController,
+ 'enableWalletMandates' => $isController,
+ 'enableWalletPayments' => $isController && (!$plan || $plan->mode != 'mandate'),
'enableCompanionapps' => $hasBeta,
];
@@ -349,6 +353,11 @@
{
$response = array_merge($user->toArray(), self::objectState($user));
+ $wallet = $user->wallet();
+
+ // IsLocked flag to lock the user to the Wallet page only
+ $response['isLocked'] = ($user->isRestricted() && ($plan = $wallet->plan()) && $plan->mode == 'mandate');
+
// Settings
$response['settings'] = [];
foreach ($user->settings()->whereIn('key', self::USER_SETTINGS)->get() as $item) {
@@ -380,7 +389,7 @@
// Information about wallets and accounts for access checks
$response['wallets'] = $user->wallets->map($map_func)->toArray();
$response['accounts'] = $user->accounts->map($map_func)->toArray();
- $response['wallet'] = $map_func($user->wallet());
+ $response['wallet'] = $map_func($wallet);
return $response;
}
diff --git a/src/app/Http/Controllers/ContentController.php b/src/app/Http/Controllers/ContentController.php
--- a/src/app/Http/Controllers/ContentController.php
+++ b/src/app/Http/Controllers/ContentController.php
@@ -147,7 +147,7 @@
}
// Unset properties that we don't need on the client side
- unset($item['admin'], $item['label']);
+ unset($item['admin']);
$menu[$idx] = $item;
}
diff --git a/src/app/Observers/WalletObserver.php b/src/app/Observers/WalletObserver.php
--- a/src/app/Observers/WalletObserver.php
+++ b/src/app/Observers/WalletObserver.php
@@ -111,14 +111,7 @@
// Remove RESTRICTED flag from the wallet owner and all users in the wallet
if ($wallet->balance > $wallet->getOriginal('balance') && $wallet->owner && $wallet->owner->isRestricted()) {
- $wallet->owner->unrestrict();
-
- User::whereIn(
- 'id',
- $wallet->entitlements()->select('entitleable_id')->where('entitleable_type', User::class)
- )->each(function ($user) {
- $user->unrestrict();
- });
+ $wallet->owner->unrestrict(true);
}
}
}
diff --git a/src/app/Package.php b/src/app/Package.php
--- a/src/app/Package.php
+++ b/src/app/Package.php
@@ -67,7 +67,7 @@
$units = $sku->pivot->qty - $sku->units_free;
if ($units < 0) {
- \Log::debug("Package {$this->id} is misconfigured for more free units than qty.");
+ \Log::warning("Package {$this->id} is misconfigured for more free units than qty.");
$units = 0;
}
diff --git a/src/app/Payment.php b/src/app/Payment.php
--- a/src/app/Payment.php
+++ b/src/app/Payment.php
@@ -106,7 +106,15 @@
*/
public function credit($method): void
{
- // TODO: Possibly we should sanity check that payment is paid, and not negative?
+ if (empty($this->wallet)) {
+ throw new \Exception("Cannot credit a payment not assigned to a wallet");
+ }
+
+ if ($this->credit_amount < 0) {
+ throw new \Exception("Cannot credit a payment with negative amount");
+ }
+
+ // TODO: Possibly we should sanity check that payment is paid?
// TODO: Localization?
$description = $this->type == self::TYPE_RECURRING ? 'Auto-payment' : 'Payment';
$description .= " transaction {$this->id} using {$method}";
@@ -117,6 +125,11 @@
if ($this->wallet->balance >= 0) {
$this->wallet->setSetting('mandate_disabled', null);
}
+
+ // Remove RESTRICTED flag from the wallet owner and all users in the wallet
+ if ($this->wallet->owner && $this->wallet->owner->isRestricted()) {
+ $this->wallet->owner->unrestrict(true);
+ }
}
/**
diff --git a/src/app/Plan.php b/src/app/Plan.php
--- a/src/app/Plan.php
+++ b/src/app/Plan.php
@@ -49,6 +49,8 @@
'discount_qty',
// the rate of the discount for this plan
'discount_rate',
+ // minimum number of months this plan is for
+ 'months',
// number of free months (trial)
'free_months',
];
@@ -59,6 +61,7 @@
'promo_to' => 'datetime:Y-m-d H:i:s',
'discount_qty' => 'integer',
'discount_rate' => 'integer',
+ 'months' => 'integer',
'free_months' => 'integer'
];
diff --git a/src/app/Providers/Payment/Mollie.php b/src/app/Providers/Payment/Mollie.php
--- a/src/app/Providers/Payment/Mollie.php
+++ b/src/app/Providers/Payment/Mollie.php
@@ -360,7 +360,7 @@
if ($mollie_payment->isPaid()) {
// The payment is paid. Update the balance, and notify the user
- if ($payment->status != Payment::STATUS_PAID && $payment->amount > 0) {
+ if ($payment->status != Payment::STATUS_PAID && $payment->amount >= 0) {
$credit = true;
$notify = $payment->type == Payment::TYPE_RECURRING;
}
diff --git a/src/app/Providers/Payment/Stripe.php b/src/app/Providers/Payment/Stripe.php
--- a/src/app/Providers/Payment/Stripe.php
+++ b/src/app/Providers/Payment/Stripe.php
@@ -369,6 +369,9 @@
$payment->wallet->setSetting('stripe_mandate_id', $intent->id);
$threshold = intval((float) $payment->wallet->getSetting('mandate_balance') * 100);
+ // Call credit() so wallet/account state is updated
+ $this->creditPayment($payment, $intent);
+
// Top-up the wallet if balance is below the threshold
if ($payment->wallet->balance < $threshold && $payment->status != Payment::STATUS_PAID) {
\App\Jobs\WalletCharge::dispatch($payment->wallet);
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -557,16 +557,27 @@
/**
* Un-restrict this user.
*
+ * @param bool $deep Unrestrict also all users in the account
+ *
* @return void
*/
- public function unrestrict(): void
+ public function unrestrict(bool $deep = false): void
{
- if (!$this->isRestricted()) {
- return;
+ if ($this->isRestricted()) {
+ $this->status ^= User::STATUS_RESTRICTED;
+ $this->save();
+ }
+
+ // Remove the flag from all users in the user's wallets
+ if ($deep) {
+ $this->wallets->each(function ($wallet) {
+ User::whereIn('id', $wallet->entitlements()->select('entitleable_id')
+ ->where('entitleable_type', User::class))
+ ->each(function ($user) {
+ $user->unrestrict();
+ });
+ });
}
-
- $this->status ^= User::STATUS_RESTRICTED;
- $this->save();
}
/**
diff --git a/src/database/migrations/2023_02_17_100000_vat_rates_table.php b/src/database/migrations/2023_02_17_100000_vat_rates_table.php
--- a/src/database/migrations/2023_02_17_100000_vat_rates_table.php
+++ b/src/database/migrations/2023_02_17_100000_vat_rates_table.php
@@ -78,6 +78,7 @@
Schema::table(
'payments',
function (Blueprint $table) {
+ $table->dropForeign(['vat_rate_id']);
$table->dropColumn('vat_rate_id');
$table->dropColumn('credit_amount');
}
diff --git a/src/database/migrations/2023_03_01_100000_plans_months.php b/src/database/migrations/2023_03_01_100000_plans_months.php
new file mode 100644
--- /dev/null
+++ b/src/database/migrations/2023_03_01_100000_plans_months.php
@@ -0,0 +1,38 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::table(
+ 'plans',
+ function (Blueprint $table) {
+ $table->tinyInteger('months')->unsigned()->default(1);
+ }
+ );
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::table(
+ 'plans',
+ function (Blueprint $table) {
+ $table->dropColumn('months');
+ }
+ );
+ }
+};
diff --git a/src/resources/build/before.php b/src/resources/build/before.php
--- a/src/resources/build/before.php
+++ b/src/resources/build/before.php
@@ -37,6 +37,20 @@
}
}
+foreach (glob("{$rootDir}/resources/themes/*/lang/*/ui.php") as $file) {
+ $content = include $file;
+
+ if (is_array($content)) {
+ preg_match('|([a-zA-Z]+)/lang/([a-z]+)/ui\.php$|', $file, $matches);
+
+ $theme = $matches[1];
+ $file = "{$rootDir}/resources/build/js/{$theme}-{$matches[2]}.json";
+ $opts = JSON_PRETTY_PRINT | JSON_INVALID_UTF8_SUBSTITUTE | JSON_UNESCAPED_UNICODE;
+
+ file_put_contents($file, json_encode($content, $opts));
+ }
+}
+
echo "OK\n";
// Move some theme-specific resources from resources/themes/ to public/themes/
@@ -54,14 +68,19 @@
mkdir("{$rootDir}/public/themes/{$theme}");
}
- if (!file_exists("{$rootDir}/public/themes/{$theme}/images")) {
- mkdir("{$rootDir}/public/themes/{$theme}/images");
- }
+ // TODO: Public dirs (glob patterns) should be in theme's meta.json
+
+ foreach (['images', 'fonts'] as $subDir) {
+ if (file_exists("{$rootDir}/resources/themes/{$theme}/{$subDir}")) {
+ if (!file_exists("{$rootDir}/public/themes/{$theme}/{$subDir}")) {
+ mkdir("{$rootDir}/public/themes/{$theme}/{$subDir}");
+ }
- foreach (glob("{$file}/images/*") as $file) {
- $path = explode('/', $file);
- $image = $path[count($path)-1];
- copy($file, "{$rootDir}/public/themes/{$theme}/images/{$image}");
+ foreach (glob("{$rootDir}/resources/themes/{$theme}/{$subDir}/*") as $file) {
+ $filename = pathinfo($file, PATHINFO_BASENAME);
+ copy($file, "{$rootDir}/public/themes/{$theme}/{$subDir}/{$filename}");
+ }
+ }
}
}
diff --git a/src/resources/js/app.js b/src/resources/js/app.js
--- a/src/resources/js/app.js
+++ b/src/resources/js/app.js
@@ -14,7 +14,8 @@
const routerState = {
afterLogin: null,
- isLoggedIn: !!localStorage.getItem('token')
+ isLoggedIn: !!localStorage.getItem('token'),
+ isLocked: false
}
let loadingRoute
@@ -29,7 +30,12 @@
// redirect to login page
next({ name: 'login' })
+ return
+ }
+ if (routerState.isLocked && to.meta.requiresAuth && !['login', 'wallet'].includes(to.name)) {
+ // redirect to the wallet page
+ next({ name: 'wallet' })
return
}
@@ -139,8 +145,13 @@
this.authInfo = response
}
+ routerState.isLocked = this.authInfo && this.authInfo.isLocked
+
if (dashboard !== false) {
- this.$router.push(routerState.afterLogin || { name: 'dashboard' })
+ this.$router.push(routerState.afterLogin || { name: response.redirect || 'dashboard' })
+ } else if (routerState.isLocked && this.$route.name != 'wallet' && this.$route.meta.requiresAuth) {
+ // Always redirect locked user, here we can be after router's beforeEach handler
+ this.$router.push({ name: 'wallet' })
}
routerState.afterLogin = null
@@ -321,6 +332,10 @@
return this.$t('status.active')
},
+ unlock() {
+ routerState.isLocked = this.authInfo.isLocked = false
+ this.$router.push({ name: 'dashboard' })
+ },
// Append some wallet properties to the object
userWalletProps(object) {
let wallet = this.authInfo.accounts[0]
diff --git a/src/resources/js/locale.js b/src/resources/js/locale.js
--- a/src/resources/js/locale.js
+++ b/src/resources/js/locale.js
@@ -17,6 +17,7 @@
let currentLanguage
const loadedLanguages = ['en'] // our default language that is preloaded
+const loadedThemeLanguages = []
const setI18nLanguage = (lang) => {
i18n.locale = lang
@@ -32,7 +33,25 @@
const age = 10 * 60 * 60 * 24 * 365
document.cookie = 'language=' + lang + '; max-age=' + age + '; path=/; secure'
- return lang
+ // Load additional localization from the theme
+ return loadThemeLang(lang)
+}
+
+const loadThemeLang = (lang) => {
+ if (loadedThemeLanguages.includes(lang)) {
+ return
+ }
+
+ const theme = window.config['app.theme']
+
+ if (theme && theme != 'default') {
+ return import(/* webpackChunkName: "locale/[request]" */ `../build/js/${theme}-${lang}.json`)
+ .then(messages => {
+ i18n.mergeLocaleMessage(lang, messages.default)
+ loadedThemeLanguages.push(lang)
+ })
+ .catch(error => { /* ignore errors */ })
+ }
}
export const getLang = () => {
@@ -61,6 +80,6 @@
.then(messages => {
i18n.setLocaleMessage(lang, messages.default)
loadedLanguages.push(lang)
- return setI18nLanguage(lang)
+ return Promise.resolve(setI18nLanguage(lang))
})
}
diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php
--- a/src/resources/lang/en/ui.php
+++ b/src/resources/lang/en/ui.php
@@ -539,6 +539,7 @@
. " We will then convert this to {pc}, and on the next page you will be provided with the bank-details to transfer the amount in {pc}.",
'fill-up' => "Fill up by",
'history' => "History",
+ 'locked-text' => "The account is locked until you set up auto-payment successfully.",
'month' => "month",
'noperm' => "Only account owners can access a wallet.",
'norefund' => "The money in your wallet is non-refundable.",
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
@@ -2,11 +2,11 @@
<div class="container">
<div id="step0" v-if="!invitation">
<div class="plan-selector row row-cols-sm-2 g-3">
- <div v-for="item in plans" :key="item.id">
+ <div v-for="item in plans" :key="item.id" :id="'plan-' + item.title">
<div :class="'card bg-light plan-' + item.title">
<div class="card-header plan-header">
<div class="plan-ico text-center">
- <svg-icon :icon="plan_icons[item.title]"></svg-icon>
+ <svg-icon :icon="plan_icons[item.title] || 'user'"></svg-icon>
</div>
</div>
<div class="card-body text-center">
@@ -65,7 +65,7 @@
<div class="card d-none" id="step3">
<div class="card-body">
- <h4 v-if="!invitation" class="card-title">{{ $t('signup.title') }} - {{ $t('nav.step', { i: steps, n: steps }) }}</h4>
+ <h4 v-if="!invitation && steps > 1" class="card-title">{{ $t('signup.title') }} - {{ $t('nav.step', { i: steps, n: steps }) }}</h4>
<p class="card-text">
{{ $t('signup.step3', { app: $root.appName }) }}
</p>
@@ -144,7 +144,15 @@
},
computed: {
steps() {
- return this.mode == 'token' ? 2 : 3
+ switch (this.mode) {
+ case 'token':
+ return 2
+ case 'mandate':
+ return 1
+ case 'email':
+ default:
+ return 3
+ }
}
},
mounted() {
@@ -197,7 +205,24 @@
if (plan) {
this.plan = title
this.mode = plan.mode
- this.displayForm(1, true)
+ this.is_domain = plan.isDomain
+ this.domain = ''
+
+ let step = 1
+
+ if (plan.mode == 'mandate') {
+ step = 3
+ if (!plan.isDomain || !this.domains.length) {
+ axios.get('/api/auth/signup/domains')
+ .then(response => {
+ this.displayForm(step, true)
+ this.setDomain(response.data)
+ })
+ return
+ }
+ }
+
+ this.displayForm(step, true)
}
},
step0(plan) {
@@ -272,13 +297,12 @@
this.$root.clearFormValidation($('#step3 form'))
let post = {
- ...this.$root.pick(this, ['login', 'domain', 'voucher']),
+ ...this.$root.pick(this, ['login', 'domain', 'voucher', 'plan']),
...this.pass
}
if (this.invitation) {
post.invitation = this.invitation.id
- post.plan = this.plan
post.first_name = this.first_name
post.last_name = this.last_name
} else {
@@ -304,6 +328,10 @@
step = 1
}
+ if (this.mode == 'mandate') {
+ step = 0
+ }
+
$('#step' + step).removeClass('d-none').find('input').first().focus()
if (!step) {
@@ -331,7 +359,14 @@
this.domains = response.domains
}
- this.domain = response.domain || window.config['app.domain']
+ this.domain = response.domain
+
+ if (!this.domain) {
+ this.domain = window.config['app.domain']
+ if (this.domains.length && !this.domains.includes(this.domain)) {
+ this.domain = this.domains[0]
+ }
+ }
}
}
}
diff --git a/src/resources/vue/Wallet.vue b/src/resources/vue/Wallet.vue
--- a/src/resources/vue/Wallet.vue
+++ b/src/resources/vue/Wallet.vue
@@ -1,5 +1,8 @@
<template>
<div class="container" dusk="wallet-component">
+ <p v-if="$root.authInfo.isLocked" id="lock-alert" class="alert alert-warning">
+ {{ $t('wallet.locked-text') }}
+ </p>
<div v-if="wallet.id" id="wallet" class="card">
<div class="card-body">
<div class="card-title">{{ $t('wallet.title') }} <span :class="wallet.balance < 0 ? 'text-danger' : 'text-success'">{{ $root.price(wallet.balance, wallet.currency) }}</span></div>
@@ -9,10 +12,10 @@
<div v-if="showPendingPayments" class="alert alert-warning">
{{ $t('wallet.pending-payments-warning') }}
</div>
- <p>
+ <p v-if="$root.hasPermission('walletPayments')">
<btn class="btn-primary" @click="paymentMethodForm('manual')">{{ $t('wallet.add-credit') }}</btn>
</p>
- <div id="mandate-form" v-if="!mandate.isValid && !mandate.isPending">
+ <div id="mandate-form" v-if="!mandate.isValid && !mandate.isPending && $root.hasPermission('walletMandates')">
<template v-if="mandate.id && !mandate.isValid">
<div class="alert alert-danger">
{{ $t('wallet.auto-payment-failed') }}
@@ -21,7 +24,7 @@
</template>
<btn class="btn-primary" @click="paymentMethodForm('auto')">{{ $t('wallet.auto-payment-setup') }}</btn>
</div>
- <div id="mandate-info" v-else>
+ <div id="mandate-info" v-else-if="$root.hasPermission('walletMandates')">
<div v-if="mandate.isDisabled" class="disabled-mandate alert alert-danger">
{{ $t('wallet.auto-payment-disabled') }}
</div>
@@ -218,6 +221,9 @@
return tabs
}
},
+ beforeDestroyed() {
+ clearTimeout(this.refreshRequest)
+ },
mounted() {
$('#wallet button').focus()
@@ -249,15 +255,30 @@
this.$refs.tabs.clickHandler('payments', () => { this.loadPayments = true })
},
methods: {
- loadMandate() {
+ loadMandate(refresh) {
const loader = '#mandate-form'
this.$root.stopLoading(loader)
- if (!this.mandate.id || this.mandate.isPending) {
- axios.get('/api/v4/payments/mandate', { loader })
+ if (!this.mandate.id || this.mandate.isPending || refresh) {
+ axios.get('/api/v4/payments/mandate', refresh ? {} : { loader })
.then(response => {
this.mandate = response.data
+
+ if (this.mandate.minAmount) {
+ if (this.mandate.minAmount > this.mandate.amount) {
+ this.mandate.amount = this.mandate.minAmount
+ }
+ }
+
+ if (this.$root.authInfo.isLocked) {
+ if (this.mandate.isValid) {
+ this.$root.unlock()
+ } else {
+ clearTimeout(this.refreshRequest)
+ this.refreshRequest = setTimeout(() => { this.loadMandate(true) }, 10 * 1000)
+ }
+ }
})
}
},
@@ -372,6 +393,10 @@
axios.get('/api/v4/payments/methods', { params: { type }, loader })
.then(response => {
this.paymentMethods = response.data
+ if (this.paymentMethods.length == 1) {
+ this.nextForm = 'auto';
+ this.selectPaymentMethod(this.paymentMethods[0]);
+ }
})
})
},
diff --git a/src/resources/vue/Widgets/Menu.vue b/src/resources/vue/Widgets/Menu.vue
--- a/src/resources/vue/Widgets/Menu.vue
+++ b/src/resources/vue/Widgets/Menu.vue
@@ -10,21 +10,21 @@
</button>
<div :id="mode + '-menu-navbar'" :class="mode == 'header' ? 'collapse navbar-collapse justify-content-end' : ''">
<ul class="navbar-nav justify-content-end">
- <li class="nav-item" v-for="item in menu" :key="item.index">
- <a v-if="item.href" :class="'nav-link link-' + item.index" :href="item.href">{{ item.title }}</a>
+ <li class="nav-item" v-for="item in menu" :key="item.label">
+ <a v-if="item.href" :class="'nav-link link-' + item.label" :href="item.href">{{ menuItemTitle(item) }}</a>
<router-link v-if="item.to"
- :class="'nav-link link-' + item.index"
+ :class="'nav-link link-' + item.label"
active-class="active"
:to="item.to"
:exact="item.exact"
>
- {{ item.title }}
+ {{ menuItemTitle(item) }}
</router-link>
</li>
- <li class="nav-item" v-if="!loggedIn && $root.isUser">
+ <li class="nav-item" v-if="!loggedIn && $root.isUser && !hasMenuItem('signup')">
<router-link class="nav-link link-signup" active-class="active" :to="{name: 'signup'}">{{ $t('menu.signup') }}</router-link>
</li>
- <li class="nav-item" v-if="loggedIn">
+ <li class="nav-item" v-if="loggedIn && !hasMenuItem('dashboard')">
<router-link class="nav-link link-dashboard" active-class="active" :to="{name: 'dashboard'}">{{ $t('menu.cockpit') }}</router-link>
</li>
<li class="nav-item" v-if="loggedIn">
@@ -70,7 +70,13 @@
},
computed: {
loggedIn() { return !!this.$root.authInfo },
- menu() { return this.menuList.filter(item => !item.footer || this.mode == 'footer') },
+ menu() {
+ // Filter menu by its position on the page, and user authentication state
+ return this.menuList.filter(item => {
+ return (!item.footer || this.mode == 'footer')
+ && (!('authenticated' in item) || this.loggedIn === item.authenticated)
+ })
+ },
route() { return this.$route.name }
},
mounted() {
@@ -79,19 +85,14 @@
methods: {
loadMenu() {
let menu = []
- const lang = this.getLang()
const loggedIn = this.loggedIn
window.config.menu.forEach(item => {
- item.title = item['title-' + lang] || item['title-en'] || item.title
-
- if (!item.location || !item.title) {
+ if (!item.location || !item.label) {
console.error("Invalid menu entry", item)
return
}
- // TODO: Different menu for different loggedIn state
-
if (item.location.match(/^https?:/)) {
item.href = item.location
} else {
@@ -99,19 +100,24 @@
}
item.exact = item.location == '/'
- item.index = item.page || item.title.toLowerCase().replace(/\s+/g, '')
menu.push(item)
})
return menu
},
+ hasMenuItem(label) {
+ return this.menuList.find(item => item.label == label)
+ },
+ menuItemTitle(item) {
+ const lang = this.getLang()
+ return item['title-' + lang] || item['title-en'] || item.title || this.$t('menu.' + item.label)
+ },
getLang() {
return getLang()
},
setLang(language) {
setLang(language)
- this.menuList = this.loadMenu()
}
}
}
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -49,6 +49,7 @@
Route::post('password-reset/verify', [API\PasswordResetController::class, 'verify']);
Route::post('password-reset', [API\PasswordResetController::class, 'reset']);
+ Route::get('signup/domains', [API\SignupController::class, 'domains']);
Route::post('signup/init', [API\SignupController::class, 'init']);
Route::get('signup/invitations/{id}', [API\SignupController::class, 'invitation']);
Route::get('signup/plans', [API\SignupController::class, 'plans']);
diff --git a/src/tests/Browser/Pages/PaymentMollie.php b/src/tests/Browser/Pages/PaymentMollie.php
--- a/src/tests/Browser/Pages/PaymentMollie.php
+++ b/src/tests/Browser/Pages/PaymentMollie.php
@@ -25,7 +25,7 @@
*/
public function assert($browser)
{
- $browser->waitFor('form#body table, form#body iframe');
+ $browser->waitFor('form#body table, form#body iframe', 10);
}
/**
@@ -64,7 +64,7 @@
$browser->type('#expiryDate', '12/' . (date('y') + 1));
})
->withinFrame('#cvv iframe', function ($browser) {
- $browser->type('#verificationCode', '123');
+ $browser->click('#verificationCode')->type('#verificationCode', '123');
})
->click('#submit-button');
}
diff --git a/src/tests/Browser/Pages/PaymentStripe.php b/src/tests/Browser/Pages/PaymentStripe.php
--- a/src/tests/Browser/Pages/PaymentStripe.php
+++ b/src/tests/Browser/Pages/PaymentStripe.php
@@ -40,7 +40,7 @@
'@title' => '.App-Overview .ProductSummary',
'@amount' => '#ProductSummary-totalAmount',
'@description' => '#ProductSummary-Description',
- '@email-input' => '.App-Payment #email',
+ '@email' => '.App-Payment .ReadOnlyFormField-email .ReadOnlyFormField-content',
'@cardnumber-input' => '.App-Payment #cardNumber',
'@cardexpiry-input' => '.App-Payment #cardExpiry',
'@cardcvc-input' => '.App-Payment #cardCvc',
diff --git a/src/tests/Browser/PaymentMollieTest.php b/src/tests/Browser/PaymentMollieTest.php
--- a/src/tests/Browser/PaymentMollieTest.php
+++ b/src/tests/Browser/PaymentMollieTest.php
@@ -108,6 +108,7 @@
->on(new WalletPage())
->assertMissing('@body #mandate-form .alert')
->click('@main #mandate-form button')
+/*
->with(new Dialog('@payment-dialog'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Set up auto-payment')
->waitFor('#payment-method-selection .link-creditcard svg')
@@ -115,8 +116,10 @@
->assertMissing('#payment-method-selection .link-banktransfer')
->click('#payment-method-selection .link-creditcard');
})
+*/
->with(new Dialog('@payment-dialog'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Set up auto-payment')
+ ->waitFor('@body #mandate_amount')
->assertSeeIn('@body label[for="mandate_amount"]', 'Fill up by')
->assertValue('@body #mandate_amount', Payment::MIN_AMOUNT / 100)
->assertSeeIn('@body label[for="mandate_balance"]', 'when account balance is below') // phpcs:ignore
@@ -229,13 +232,16 @@
$browser->on(new WalletPage())
->assertMissing('@body #mandate-form .alert')
->click('@main #mandate-form button')
+/*
->with(new Dialog('@payment-dialog'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Set up auto-payment')
->waitFor('#payment-method-selection .link-creditcard')
->click('#payment-method-selection .link-creditcard');
})
+*/
->with(new Dialog('@payment-dialog'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Set up auto-payment')
+ ->waitFor('@body #mandate_amount')
->assertSeeIn('@button-cancel', 'Cancel')
->assertSeeIn('@button-action', 'Continue')
// Submit valid data
@@ -259,13 +265,16 @@
// Create a new mandate
->click('@main #mandate-form button')
+/*
->with(new Dialog('@payment-dialog'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Set up auto-payment')
->waitFor('#payment-method-selection .link-creditcard')
->click('#payment-method-selection .link-creditcard');
})
+*/
->with(new Dialog('@payment-dialog'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Set up auto-payment')
+ ->waitFor('@body #mandate_amount')
->assertSeeIn('@button-cancel', 'Cancel')
->assertSeeIn('@button-action', 'Continue')
// Submit valid data
diff --git a/src/tests/Browser/PaymentStripeTest.php b/src/tests/Browser/PaymentStripeTest.php
--- a/src/tests/Browser/PaymentStripeTest.php
+++ b/src/tests/Browser/PaymentStripeTest.php
@@ -80,7 +80,7 @@
->on(new PaymentStripe())
->assertSeeIn('@title', $user->tenant->title . ' Payment')
->assertSeeIn('@amount', 'CHF 12.34')
- ->assertValue('@email-input', $user->email)
+ ->assertSeeIn('@email', $user->email)
->submitValidCreditCard();
// Now it should redirect back to wallet page and in background
@@ -115,6 +115,7 @@
->on(new WalletPage())
->assertMissing('@body #mandate-form .alert')
->click('@main #mandate-form button')
+/*
->with(new Dialog('@payment-dialog'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Set up auto-payment')
->waitFor('#payment-method-selection .link-creditcard')
@@ -122,8 +123,10 @@
->assertMissing('#payment-method-selection .link-banktransfer')
->click('#payment-method-selection .link-creditcard');
})
+*/
->with(new Dialog('@payment-dialog'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Set up auto-payment')
+ ->waitFor('@body #mandate_amount')
->assertSeeIn('@body label[for="mandate_amount"]', 'Fill up by')
->assertValue('@body #mandate_amount', Payment::MIN_AMOUNT / 100)
->assertSeeIn('@body label[for="mandate_balance"]', 'when account balance is below') // phpcs:ignore
@@ -157,7 +160,7 @@
->on(new PaymentStripe())
->assertMissing('@title')
->assertMissing('@amount')
- ->assertValue('@email-input', $user->email)
+ ->assertSeeIn('@email', $user->email)
->submitValidCreditCard()
->waitForLocation('/wallet', 30) // need more time than default 5 sec.
->visit('/wallet?paymentProvider=stripe')
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
@@ -12,7 +12,9 @@
use Tests\Browser\Components\Menu;
use Tests\Browser\Components\Toast;
use Tests\Browser\Pages\Dashboard;
+use Tests\Browser\Pages\Home;
use Tests\Browser\Pages\Signup;
+use Tests\Browser\Pages\Wallet;
use Tests\TestCaseDusk;
use Illuminate\Foundation\Testing\DatabaseMigrations;
@@ -29,7 +31,7 @@
$this->deleteTestUser('admin@user-domain-signup.com');
$this->deleteTestDomain('user-domain-signup.com');
- Plan::where('mode', 'token')->update(['mode' => 'email']);
+ Plan::whereIn('mode', ['token', 'mandate'])->update(['mode' => 'email']);
}
/**
@@ -42,7 +44,7 @@
$this->deleteTestDomain('user-domain-signup.com');
SignupInvitation::truncate();
- Plan::where('mode', 'token')->update(['mode' => 'email']);
+ Plan::whereIn('mode', ['token', 'mandate'])->update(['mode' => 'email']);
@unlink(storage_path('signup-tokens.txt'));
@@ -519,6 +521,67 @@
}
/**
+ * Test signup with a mandate plan, also the wallet lock
+ */
+ public function testSignupMandate(): void
+ {
+ // Test the individual plan
+ $plan = Plan::withEnvTenantContext()->where('title', 'individual')->first();
+ $plan->mode = 'mandate';
+ $plan->save();
+
+ $this->browse(function (Browser $browser) {
+ $browser->visit(new Signup())
+ ->waitFor('@step0 .plan-individual button')
+ ->click('@step0 .plan-individual button')
+ // Test Back button
+ ->whenAvailable('@step3', function ($browser) {
+ $browser->click('button[type=button]');
+ })
+ ->whenAvailable('@step0', function ($browser) {
+ $browser->click('.plan-individual button');
+ })
+ // Test submit
+ ->whenAvailable('@step3', function ($browser) {
+ $domains = Domain::getPublicDomains();
+ $domains_count = count($domains);
+
+ $browser->assertMissing('.card-title')
+ ->assertElementsCount('select#signup_domain option', $domains_count, false)
+ ->assertText('select#signup_domain option:nth-child(1)', $domains[0])
+ ->assertValue('select#signup_domain option:nth-child(1)', $domains[0])
+ ->type('#signup_login', 'signuptestdusk')
+ ->type('#signup_password', '12345678')
+ ->type('#signup_password_confirmation', '12345678')
+ ->click('[type=submit]');
+ })
+ ->waitUntilMissing('@step3')
+ ->on(new Wallet())
+ ->assertSeeIn('#lock-alert', "The account is locked")
+ ->within(new Menu(), function ($browser) {
+ $browser->clickMenuItem('logout');
+ });
+ });
+
+ $user = User::where('email', 'signuptestdusk@' . \config('app.domain'))->first();
+ $this->assertSame($plan->id, $user->getSetting('plan_id'));
+
+ // Login again and see that the account is still locked
+ $this->browse(function (Browser $browser) use ($user) {
+ $browser->on(new Home())
+ ->submitLogon($user->email, '12345678', false)
+ ->waitForLocation('/wallet')
+ ->on(new Wallet())
+ ->assertSeeIn('#lock-alert', "The account is locked")
+ ->within(new Menu(), function ($browser) {
+ $browser->clickMenuItem('logout');
+ });
+
+ // TODO: Test automatic UI unlock after creating a valid auto-payment mandate
+ });
+ }
+
+ /**
* Test signup with a token plan
*/
public function testSignupToken(): void
diff --git a/src/tests/Feature/Controller/PaymentsMollieEuroTest.php b/src/tests/Feature/Controller/PaymentsMollieEuroTest.php
--- a/src/tests/Feature/Controller/PaymentsMollieEuroTest.php
+++ b/src/tests/Feature/Controller/PaymentsMollieEuroTest.php
@@ -501,7 +501,7 @@
$this->assertSame(2010, $transaction->amount);
$this->assertSame(
- "Auto-payment transaction {$payment->id} using Mastercard (**** **** **** 9399)",
+ "Auto-payment transaction {$payment->id} using Mastercard (**** **** **** 6787)",
$transaction->description
);
diff --git a/src/tests/Feature/Controller/PaymentsMollieTest.php b/src/tests/Feature/Controller/PaymentsMollieTest.php
--- a/src/tests/Feature/Controller/PaymentsMollieTest.php
+++ b/src/tests/Feature/Controller/PaymentsMollieTest.php
@@ -4,6 +4,7 @@
use App\Http\Controllers\API\V4\PaymentsController;
use App\Payment;
+use App\Plan;
use App\Providers\PaymentProvider;
use App\Transaction;
use App\Wallet;
@@ -47,6 +48,7 @@
Transaction::WALLET_CHARGEBACK,
];
Transaction::where('object_id', $wallet->id)->whereIn('type', $types)->delete();
+ Plan::withEnvTenantContext()->where('title', 'individual')->update(['mode' => 'email', 'months' => 1]);
}
/**
@@ -68,6 +70,7 @@
Transaction::WALLET_CHARGEBACK,
];
Transaction::where('object_id', $wallet->id)->whereIn('type', $types)->delete();
+ Plan::withEnvTenantContext()->where('title', 'individual')->update(['mode' => 'email', 'months' => 1]);
Utils::setTestExchangeRates([]);
parent::tearDown();
@@ -338,6 +341,44 @@
}
/**
+ * Test fetching an outo-payment mandate parameters
+ *
+ * @group mollie
+ */
+ public function testMandateParams(): void
+ {
+ $plan = Plan::withEnvTenantContext()->where('title', 'individual')->first();
+ $user = $this->getTestUser('payment-test@' . \config('app.domain'));
+ $wallet = $user->wallets()->first();
+
+ $response = $this->actingAs($user)->get("api/v4/payments/mandate");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame((int) ceil(Payment::MIN_AMOUNT / 100), $json['amount']);
+ $this->assertSame($json['amount'], $json['minAmount']);
+ $this->assertSame(0, $json['balance']);
+ $this->assertFalse($json['isValid']);
+ $this->assertFalse($json['isDisabled']);
+
+ $plan->months = 12;
+ $plan->save();
+ $user->setSetting('plan_id', $plan->id);
+
+ $response = $this->actingAs($user)->get("api/v4/payments/mandate");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame((int) ceil(Payment::MIN_AMOUNT / 100), $json['amount']);
+ $this->assertSame((int) ceil(($plan->cost() * $plan->months) / 100), $json['minAmount']);
+
+ // TODO: Test more cases
+ // TODO: Test user unrestricting if mandate is valid
+ }
+
+ /**
* Test creating a payment and receiving a status via webhook
*
* @group mollie
@@ -569,7 +610,7 @@
$this->assertSame(2010, $transaction->amount);
$this->assertSame(
- "Auto-payment transaction {$payment->id} using Mastercard (**** **** **** 9399)",
+ "Auto-payment transaction {$payment->id} using Mastercard (**** **** **** 6787)",
$transaction->description
);
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
@@ -5,6 +5,8 @@
use App\Http\Controllers\API\SignupController;
use App\Discount;
use App\Domain;
+use App\Plan;
+use App\Package;
use App\SignupCode;
use App\SignupInvitation as SI;
use App\User;
@@ -36,6 +38,7 @@
$this->deleteTestGroup('group-test@kolabnow.com');
SI::truncate();
+ Plan::where('title', 'test')->delete();
}
/**
@@ -53,6 +56,7 @@
$this->deleteTestGroup('group-test@kolabnow.com');
SI::truncate();
+ Plan::where('title', 'test')->delete();
parent::tearDown();
}
@@ -81,10 +85,28 @@
}
/**
+ * Test fetching public domains for signup
+ */
+ public function testSignupDomains(): void
+ {
+ $response = $this->get('/api/auth/signup/domains');
+ $json = $response->json();
+
+ $response->assertStatus(200);
+
+ $this->assertCount(2, $json);
+ $this->assertSame('success', $json['status']);
+ $this->assertSame(Domain::getPublicDomains(), $json['domains']);
+ }
+
+ /**
* Test fetching plans for signup
*/
public function testSignupPlans(): void
{
+ $individual = Plan::withEnvTenantContext()->where('title', 'individual')->first();
+ $group = Plan::withEnvTenantContext()->where('title', 'group')->first();
+
$response = $this->get('/api/auth/signup/plans');
$json = $response->json();
@@ -92,10 +114,16 @@
$this->assertSame('success', $json['status']);
$this->assertCount(2, $json['plans']);
- $this->assertArrayHasKey('title', $json['plans'][0]);
- $this->assertArrayHasKey('name', $json['plans'][0]);
- $this->assertArrayHasKey('description', $json['plans'][0]);
+ $this->assertSame($individual->title, $json['plans'][0]['title']);
+ $this->assertSame($individual->name, $json['plans'][0]['name']);
+ $this->assertSame($individual->description, $json['plans'][0]['description']);
+ $this->assertFalse($json['plans'][0]['isDomain']);
$this->assertArrayHasKey('button', $json['plans'][0]);
+ $this->assertSame($group->title, $json['plans'][1]['title']);
+ $this->assertSame($group->name, $json['plans'][1]['name']);
+ $this->assertSame($group->description, $json['plans'][1]['description']);
+ $this->assertTrue($json['plans'][1]['isDomain']);
+ $this->assertArrayHasKey('button', $json['plans'][1]);
}
/**
@@ -762,6 +790,62 @@
}
/**
+ * Test signup with mode=mandate
+ */
+ public function testSignupMandateMode(): void
+ {
+ Queue::fake();
+
+ $plan = Plan::create([
+ 'title' => 'test',
+ 'name' => 'Test Account',
+ 'description' => 'Test',
+ 'free_months' => 1,
+ 'discount_qty' => 0,
+ 'discount_rate' => 0,
+ 'mode' => 'mandate',
+ ]);
+
+ $packages = [
+ Package::where(['title' => 'kolab', 'tenant_id' => \config('app.tenant_id')])->first()
+ ];
+
+ $plan->packages()->saveMany($packages);
+
+ $post = [
+ 'plan' => 'abc',
+ 'login' => 'test-inv',
+ 'domain' => 'kolabnow.com',
+ 'password' => 'testtest',
+ 'password_confirmation' => 'testtest',
+ ];
+
+ // Test invalid plan identifier
+ $response = $this->post('/api/auth/signup', $post);
+ $response->assertStatus(422);
+
+ $json = $response->json();
+
+ $this->assertSame('error', $json['status']);
+ $this->assertCount(1, $json['errors']);
+ $this->assertSame("The selected plan is invalid.", $json['errors']['plan']);
+
+ // Test valid input
+ $post['plan'] = $plan->title;
+ $response = $this->post('/api/auth/signup', $post);
+ $json = $response->json();
+
+ $response->assertStatus(200);
+ $this->assertSame('success', $json['status']);
+ $this->assertNotEmpty($json['access_token']);
+ $this->assertSame('test-inv@kolabnow.com', $json['email']);
+ $this->assertTrue($json['isLocked']);
+ $user = User::where('email', 'test-inv@kolabnow.com')->first();
+ $this->assertNotEmpty($user);
+ $this->assertSame($plan->id, $user->getSetting('plan_id'));
+ }
+
+ /**
* Test signup via invitation
*/
public function testSignupViaInvitation(): void
diff --git a/src/tests/Feature/Controller/UsersTest.php b/src/tests/Feature/Controller/UsersTest.php
--- a/src/tests/Feature/Controller/UsersTest.php
+++ b/src/tests/Feature/Controller/UsersTest.php
@@ -6,6 +6,7 @@
use App\Domain;
use App\Http\Controllers\API\V4\UsersController;
use App\Package;
+use App\Plan;
use App\Sku;
use App\Tenant;
use App\User;
@@ -47,7 +48,10 @@
$wallet->save();
$user->settings()->whereIn('key', ['greylist_enabled', 'guam_enabled'])->delete();
$user->status |= User::STATUS_IMAP_READY | User::STATUS_LDAP_READY;
+ $user->status &= ~User::STATUS_RESTRICTED;
$user->save();
+ Plan::withEnvTenantContext()->where('title', 'individual')->update(['mode' => 'email']);
+ $user->setSettings(['plan_id' => null]);
}
/**
@@ -78,7 +82,10 @@
$wallet->save();
$user->settings()->whereIn('key', ['greylist_enabled', 'guam_enabled'])->delete();
$user->status |= User::STATUS_IMAP_READY | User::STATUS_LDAP_READY;
+ $user->status &= ~User::STATUS_RESTRICTED;
$user->save();
+ Plan::withEnvTenantContext()->where('title', 'individual')->update(['mode' => 'email']);
+ $user->setSettings(['plan_id' => null]);
parent::tearDown();
}
@@ -1372,14 +1379,15 @@
public function testUserResponse(): void
{
$provider = \config('services.payment_provider') ?: 'mollie';
- $user = $this->getTestUser('john@kolab.org');
- $wallet = $user->wallets()->first();
+ $john = $this->getTestUser('john@kolab.org');
+ $wallet = $john->wallets()->first();
$wallet->setSettings(['mollie_id' => null, 'stripe_id' => null]);
- $result = $this->invokeMethod(new UsersController(), 'userResponse', [$user]);
+ $wallet->owner->setSettings(['plan_id' => null]);
+ $result = $this->invokeMethod(new UsersController(), 'userResponse', [$john]);
- $this->assertEquals($user->id, $result['id']);
- $this->assertEquals($user->email, $result['email']);
- $this->assertEquals($user->status, $result['status']);
+ $this->assertEquals($john->id, $result['id']);
+ $this->assertEquals($john->email, $result['email']);
+ $this->assertEquals($john->status, $result['status']);
$this->assertTrue(is_array($result['statusInfo']));
$this->assertTrue(is_array($result['settings']));
@@ -1392,13 +1400,20 @@
$this->assertCount(1, $result['wallets']);
$this->assertSame($wallet->id, $result['wallet']['id']);
$this->assertArrayNotHasKey('discount', $result['wallet']);
+ $this->assertFalse($result['isLocked']);
$this->assertTrue($result['statusInfo']['enableDomains']);
$this->assertTrue($result['statusInfo']['enableWallets']);
+ $this->assertTrue($result['statusInfo']['enableWalletMandates']);
+ $this->assertTrue($result['statusInfo']['enableWalletPayments']);
$this->assertTrue($result['statusInfo']['enableUsers']);
$this->assertTrue($result['statusInfo']['enableSettings']);
// Ned is John's wallet controller
+ $plan = Plan::withEnvTenantContext()->where('title', 'individual')->first();
+ $plan->mode = 'mandate';
+ $plan->save();
+ $wallet->owner->setSettings(['plan_id' => $plan->id]);
$ned = $this->getTestUser('ned@kolab.org');
$ned_wallet = $ned->wallets()->first();
$result = $this->invokeMethod(new UsersController(), 'userResponse', [$ned]);
@@ -1414,9 +1429,12 @@
$this->assertSame($ned_wallet->id, $result['wallets'][0]['id']);
$this->assertSame($provider, $result['wallet']['provider']);
$this->assertSame($provider, $result['wallets'][0]['provider']);
+ $this->assertFalse($result['isLocked']);
$this->assertTrue($result['statusInfo']['enableDomains']);
$this->assertTrue($result['statusInfo']['enableWallets']);
+ $this->assertTrue($result['statusInfo']['enableWalletMandates']);
+ $this->assertFalse($result['statusInfo']['enableWalletPayments']);
$this->assertTrue($result['statusInfo']['enableUsers']);
$this->assertTrue($result['statusInfo']['enableSettings']);
@@ -1426,11 +1444,11 @@
$wallet->save();
$mod_provider = $provider == 'mollie' ? 'stripe' : 'mollie';
$wallet->setSetting($mod_provider . '_id', 123);
- $user->refresh();
+ $john->refresh();
- $result = $this->invokeMethod(new UsersController(), 'userResponse', [$user]);
+ $result = $this->invokeMethod(new UsersController(), 'userResponse', [$john]);
- $this->assertEquals($user->id, $result['id']);
+ $this->assertEquals($john->id, $result['id']);
$this->assertSame($discount->id, $result['wallet']['discount_id']);
$this->assertSame($discount->discount, $result['wallet']['discount']);
$this->assertSame($discount->description, $result['wallet']['discount_description']);
@@ -1439,6 +1457,7 @@
$this->assertSame($discount->discount, $result['wallets'][0]['discount']);
$this->assertSame($discount->description, $result['wallets'][0]['discount_description']);
$this->assertSame($mod_provider, $result['wallets'][0]['provider']);
+ $this->assertFalse($result['isLocked']);
// Jack is not a John's wallet controller
$jack = $this->getTestUser('jack@kolab.org');
@@ -1446,8 +1465,17 @@
$this->assertFalse($result['statusInfo']['enableDomains']);
$this->assertFalse($result['statusInfo']['enableWallets']);
+ $this->assertFalse($result['statusInfo']['enableWalletMandates']);
+ $this->assertFalse($result['statusInfo']['enableWalletPayments']);
$this->assertFalse($result['statusInfo']['enableUsers']);
$this->assertFalse($result['statusInfo']['enableSettings']);
+ $this->assertFalse($result['isLocked']);
+
+ // Test locked user
+ $john->restrict();
+ $result = $this->invokeMethod(new UsersController(), 'userResponse', [$john]);
+
+ $this->assertTrue($result['isLocked']);
}
/**
diff --git a/src/tests/Feature/PaymentTest.php b/src/tests/Feature/PaymentTest.php
--- a/src/tests/Feature/PaymentTest.php
+++ b/src/tests/Feature/PaymentTest.php
@@ -82,7 +82,8 @@
$wallet->balance = -5000;
$wallet->save();
- // Credit the 2nd payment
+ // Credit the 2nd payment (restricted user)
+ $user->restrict();
$payment2->credit('Test2');
$wallet->refresh();
$transaction = $wallet->transactions()->first();
@@ -91,6 +92,7 @@
$this->assertSame('1', $wallet->getSetting('mandate_disabled'));
$this->assertSame($payment2->credit_amount, $transaction->amount);
$this->assertSame("Auto-payment transaction {$payment2->id} using Test2", $transaction->description);
+ $this->assertFalse($user->refresh()->isRestricted());
}
/**
diff --git a/src/tests/Feature/UserTest.php b/src/tests/Feature/UserTest.php
--- a/src/tests/Feature/UserTest.php
+++ b/src/tests/Feature/UserTest.php
@@ -1160,12 +1160,24 @@
// Test an account with users, domain
$user = $this->getTestUser('UserAccountA@UserAccount.com');
+ $userB = $this->getTestUser('UserAccountB@UserAccount.com');
+ $package_kolab = \App\Package::withEnvTenantContext()->where('title', 'kolab')->first();
+ $package_domain = \App\Package::withEnvTenantContext()->where('title', 'domain-hosting')->first();
+ $domain = $this->getTestDomain('UserAccount.com', [
+ 'status' => Domain::STATUS_NEW,
+ 'type' => Domain::TYPE_HOSTED,
+ ]);
+ $user->assignPackage($package_kolab);
+ $domain->assignPackage($package_domain, $user);
+ $user->assignPackage($package_kolab, $userB);
$this->assertFalse($user->isRestricted());
+ $this->assertFalse($userB->isRestricted());
$user->restrict();
$this->assertTrue($user->fresh()->isRestricted());
+ $this->assertFalse($userB->fresh()->isRestricted());
Queue::assertPushed(
\App\Jobs\User\UpdateJob::class,
@@ -1174,12 +1186,16 @@
}
);
+ $userB->restrict();
+ $this->assertTrue($userB->fresh()->isRestricted());
+
Queue::fake(); // reset queue state
$user->refresh();
$user->unrestrict();
$this->assertFalse($user->fresh()->isRestricted());
+ $this->assertTrue($userB->fresh()->isRestricted());
Queue::assertPushed(
\App\Jobs\User\UpdateJob::class,
@@ -1187,6 +1203,20 @@
return TestCase::getObjectProperty($job, 'userId') == $user->id;
}
);
+
+ Queue::fake(); // reset queue state
+
+ $user->unrestrict(true);
+
+ $this->assertFalse($user->fresh()->isRestricted());
+ $this->assertFalse($userB->fresh()->isRestricted());
+
+ Queue::assertPushed(
+ \App\Jobs\User\UpdateJob::class,
+ function ($job) use ($userB) {
+ return TestCase::getObjectProperty($job, 'userId') == $userB->id;
+ }
+ );
}
/**
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sun, Apr 5, 1:07 PM (8 h, 2 s ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18833513
Default Alt Text
D4136.1775394458.diff (63 KB)
Attached To
Mode
D4136: Signup 'mandate' mode and other improvements
Attached
Detach File
Event Timeline