Page MenuHomePhorge

D4136.1775394458.diff
No OneTemporary

Authored By
Unknown
Size
63 KB
Referenced Files
None
Subscribers
None

D4136.1775394458.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
@@ -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

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)

Event Timeline