Page MenuHomePhorge

D4202.1775368305.diff
No OneTemporary

Authored By
Unknown
Size
49 KB
Referenced Files
None
Subscribers
None

D4202.1775368305.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
@@ -7,6 +7,7 @@
use App\Discount;
use App\Domain;
use App\Plan;
+use App\Providers\PaymentProvider;
use App\Rules\SignupExternalEmail;
use App\Rules\SignupToken;
use App\Rules\Password;
@@ -34,27 +35,25 @@
*/
public function plans(Request $request)
{
- $plans = [];
-
// Use reverse order just to have individual on left, group on right ;)
// 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);
- if ($button == 'theme::app.planbutton-' . $plan->title) {
- $button = \trans('app.planbutton', ['plan' => $plan->name]);
+ $plans = Plan::withEnvTenantContext()->orderBy('months')->orderByDesc('title')->get()
+ ->map(function ($plan) {
+ $button = self::trans("app.planbutton-{$plan->title}");
+ if (strpos($button, 'app.planbutton') !== false) {
+ $button = self::trans('app.planbutton', ['plan' => $plan->name]);
}
- $plans[] = [
+ return [
'title' => $plan->title,
'name' => $plan->name,
'button' => $button,
'description' => $plan->description,
- 'mode' => $plan->mode ?: 'email',
+ 'mode' => $plan->mode ?: Plan::MODE_EMAIL,
'isDomain' => $plan->hasDomain(),
];
- });
+ })
+ ->all();
return response()->json(['status' => 'success', 'plans' => $plans]);
}
@@ -91,7 +90,7 @@
$plan = $this->getPlan();
- if ($plan->mode == 'token') {
+ if ($plan->mode == Plan::MODE_TOKEN) {
$rules['token'] = ['required', 'string', new SignupToken()];
} else {
$rules['email'] = ['required', 'string', new SignupExternalEmail()];
@@ -106,7 +105,7 @@
// Generate the verification code
$code = SignupCode::create([
- 'email' => $plan->mode == 'token' ? $request->token : $request->email,
+ 'email' => $plan->mode == Plan::MODE_TOKEN ? $request->token : $request->email,
'first_name' => $request->first_name,
'last_name' => $request->last_name,
'plan' => $plan->title,
@@ -119,7 +118,7 @@
'mode' => $plan->mode ?: 'email',
];
- if ($plan->mode == 'token') {
+ if ($plan->mode == Plan::MODE_TOKEN) {
// Token verification, jump to the last step
$has_domain = $plan->hasDomain();
@@ -221,13 +220,13 @@
}
/**
- * Finishes the signup process by creating the user account.
+ * Validates the input to the final signup request.
*
* @param \Illuminate\Http\Request $request HTTP request
*
* @return \Illuminate\Http\JsonResponse JSON response
*/
- public function signup(Request $request)
+ public function signupValidate(Request $request)
{
// Validate input
$v = Validator::make(
@@ -244,14 +243,13 @@
return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
}
-
$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') {
+ if (!$plan || $plan->mode != Plan::MODE_MANDATE) {
$msg = \trans('validation.exists', ['attribute' => 'plan']);
return response()->json(['status' => 'error', 'errors' => ['plan' => $msg]], 422);
}
@@ -300,7 +298,7 @@
'last_name' => $code_data->last_name,
];
- if ($plan->mode == 'token') {
+ if ($plan->mode == Plan::MODE_TOKEN) {
$settings['signup_token'] = $code_data->email;
} else {
$settings['external_email'] = $code_data->email;
@@ -323,17 +321,46 @@
}
$is_domain = $plan->hasDomain();
- $login = $request->login;
- $domain_name = $request->domain;
// Validate login
- if ($errors = self::validateLogin($login, $domain_name, $is_domain)) {
+ if ($errors = self::validateLogin($request->login, $request->domain, $is_domain)) {
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
+ // Set some properties for signup() method
+ $request->settings = $settings;
+ $request->plan = $plan;
+ $request->discount = $discount ?? null;
+ $request->invitation = $invitation ?? null;
+
+ $result = [];
+
+ if ($plan->mode == Plan::MODE_MANDATE) {
+ $result = $this->mandateForPlan($plan, $request->discount);
+ }
+
+ return response()->json($result);
+ }
+
+ /**
+ * Finishes the signup process by creating the user account.
+ *
+ * @param \Illuminate\Http\Request $request HTTP request
+ *
+ * @return \Illuminate\Http\JsonResponse JSON response
+ */
+ public function signup(Request $request)
+ {
+ $v = $this->signupValidate($request);
+ if ($v->status() !== 200) {
+ return $v;
+ }
+
+ $is_domain = $request->plan->hasDomain();
+
// We allow only ASCII, so we can safely lower-case the email address
- $login = Str::lower($login);
- $domain_name = Str::lower($domain_name);
+ $login = Str::lower($request->login);
+ $domain_name = Str::lower($request->domain);
$domain = null;
DB::beginTransaction();
@@ -353,22 +380,22 @@
'status' => User::STATUS_RESTRICTED,
]);
- if (!empty($discount)) {
+ if ($request->discount) {
$wallet = $user->wallets()->first();
- $wallet->discount()->associate($discount);
+ $wallet->discount()->associate($request->discount);
$wallet->save();
}
- $user->assignPlan($plan, $domain);
+ $user->assignPlan($request->plan, $domain);
// Save the external email and plan in user settings
- $user->setSettings($settings);
+ $user->setSettings($request->settings);
// Update the invitation
- if (!empty($invitation)) {
- $invitation->status = SignupInvitation::STATUS_COMPLETED;
- $invitation->user_id = $user->id;
- $invitation->save();
+ if ($request->invitation) {
+ $request->invitation->status = SignupInvitation::STATUS_COMPLETED;
+ $request->invitation->user_id = $user->id;
+ $request->invitation->save();
}
// Soft-delete the verification code, and store some more info with it
@@ -384,14 +411,67 @@
$response = AuthController::logonResponse($user, $request->password);
- // Redirect the user to the specified page
- // $data = $response->getData(true);
- // $data['redirect'] = 'wallet';
- // $response->setData($data);
+ if ($request->plan->mode == Plan::MODE_MANDATE) {
+ $data = $response->getData(true);
+ $data['checkout'] = $this->mandateForPlan($request->plan, $request->discount, $user);
+ $response->setData($data);
+ }
return $response;
}
+ /**
+ * Collects some content to display to the user before redirect to a checkout page.
+ * Optionally creates a recurrent payment mandate for specified user/plan.
+ */
+ protected function mandateForPlan(Plan $plan, Discount $discount = null, User $user = null): array
+ {
+ $result = [];
+
+ $min = \App\Payment::MIN_AMOUNT;
+ $planCost = $plan->cost() * $plan->months;
+
+ if ($discount) {
+ $planCost -= ceil($planCost * (100 - $discount->discount) / 100);
+ }
+
+ if ($planCost > $min) {
+ $min = $planCost;
+ }
+
+ if ($user) {
+ $wallet = $user->wallets()->first();
+ $wallet->setSettings([
+ 'mandate_amount' => sprintf('%.2f', round($min / 100, 2)),
+ 'mandate_balance' => 0,
+ ]);
+
+ $mandate = [
+ 'currency' => $wallet->currency,
+ 'description' => \App\Tenant::getConfig($user->tenant_id, 'app.name') . ' Auto-Payment Setup',
+ 'methodId' => PaymentProvider::METHOD_CREDITCARD,
+ 'redirectUrl' => \App\Utils::serviceUrl('/payment/status', $user->tenant_id),
+ ];
+
+ $provider = PaymentProvider::factory($wallet);
+
+ $result = $provider->createMandate($wallet, $mandate);
+ }
+
+ $params = [
+ 'cost' => \App\Utils::money($planCost, \config('app.currency')),
+ 'period' => \trans($plan->months == 12 ? 'app.period-year' : 'app.period-month'),
+ ];
+
+ $content = '<b>' . self::trans('app.signup-account-tobecreated') . '</b><br><br>'
+ . self::trans('app.signup-account-summary', $params) . '<br><br>'
+ . self::trans('app.signup-account-mandate', $params);
+
+ $result['content'] = $content;
+
+ return $result;
+ }
+
/**
* Returns plan for the signup process
*
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
@@ -139,6 +139,36 @@
return response()->json($result);
}
+ /**
+ * Reset the auto-payment mandate, create a new payment for it.
+ *
+ * @param \Illuminate\Http\Request $request The API request.
+ *
+ * @return \Illuminate\Http\JsonResponse The response
+ */
+ public function mandateReset(Request $request)
+ {
+ $user = $this->guard()->user();
+
+ // TODO: Wallet selection
+ $wallet = $user->wallets()->first();
+
+ $mandate = [
+ 'currency' => $wallet->currency,
+ 'description' => Tenant::getConfig($user->tenant_id, 'app.name') . ' Auto-Payment Setup',
+ 'methodId' => $request->methodId ?: PaymentProvider::METHOD_CREDITCARD,
+ 'redirectUrl' => \App\Utils::serviceUrl('/payment/status', $user->tenant_id),
+ ];
+
+ $provider = PaymentProvider::factory($wallet);
+
+ $result = $provider->createMandate($wallet, $mandate);
+
+ $result['status'] = 'success';
+
+ return response()->json($result);
+ }
+
/**
* Validate an auto-payment mandate request.
*
@@ -172,7 +202,7 @@
$label = 'minamount';
if (($plan = $wallet->plan()) && $plan->months >= 1) {
- $planCost = (int) ceil($plan->cost() * $plan->months);
+ $planCost = $plan->cost() * $plan->months;
if ($planCost > $min) {
$min = $planCost;
}
@@ -190,6 +220,39 @@
return null;
}
+ /**
+ * Get status of the last payment.
+ *
+ * @return \Illuminate\Http\JsonResponse The response
+ */
+ public function paymentStatus()
+ {
+ $user = $this->guard()->user();
+ $wallet = $user->wallets()->first();
+
+ $payment = $wallet->payments()->orderBy('created_at', 'desc')->first();
+
+ if (empty($payment)) {
+ return $this->errorResponse(404);
+ }
+
+ $done = [Payment::STATUS_PAID, Payment::STATUS_CANCELED, Payment::STATUS_FAILED, Payment::STATUS_EXPIRED];
+
+ if (in_array($payment->status, $done)) {
+ $label = "app.payment-status-{$payment->status}";
+ } else {
+ $label = "app.payment-status-checking";
+ }
+
+ return response()->json([
+ 'id' => $payment->id,
+ 'status' => $payment->status,
+ 'type' => $payment->type,
+ 'statusMessage' => \trans($label),
+ 'description' => $payment->description,
+ ]);
+ }
+
/**
* Create a new payment.
*
@@ -386,7 +449,7 @@
// 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);
+ $planCost = round($plan->cost() * $plan->months / 100, 2);
if ($planCost > $mandate['minAmount']) {
$mandate['minAmount'] = $planCost;
}
@@ -514,6 +577,9 @@
/**
* Calculates tax for the payment, fills the request with additional properties
+ *
+ * @param \App\Wallet $wallet The wallet
+ * @param array $request The request data with the payment amount
*/
protected static function addTax(Wallet $wallet, array &$request): void
{
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
@@ -4,6 +4,7 @@
use App\Http\Controllers\RelationController;
use App\Domain;
+use App\Plan;
use App\Rules\Password;
use App\Rules\UserEmailDomain;
use App\Rules\UserEmailLocal;
@@ -203,7 +204,7 @@
'enableUsers' => $isController,
'enableWallets' => $isController,
'enableWalletMandates' => $isController,
- 'enableWalletPayments' => $isController && (!$plan || $plan->mode != 'mandate'),
+ 'enableWalletPayments' => $isController && (!$plan || $plan->mode != Plan::MODE_MANDATE),
'enableCompanionapps' => $hasBeta,
];
@@ -356,7 +357,7 @@
$wallet = $user->wallet();
// IsLocked flag to lock the user to the Wallet page only
- $response['isLocked'] = ($user->isRestricted() && ($plan = $wallet->plan()) && $plan->mode == 'mandate');
+ $response['isLocked'] = (!$user->isActive() && ($plan = $wallet->plan()) && $plan->mode == Plan::MODE_MANDATE);
// Settings
$response['settings'] = [];
diff --git a/src/app/Http/Controllers/Controller.php b/src/app/Http/Controllers/Controller.php
--- a/src/app/Http/Controllers/Controller.php
+++ b/src/app/Http/Controllers/Controller.php
@@ -81,4 +81,20 @@
{
return Auth::guard();
}
+
+ /**
+ * A wrapper for \trans() with theme localization support.
+ *
+ * @param string $label Localization label
+ * @param array $params Translation parameters
+ */
+ public static function trans(string $label, array $params = []): string
+ {
+ $result = \trans("theme::{$label}", $params);
+ if ($result === "theme::{$label}") {
+ $result = \trans($label, $params);
+ }
+
+ return $result;
+ }
}
diff --git a/src/app/Jobs/User/CreateJob.php b/src/app/Jobs/User/CreateJob.php
--- a/src/app/Jobs/User/CreateJob.php
+++ b/src/app/Jobs/User/CreateJob.php
@@ -102,7 +102,14 @@
$user->status |= \App\User::STATUS_IMAP_READY;
}
- $user->status |= \App\User::STATUS_ACTIVE;
+ // Make user active in non-mandate mode only
+ if (!($wallet = $user->wallet())
+ || !($plan = $user->wallet()->plan())
+ || $plan->mode != \App\Plan::MODE_MANDATE
+ ) {
+ $user->status |= \App\User::STATUS_ACTIVE;
+ }
+
$user->save();
}
}
diff --git a/src/app/Payment.php b/src/app/Payment.php
--- a/src/app/Payment.php
+++ b/src/app/Payment.php
@@ -12,6 +12,8 @@
* @property int $credit_amount Amount of money in cents of system currency (wallet balance)
* @property string $description Payment description
* @property string $id Mollie's Payment ID
+ * @property string $status Payment status (Payment::STATUS_*)
+ * @property string $type Payment type (Payment::TYPE_*)
* @property ?string $vat_rate_id VAT rate identifier
* @property \App\Wallet $wallet The wallet
* @property string $wallet_id The ID of the wallet
@@ -126,9 +128,16 @@
$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);
+ if ($owner = $this->wallet->owner) {
+ // Remove RESTRICTED flag from the wallet owner and all users in the wallet
+ if ($owner->isRestricted()) {
+ $owner->unrestrict(true);
+ }
+ // Activate the inactive user
+ if (!$owner->isActive()) {
+ $owner->status |= User::STATUS_ACTIVE;
+ $owner->save();
+ }
}
}
diff --git a/src/app/Plan.php b/src/app/Plan.php
--- a/src/app/Plan.php
+++ b/src/app/Plan.php
@@ -20,7 +20,7 @@
* @property int $discount_rate
* @property int $free_months
* @property string $id
- * @property string $mode Plan signup mode (email|token)
+ * @property string $mode Plan signup mode (Plan::MODE_*)
* @property string $name
* @property \App\Package[] $packages
* @property datetime $promo_from
@@ -34,6 +34,11 @@
use HasTranslations;
use UuidStrKeyTrait;
+ public const MODE_EMAIL = 'email';
+ public const MODE_TOKEN = 'token';
+ public const MODE_MANDATE = 'mandate';
+
+ /** @var bool Indicates if the model should be timestamped. */
public $timestamps = false;
/** @var array<int, string> The attributes that are mass assignable */
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
@@ -54,6 +54,7 @@
* - currency: The operation currency
* - description: Operation desc.
* - methodId: Payment method
+ * - redirectUrl: The location to goto after checkout
*
* @return array Provider payment data:
* - id: Operation identifier
@@ -80,7 +81,7 @@
'sequenceType' => 'first',
'description' => $payment['description'],
'webhookUrl' => Utils::serviceUrl('/api/webhooks/payment/mollie'),
- 'redirectUrl' => self::redirectUrl(),
+ 'redirectUrl' => $payment['redirectUrl'] ?? self::redirectUrl(),
'locale' => 'en_US',
'method' => $payment['methodId']
];
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
@@ -59,6 +59,7 @@
* - amount: Value in cents (not used)
* - currency: The operation currency
* - description: Operation desc.
+ * - redirectUrl: The location to goto after checkout
*
* @return array Provider payment/session data:
* - id: Session identifier
@@ -70,8 +71,8 @@
$request = [
'customer' => $customer_id,
- 'cancel_url' => self::redirectUrl(), // required
- 'success_url' => self::redirectUrl(), // required
+ 'cancel_url' => $payment['redirectUrl'] ?? self::redirectUrl(), // required
+ 'success_url' => $payment['redirectUrl'] ?? self::redirectUrl(), // required
'payment_method_types' => ['card'], // required
'locale' => 'en',
'mode' => 'setup',
diff --git a/src/app/Providers/PaymentProvider.php b/src/app/Providers/PaymentProvider.php
--- a/src/app/Providers/PaymentProvider.php
+++ b/src/app/Providers/PaymentProvider.php
@@ -89,6 +89,7 @@
* - currency: The operation currency
* - description: Operation desc.
* - methodId: Payment method
+ * - redirectUrl: The location to goto after checkout
*
* @return array Provider payment data:
* - id: Operation identifier
diff --git a/src/app/Utils.php b/src/app/Utils.php
--- a/src/app/Utils.php
+++ b/src/app/Utils.php
@@ -576,4 +576,25 @@
return floatval($rates[$targetCurrency]);
}
+
+ /**
+ * A helper to display human-readable amount of money using
+ * for specified currency and locale.
+ *
+ * @param int $amount Amount of money (in cents)
+ * @param string $currency Currency code
+ * @param string $locale Output locale
+ *
+ * @return string String representation, e.g. "9.99 CHF"
+ */
+ public static function money(int $amount, $currency, $locale = 'de_DE'): string
+ {
+ $amount = round($amount / 100, 2);
+
+ $nf = new \NumberFormatter($locale, \NumberFormatter::CURRENCY);
+ $result = $nf->formatCurrency($amount, $currency);
+
+ // Replace non-breaking space
+ return str_replace("\xC2\xA0", " ", $result);
+ }
}
diff --git a/src/app/Wallet.php b/src/app/Wallet.php
--- a/src/app/Wallet.php
+++ b/src/app/Wallet.php
@@ -462,12 +462,7 @@
*/
public function money(int $amount, $locale = 'de_DE')
{
- $amount = round($amount / 100, 2);
-
- $nf = new \NumberFormatter($locale, \NumberFormatter::CURRENCY);
- $result = $nf->formatCurrency($amount, $this->currency);
- // Replace non-breaking space
- return str_replace("\xC2\xA0", " ", $result);
+ return \App\Utils::money($amount, $this->currency, $locale);
}
/**
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
@@ -33,9 +33,9 @@
return
}
- if (routerState.isLocked && to.meta.requiresAuth && !['login', 'wallet'].includes(to.name)) {
- // redirect to the wallet page
- next({ name: 'wallet' })
+ if (routerState.isLocked && to.meta.requiresAuth && !['login', 'payment-status'].includes(to.name)) {
+ // redirect to the payment-status page
+ next({ name: 'payment-status' })
return
}
@@ -149,9 +149,9 @@
if (dashboard !== false) {
this.$router.push(routerState.afterLogin || { name: response.redirect || 'dashboard' })
- } else if (routerState.isLocked && this.$route.name != 'wallet' && this.$route.meta.requiresAuth) {
+ } else if (routerState.isLocked && this.$route.meta.requiresAuth && this.$route.name != 'payment-status') {
// Always redirect locked user, here we can be after router's beforeEach handler
- this.$router.push({ name: 'wallet' })
+ this.$router.push({ name: 'payment-status' })
}
routerState.afterLogin = null
diff --git a/src/resources/js/user/routes.js b/src/resources/js/user/routes.js
--- a/src/resources/js/user/routes.js
+++ b/src/resources/js/user/routes.js
@@ -17,6 +17,7 @@
const DomainListComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Domain/List')
const FileInfoComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/File/Info')
const FileListComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/File/List')
+const PaymentStatusComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Payment/Status')
const ResourceInfoComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Resource/Info')
const ResourceListComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Resource/List')
const RoomInfoComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Room/Info')
@@ -107,6 +108,12 @@
name: 'password-reset',
component: PasswordResetComponent
},
+ {
+ path: '/payment/status',
+ name: 'payment-status',
+ component: PaymentStatusComponent,
+ meta: { requiresAuth: true }
+ },
{
path: '/profile',
name: 'profile',
diff --git a/src/resources/lang/en/app.php b/src/resources/lang/en/app.php
--- a/src/resources/lang/en/app.php
+++ b/src/resources/lang/en/app.php
@@ -79,6 +79,15 @@
'file-permissions-update-success' => 'File permissions updated successfully.',
'file-permissions-delete-success' => 'File permissions deleted successfully.',
+ 'payment-status-paid' => 'The payment has been completed successfully.',
+ 'payment-status-canceled' => 'The payment has been canceled.',
+ 'payment-status-failed' => 'The payment failed.',
+ 'payment-status-expired' => 'The payment expired.',
+ 'payment-status-checking' => "The payment hasn't been completed yet. Checking the status...",
+
+ 'period-year' => 'year',
+ 'period-month' => 'month',
+
'resource-update-success' => 'Resource updated successfully.',
'resource-create-success' => 'Resource created successfully.',
'resource-delete-success' => 'Resource deleted successfully.',
@@ -112,6 +121,10 @@
'search-foundxshared-folders' => ':x shared folders have been found.',
'search-foundxusers' => ':x user accounts have been found.',
+ 'signup-account-tobecreated' => 'The account is about to be created!',
+ 'signup-account-mandate' => 'Now it is required to provide your credit card details.'
+ . ' This way you agree to charge you with an appropriate amount of money according to the plan you signed up for.',
+ 'signup-account-summary' => 'You signed up for an account with a base cost of :cost per :period.',
'signup-invitations-created' => 'The invitation has been created.|:count invitations has been created.',
'signup-invitations-csv-empty' => 'Failed to find any valid email addresses in the uploaded file.',
'signup-invitations-csv-invalid-email' => 'Found an invalid email address (:email) on line :line.',
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
@@ -35,6 +35,7 @@
'signup' => "Sign Up",
'submit' => "Submit",
'suspend' => "Suspend",
+ 'tryagain' => "Try again",
'unsuspend' => "Unsuspend",
'verify' => "Verify",
],
diff --git a/src/resources/vue/Payment/Status.vue b/src/resources/vue/Payment/Status.vue
new file mode 100644
--- /dev/null
+++ b/src/resources/vue/Payment/Status.vue
@@ -0,0 +1,64 @@
+<template>
+ <div class="container">
+ <p v-if="$root.authInfo.isLocked" id="lock-alert" class="alert alert-warning">
+ {{ $t('wallet.locked-text') }}
+ </p>
+ <div class="card">
+ <div class="card-body">
+ <div class="card-text" v-html="payment.statusMessage"></div>
+ <div class="mt-4">
+ <btn v-if="payment.tryagain" @click="tryAgain" class="btn-primary">{{ $t('btn.tryagain') }}</btn>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script>
+ export default {
+ data() {
+ return {
+ payment: {}
+ }
+ },
+ mounted() {
+ this.paymentStatus(true)
+ },
+ beforeDestroy() {
+ clearTimeout(this.timeout)
+ },
+ methods: {
+ paymentStatus(loader) {
+ axios.get('/api/v4/payments/status', { loader })
+ .then(response => {
+ this.payment = response.data
+ this.payment.tryagain = this.payment.type == 'mandate' && this.payment.status != 'paid'
+
+ if (this.payment.status == 'paid' && this.$root.authInfo.isLocked) {
+ // unlock, and redirect to the Dashboard
+ this.timeout = setTimeout(() => this.$root.unlock(), 5000)
+ } else if (['open', 'pending', 'authorized'].includes(this.payment.status)) {
+ // wait some time and check again
+ this.timeout = setTimeout(() => this.paymentStatus(false), 5000)
+ }
+ })
+ .catch(error => {
+ this.$root.errorHandler(error)
+ })
+ },
+ tryAgain() {
+ // Create the first payment and goto to the checkout page, again
+ axios.post('/api/v4/payments/mandate/reset')
+ .then(response => {
+ clearTimeout(this.timeout)
+ // TODO: We have this code in a few places now, de-duplicate!
+ if (response.data.redirectUrl) {
+ location.href = response.data.redirectUrl
+ } else if (response.data.id) {
+ // TODO: this.stripeCheckout(response.data)
+ }
+ })
+ }
+ }
+ }
+</script>
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
@@ -100,6 +100,16 @@
</form>
</div>
</div>
+
+ <div class="card d-none" id="step4">
+ <div class="card-body">
+ <div class="card-text mb-4" v-html="checkout.content"></div>
+ <form>
+ <btn class="btn-secondary me-2" @click="stepBack">{{ $t('btn.back') }}</btn>
+ <btn class="btn-primary" @click="submitStep4">{{ $t('btn.continue') }}</btn>
+ </form>
+ </div>
+ </div>
</div>
</template>
@@ -119,6 +129,7 @@
},
data() {
return {
+ checkout: {},
email: '',
first_name: '',
last_name: '',
@@ -296,23 +307,34 @@
submitStep3() {
this.$root.clearFormValidation($('#step3 form'))
- let post = {
- ...this.$root.pick(this, ['login', 'domain', 'voucher', 'plan']),
- ...this.pass
- }
+ const post = this.lastStepPostData()
- if (this.invitation) {
- post.invitation = this.invitation.id
- post.first_name = this.first_name
- post.last_name = this.last_name
+ if (this.mode == 'mandate') {
+ axios.post('/api/auth/signup/validate', post).then(response => {
+ this.checkout = response.data
+ this.displayForm(4)
+ })
} else {
- post.code = this.code
- post.short_code = this.short_code
+ axios.post('/api/auth/signup', post).then(response => {
+ // auto-login and goto dashboard
+ this.$root.loginUser(response.data)
+ })
}
+ },
+ submitStep4() {
+ const post = this.lastStepPostData()
axios.post('/api/auth/signup', post).then(response => {
- // auto-login and goto dashboard
- this.$root.loginUser(response.data)
+ // auto-login and goto to the payment checkout
+ this.$root.loginUser(response.data, false)
+
+ let checkout = response.data.checkout
+
+ if (checkout.redirectUrl) {
+ location.href = checkout.redirectUrl
+ } else if (checkout.id) {
+ // TODO: this.stripeCheckout(checkout)
+ }
})
},
// Moves the user a step back in registration form
@@ -328,7 +350,7 @@
step = 1
}
- if (this.mode == 'mandate') {
+ if (this.mode == 'mandate' && step < 3) {
step = 0
}
@@ -340,7 +362,7 @@
}
},
displayForm(step, focus) {
- [0, 1, 2, 3].filter(value => value != step).forEach(value => {
+ [0, 1, 2, 3, 4].filter(value => value != step).forEach(value => {
$('#step' + value).addClass('d-none')
})
@@ -354,6 +376,23 @@
$('#step' + step).find('input').first().focus()
}
},
+ lastStepPostData() {
+ let post = {
+ ...this.$root.pick(this, ['login', 'domain', 'voucher', 'plan']),
+ ...this.pass
+ }
+
+ if (this.invitation) {
+ post.invitation = this.invitation.id
+ post.first_name = this.first_name
+ post.last_name = this.last_name
+ } else {
+ post.code = this.code
+ post.short_code = this.short_code
+ }
+
+ return post
+ },
setDomain(response) {
if (response.domains) {
this.domains = response.domains
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,8 +1,5 @@
<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>
@@ -221,9 +218,6 @@
return tabs
}
},
- beforeDestroyed() {
- clearTimeout(this.refreshRequest)
- },
mounted() {
$('#wallet button').focus()
@@ -255,32 +249,21 @@
this.$refs.tabs.clickHandler('payments', () => { this.loadPayments = true })
},
methods: {
- loadMandate(refresh) {
+ loadMandate() {
const loader = '#mandate-form'
this.$root.stopLoading(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
- }
- }
+ axios.get('/api/v4/payments/mandate', { loader })
+ .then(response => {
+ this.mandate = response.data
- if (this.$root.authInfo.isLocked) {
- if (this.mandate.isValid) {
- this.$root.unlock()
- } else {
- clearTimeout(this.refreshRequest)
- this.refreshRequest = setTimeout(() => { this.loadMandate(true) }, 10 * 1000)
- }
+ if (this.mandate.minAmount) {
+ if (this.mandate.minAmount > this.mandate.amount) {
+ this.mandate.amount = this.mandate.minAmount
}
- })
- }
+ }
+ })
},
selectPaymentMethod(method) {
this.formLock = false
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -53,6 +53,7 @@
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']);
+ Route::post('signup/validate', [API\SignupController::class, 'signupValidate']);
Route::post('signup/verify', [API\SignupController::class, 'verify']);
Route::post('signup', [API\SignupController::class, 'signup']);
}
@@ -151,9 +152,11 @@
Route::post('payments/mandate', [API\V4\PaymentsController::class, 'mandateCreate']);
Route::put('payments/mandate', [API\V4\PaymentsController::class, 'mandateUpdate']);
Route::delete('payments/mandate', [API\V4\PaymentsController::class, 'mandateDelete']);
+ Route::post('payments/mandate/reset', [API\V4\PaymentsController::class, 'mandateReset']);
Route::get('payments/methods', [API\V4\PaymentsController::class, 'paymentMethods']);
Route::get('payments/pending', [API\V4\PaymentsController::class, 'payments']);
Route::get('payments/has-pending', [API\V4\PaymentsController::class, 'hasPayments']);
+ Route::get('payments/status', [API\V4\PaymentsController::class, 'paymentStatus']);
Route::post('support/request', [API\V4\SupportController::class, 'request'])
->withoutMiddleware(['auth:api', 'scope:api'])
diff --git a/src/tests/Browser/Pages/Signup.php b/src/tests/Browser/Pages/PaymentStatus.php
copy from src/tests/Browser/Pages/Signup.php
copy to src/tests/Browser/Pages/PaymentStatus.php
--- a/src/tests/Browser/Pages/Signup.php
+++ b/src/tests/Browser/Pages/PaymentStatus.php
@@ -4,7 +4,7 @@
use Laravel\Dusk\Page;
-class Signup extends Page
+class PaymentStatus extends Page
{
/**
* Get the URL for the page.
@@ -13,7 +13,7 @@
*/
public function url(): string
{
- return '/signup';
+ return '/payment/status';
}
/**
@@ -25,12 +25,8 @@
*/
public function assert($browser)
{
- $browser->assertPathIs('/signup')
- ->waitUntilMissing('.app-loader')
- ->assertPresent('@step0')
- ->assertPresent('@step1')
- ->assertPresent('@step2')
- ->assertPresent('@step3');
+ $browser->waitForLocation($this->url())
+ ->waitUntilMissing('@app .app-loader');
}
/**
@@ -42,10 +38,9 @@
{
return [
'@app' => '#app',
- '@step0' => '#step0',
- '@step1' => '#step1',
- '@step2' => '#step2',
- '@step3' => '#step3',
+ '@content' => '.card .card-text',
+ '@lock-alert' => '#lock-alert',
+ '@button' => '.card button.btn-primary',
];
}
}
diff --git a/src/tests/Browser/Pages/Signup.php b/src/tests/Browser/Pages/Signup.php
--- a/src/tests/Browser/Pages/Signup.php
+++ b/src/tests/Browser/Pages/Signup.php
@@ -46,6 +46,7 @@
'@step1' => '#step1',
'@step2' => '#step2',
'@step3' => '#step3',
+ '@step4' => '#step4',
];
}
}
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
@@ -13,8 +13,9 @@
use Tests\Browser\Components\Toast;
use Tests\Browser\Pages\Dashboard;
use Tests\Browser\Pages\Home;
+use Tests\Browser\Pages\PaymentMollie;
+use Tests\Browser\Pages\PaymentStatus;
use Tests\Browser\Pages\Signup;
-use Tests\Browser\Pages\Wallet;
use Tests\TestCaseDusk;
use Illuminate\Foundation\Testing\DatabaseMigrations;
@@ -31,7 +32,7 @@
$this->deleteTestUser('admin@user-domain-signup.com');
$this->deleteTestDomain('user-domain-signup.com');
- Plan::whereIn('mode', ['token', 'mandate'])->update(['mode' => 'email']);
+ Plan::whereNot('mode', Plan::MODE_EMAIL)->update(['mode' => Plan::MODE_EMAIL]);
}
/**
@@ -44,7 +45,7 @@
$this->deleteTestDomain('user-domain-signup.com');
SignupInvitation::truncate();
- Plan::whereIn('mode', ['token', 'mandate'])->update(['mode' => 'email']);
+ Plan::whereNot('mode', Plan::MODE_EMAIL)->update(['mode' => Plan::MODE_EMAIL]);
@unlink(storage_path('signup-tokens.txt'));
@@ -521,17 +522,22 @@
}
/**
- * Test signup with a mandate plan, also the wallet lock
+ * Test signup with a mandate plan, also the UI lock
+ *
+ * @group mollie
*/
public function testSignupMandate(): void
{
// Test the individual plan
$plan = Plan::withEnvTenantContext()->where('title', 'individual')->first();
- $plan->mode = 'mandate';
+ $plan->mode = Plan::MODE_MANDATE;
$plan->save();
$this->browse(function (Browser $browser) {
+ $config = ['paymentProvider' => 'mollie'];
$browser->visit(new Signup())
+ // Force Mollie
+ ->execScript(sprintf('Object.assign(window.config, %s)', \json_encode($config)))
->waitFor('@step0 .plan-individual button')
->click('@step0 .plan-individual button')
// Test Back button
@@ -555,30 +561,57 @@
->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');
- });
+ ->whenAvailable('@step4', function ($browser) {
+ $browser->assertSeeIn('.card-text', 'The account is about to be created!')
+ ->assertSeeIn('.card-text', 'You signed up for an account')
+ ->assertSeeIn('button.btn-primary', 'Continue')
+ ->assertSeeIn('button.btn-secondary', 'Back')
+ ->click('button.btn-secondary');
+ })
+ ->whenAvailable('@step3', function ($browser) {
+ $browser->assertValue('#signup_login', 'signuptestdusk')
+ ->click('[type=submit]');
+ })
+ ->whenAvailable('@step4', function ($browser) {
+ $browser->click('button.btn-primary');
+ })
+ ->on(new PaymentMollie())
+ ->assertSeeIn('@title', 'Auto-Payment Setup')
+ ->assertMissing('@amount')
+ ->submitPayment('open')
+ ->on(new PaymentStatus())
+ ->assertSeeIn('@lock-alert', 'The account is locked')
+ ->assertSeeIn('@content', 'Checking the status...')
+ ->assertSeeIn('@button', 'Try again');
});
$user = User::where('email', 'signuptestdusk@' . \config('app.domain'))->first();
$this->assertSame($plan->id, $user->getSetting('plan_id'));
+ $this->assertFalse($user->isActive());
- // Login again and see that the account is still locked
+ // Refresh 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")
+ $browser->visit('/dashboard')
+ ->on(new PaymentStatus())
+ ->assertSeeIn('@lock-alert', 'The account is locked')
+ ->assertSeeIn('@content', 'Checking the status...');
+
+ // Mark the payment paid, and activate the user in background,
+ // expect unlock and redirect to the dashboard
+ // TODO: Move this to a separate tests file for PaymentStatus page
+ $payment = $user->wallets()->first()->payments()->first();
+ $payment->credit('Test');
+ $payment->status = \App\Payment::STATUS_PAID;
+ $payment->save();
+ $this->assertTrue($user->fresh()->isActive());
+
+ $browser->waitForLocation('/dashboard', 10)
->within(new Menu(), function ($browser) {
$browser->clickMenuItem('logout');
});
-
- // TODO: Test automatic UI unlock after creating a valid auto-payment mandate
});
+
+ // TODO: Test the 'Try again' button on /payment/status page
}
/**
@@ -587,7 +620,7 @@
public function testSignupToken(): void
{
// Test the individual plan
- Plan::where('title', 'individual')->update(['mode' => 'token']);
+ Plan::where('title', 'individual')->update(['mode' => Plan::MODE_TOKEN]);
// Register some valid tokens
$tokens = ['1234567890', 'abcdefghijk'];
@@ -640,7 +673,7 @@
$this->assertSame(null, $user->getSetting('external_email'));
// Test the group plan
- Plan::where('title', 'group')->update(['mode' => 'token']);
+ Plan::where('title', 'group')->update(['mode' => Plan::MODE_TOKEN]);
$this->browse(function (Browser $browser) use ($tokens) {
$browser->visit(new Signup())
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
@@ -803,7 +803,7 @@
'free_months' => 1,
'discount_qty' => 0,
'discount_rate' => 0,
- 'mode' => 'mandate',
+ 'mode' => Plan::MODE_MANDATE,
]);
$packages = [
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
@@ -47,8 +47,7 @@
$wallet->settings()->whereIn('key', ['mollie_id', 'stripe_id'])->delete();
$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->status |= User::STATUS_IMAP_READY | User::STATUS_LDAP_READY | User::STATUS_ACTIVE;
$user->save();
Plan::withEnvTenantContext()->where('title', 'individual')->update(['mode' => 'email']);
$user->setSettings(['plan_id' => null]);
@@ -81,8 +80,7 @@
$wallet->settings()->whereIn('key', ['mollie_id', 'stripe_id'])->delete();
$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->status |= User::STATUS_IMAP_READY | User::STATUS_LDAP_READY | User::STATUS_ACTIVE;
$user->save();
Plan::withEnvTenantContext()->where('title', 'individual')->update(['mode' => 'email']);
$user->setSettings(['plan_id' => null]);
@@ -1411,7 +1409,7 @@
// Ned is John's wallet controller
$plan = Plan::withEnvTenantContext()->where('title', 'individual')->first();
- $plan->mode = 'mandate';
+ $plan->mode = Plan::MODE_MANDATE;
$plan->save();
$wallet->owner->setSettings(['plan_id' => $plan->id]);
$ned = $this->getTestUser('ned@kolab.org');
@@ -1472,7 +1470,8 @@
$this->assertFalse($result['isLocked']);
// Test locked user
- $john->restrict();
+ $john->status &= ~User::STATUS_ACTIVE;
+ $john->save();
$result = $this->invokeMethod(new UsersController(), 'userResponse', [$john]);
$this->assertTrue($result['isLocked']);
diff --git a/src/tests/Unit/UtilsTest.php b/src/tests/Unit/UtilsTest.php
--- a/src/tests/Unit/UtilsTest.php
+++ b/src/tests/Unit/UtilsTest.php
@@ -58,6 +58,18 @@
$this->assertSame('shared+shared/Test@test.tld', Utils::emailToLower('shared+shared/Test@Test.Tld'));
}
+ /**
+ * Test for Utils::money()
+ */
+ public function testMoney(): void
+ {
+ $this->assertSame('-0,01 CHF', Utils::money(-1, 'CHF'));
+ $this->assertSame('0,00 CHF', Utils::money(0, 'CHF'));
+ $this->assertSame('1,11 €', Utils::money(111, 'EUR'));
+ $this->assertSame('1,00 CHF', Utils::money(100, 'CHF'));
+ $this->assertSame('€0.00', Utils::money(0, 'EUR', 'en_US'));
+ }
+
/**
* Test for Utils::normalizeAddress()
*/

File Metadata

Mime Type
text/plain
Expires
Sun, Apr 5, 5:51 AM (19 h, 3 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18832555
Default Alt Text
D4202.1775368305.diff (49 KB)

Event Timeline