diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 443f8601157..5d867f98961 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -85,7 +85,7 @@ jobs: mkdir -p public/js public/css results/coverage {\ echo "{"; \ - for f in app.js manifest.js vendor.js app-ltr.css app-rtl.css stripe.js stripe.css; do \ + for f in app.js manifest.js vendor.js app-ltr.css app-rtl.css; do \ [[ $first == 1 ]] && echo -n "," || first=1; \ k=${f##*.}/$f; \ echo "\"/$k\": \"/$k\""; \ diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 0a13d4ab226..9b8229a975c 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -3,6 +3,7 @@ namespace App\Console; use App\Console\Scheduling\CronEvent; +use App\Jobs\Settings\CheckLicenceKeys; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; @@ -54,6 +55,18 @@ protected function schedule(Schedule $schedule) $this->scheduleCommand($schedule, 'cloudflare:reload', 'daily'); } $this->scheduleCommand($schedule, 'model:prune', 'daily'); + + $this->scheduleJob($schedule, CheckLicenceKeys::class, 'minutes', 60 * 6); + } + + /** + * Define a new schedule command with a frequency. + * + * @codeCoverageIgnore + */ + private function scheduleCommand(Schedule $schedule, string $command, string $frequency, mixed ...$params) + { + $this->scheduleAction($schedule, $command, $frequency, $params); } /** @@ -61,12 +74,22 @@ protected function schedule(Schedule $schedule) * * @codeCoverageIgnore */ - private function scheduleCommand(Schedule $schedule, string $command, $frequency) + private function scheduleJob(Schedule $schedule, string $job, string $frequency, mixed ...$params) + { + $this->scheduleAction($schedule, $job, $frequency, $params, 'job'); + } + + /** + * Define a new schedule. + * + * @codeCoverageIgnore + */ + private function scheduleAction(Schedule $schedule, string $command, string $frequency, array $params, string $action = 'command') { - $schedule->command($command)->when(function () use ($command, $frequency) { + $schedule->$action($command)->when(function () use ($command, $frequency, $params) { $event = CronEvent::command($command); if ($frequency) { - $event = $event->$frequency(); + $event = $event->{$frequency}(...$params); } return $event->isDue(); diff --git a/app/Console/Scheduling/CronEvent.php b/app/Console/Scheduling/CronEvent.php index 3245c7705ce..0fca6639113 100644 --- a/app/Console/Scheduling/CronEvent.php +++ b/app/Console/Scheduling/CronEvent.php @@ -96,6 +96,20 @@ public function weekly(): self return $this; } + /** + * Run the command every $minutes. + * + * @param int $minutes + * @return self + */ + public function minutes(int $minutes): self + { + $this->minutes = $minutes; + $this->days = 0; + + return $this; + } + /** * Test if the command is due to run. * diff --git a/app/Exceptions/CustomerPortalWrongCredentials.php b/app/Exceptions/CustomerPortalWrongCredentials.php new file mode 100644 index 00000000000..89a189444d0 --- /dev/null +++ b/app/Exceptions/CustomerPortalWrongCredentials.php @@ -0,0 +1,19 @@ +translatedFormat($format) ?: ''; } - /** - * Gets the next theoritical billing date. - * This is used on the Upgrade page to tell the user when the next billing - * date would be if he subscribed. - * - * @param string $interval - * @return Carbon - */ - public static function getNextTheoriticalBillingDate(string $interval): Carbon - { - if ($interval == 'monthly') { - return now(static::getTimezone())->addMonth(); - } - - return now(static::getTimezone())->addYear(); - } - /** * Gets a list of all the year from min to max (0 is the current year). * diff --git a/app/Helpers/InstanceHelper.php b/app/Helpers/InstanceHelper.php index 7c148702ca3..fc2ccca3f94 100644 --- a/app/Helpers/InstanceHelper.php +++ b/app/Helpers/InstanceHelper.php @@ -2,26 +2,13 @@ namespace App\Helpers; -use Carbon\Carbon; use function Safe\json_decode; -use App\Models\Account\Account; -use App\Models\Instance\Instance; use App\Models\Settings\Currency; use Illuminate\Support\Facades\DB; use function Safe\file_get_contents; class InstanceHelper { - /** - * Get the number of paid accounts in the instance. - * - * @return int - */ - public static function getNumberOfPaidSubscribers() - { - return Account::where('stripe_id', '!=', null)->count(); - } - /** * Get the plan information for the given time period. * @@ -48,46 +35,6 @@ public static function getPlanInformationFromConfig(string $timePeriod): ?array ]; } - /** - * Get the plan information for the given time period. - * - * @param \Laravel\Cashier\Subscription $subscription - * @return array|null - */ - public static function getPlanInformationFromSubscription(\Laravel\Cashier\Subscription $subscription): ?array - { - try { - $stripeSubscription = $subscription->asStripeSubscription(); - $plan = $stripeSubscription->plan; - } catch (\Stripe\Exception\ApiErrorException $e) { - $stripeSubscription = null; - $plan = null; - } - - if (is_null($stripeSubscription) || is_null($plan)) { - return [ - 'type' => $subscription->stripe_price, - 'name' => $subscription->name, - 'id' => $subscription->stripe_id, - 'price' => '?', - 'friendlyPrice' => '?', - 'nextBillingDate' => '', - ]; - } - - $currency = Currency::where('iso', strtoupper($plan->currency))->first(); - $amount = MoneyHelper::format($plan->amount, $currency); - - return [ - 'type' => $plan->interval === 'month' ? 'monthly' : 'annual', - 'name' => $subscription->name, - 'id' => $plan->id, - 'price' => $plan->amount, - 'friendlyPrice' => $amount, - 'nextBillingDate' => DateHelper::getFullDate(Carbon::createFromTimestamp($stripeSubscription->current_period_end)), - ]; - } - /** * Get changelogs entries. * diff --git a/app/Http/Controllers/Settings/SubscriptionsController.php b/app/Http/Controllers/Settings/SubscriptionsController.php index d87816fcf09..b3413f9142f 100644 --- a/app/Http/Controllers/Settings/SubscriptionsController.php +++ b/app/Http/Controllers/Settings/SubscriptionsController.php @@ -2,32 +2,16 @@ namespace App\Http\Controllers\Settings; -use Illuminate\View\View; -use App\Traits\StripeCall; +use Exception; use App\Helpers\DateHelper; use Illuminate\Http\Request; -use Laravel\Cashier\Cashier; -use Laravel\Cashier\Payment; use App\Helpers\AccountHelper; -use App\Helpers\InstanceHelper; -use App\Exceptions\StripeException; -use Illuminate\Support\Facades\App; use App\Http\Controllers\Controller; -use Illuminate\Http\RedirectResponse; -use Illuminate\Contracts\View\Factory; -use Stripe\Exception\ApiErrorException; -use Laravel\Cashier\Exceptions\IncompletePayment; -use App\Services\Account\Settings\ArchiveAllContacts; +use Illuminate\Validation\ValidationException; +use App\Services\Account\Subscription\ActivateLicenceKey; class SubscriptionsController extends Controller { - use StripeCall; - - /** - * Display a listing of the resource. - * - * @return View|Factory|RedirectResponse - */ public function index() { if (! config('monica.requires_subscription')) { @@ -36,332 +20,44 @@ public function index() $account = auth()->user()->account; - $subscription = $account->getSubscribedPlan(); - if (! $account->isSubscribed() && (! $subscription || $subscription->ended())) { - return view('settings.subscriptions.blank', [ - 'numberOfCustomers' => InstanceHelper::getNumberOfPaidSubscribers(), + if ($account->is_on_stripe) { + return view('settings.subscriptions.stripe', [ + 'customerPortalUrl' => config('monica.customer_portal_stripe_url'), + 'accountHasLimitations' => AccountHelper::hasLimitations($account), ]); } - $hasInvoices = $account->hasStripeId() && $account->hasInvoices(); - $invoices = null; - if ($hasInvoices) { - $invoices = $account->invoices(); - } - - try { - $planInformation = $this->stripeCall(function () use ($subscription): ?array { - return InstanceHelper::getPlanInformationFromSubscription($subscription); - }); - } catch (StripeException $e) { - $planInformation = null; - } - - return view('settings.subscriptions.account', [ - 'planInformation' => $planInformation, - 'subscription' => $subscription, - 'hasInvoices' => $hasInvoices, - 'invoices' => $invoices, - 'accountHasLimitations' => AccountHelper::hasLimitations($account), - ]); - } - - /** - * Display the upgrade view page. - * - * @param Request $request - * @return View|Factory|RedirectResponse - */ - public function upgrade(Request $request) - { - if (! config('monica.requires_subscription')) { - return redirect()->route('settings.index'); - } - - if (auth()->user()->account->isSubscribed()) { - return redirect()->route('settings.subscriptions.index'); - } - - $plan = $request->query('plan'); - if ($plan !== 'monthly' && $plan !== 'annual') { - abort(404); - } - - $planInformation = InstanceHelper::getPlanInformationFromConfig($plan); - - if ($planInformation === null) { - abort(404); - } - - return view('settings.subscriptions.upgrade', [ - 'planInformation' => $planInformation, - 'nextTheoriticalBillingDate' => DateHelper::getFullDate(DateHelper::getNextTheoriticalBillingDate($plan)), - 'intent' => auth()->user()->account->createSetupIntent(), - ]); - } - - /** - * Display the update view page. - * - * @param Request $request - * @return View|Factory|RedirectResponse - */ - public function update(Request $request) - { - if (! config('monica.requires_subscription')) { - return redirect()->route('settings.index'); - } - - $account = auth()->user()->account; - - $subscription = $account->getSubscribedPlan(); - if (! $account->isSubscribed() && (! $subscription || $subscription->ended())) { + if (! $account->isSubscribed()) { return view('settings.subscriptions.blank', [ - 'numberOfCustomers' => InstanceHelper::getNumberOfPaidSubscribers(), + 'customerPortalUrl' => config('monica.customer_portal_url'), ]); } - $planInformation = InstanceHelper::getPlanInformationFromSubscription($subscription); - - if ($planInformation === null) { - abort(404); - } - - $plans = collect(); - foreach (['monthly', 'annual'] as $plan) { - $plans->push(InstanceHelper::getPlanInformationFromConfig($plan)); - } - - $legacyPlan = null; - if (! $plans->contains(function ($value) use ($planInformation) { - return $value['id'] === $planInformation['id']; - })) { - $legacyPlan = $planInformation; - } - - return view('settings.subscriptions.update', [ - 'planInformation' => $planInformation, - 'plans' => $plans, - 'legacyPlan' => $legacyPlan, - ]); - } - - /** - * Process the update process. - * - * @param Request $request - * @return View|Factory|RedirectResponse - */ - public function processUpdate(Request $request) - { - $account = auth()->user()->account; - - $subscription = $account->getSubscribedPlan(); - if (! $account->isSubscribed() && ! $subscription) { - return redirect()->route('settings.index'); - } - - try { - $account->updateSubscription($request->input('frequency'), $subscription); - } catch (StripeException $e) { - return back() - ->withInput() - ->withErrors($e->getMessage()); - } - - return redirect()->route('settings.subscriptions.index'); - } - - /** - * Display the confirm view page. - * - * @return View|Factory|RedirectResponse - * - * @throws ApiErrorException - */ - public function confirmPayment($id) - { - try { - $payment = $this->stripeCall(function () use ($id): \Stripe\PaymentIntent { - return Cashier::stripe()->paymentIntents->retrieve($id); - }); - } catch (StripeException $e) { - return back()->withErrors($e->getMessage()); - } - - return view('settings.subscriptions.confirm', [ - 'payment' => new Payment($payment), - 'redirect' => request('redirect'), - ]); - } - - /** - * Display the upgrade success page. - * - * @return View|Factory|RedirectResponse - */ - public function upgradeSuccess() - { - if (! config('monica.requires_subscription')) { - return redirect()->route('settings.index'); - } - - return view('settings.subscriptions.success'); - } - - /** - * Display the downgrade success page. - * - * @param Request $request - * @return View|Factory|RedirectResponse - */ - public function downgradeSuccess(Request $request) - { - if (! config('monica.requires_subscription')) { - return redirect()->route('settings.index'); - } - - return view('settings.subscriptions.downgrade-success'); - } - - /** - * Display the archive all your contacts page. - * - * @return View|Factory|RedirectResponse - */ - public function archive() - { - return view('settings.subscriptions.archive'); - } - - /** - * Process the Archive process. - * - * @return RedirectResponse - */ - public function processArchive() - { - app(ArchiveAllContacts::class)->execute([ - 'account_id' => auth()->user()->account_id, + return view('settings.subscriptions.account', [ + 'planInformation' => trans('settings.subscriptions_licence_key_frequency_'.$account->frequency), + 'nextBillingDate' => DateHelper::getFullDate($account->valid_until_at), + 'accountHasLimitations' => AccountHelper::hasLimitations($account), + 'customerPortalUrl' => config('monica.customer_portal_url'), ]); - - return redirect()->route('settings.subscriptions.downgrade'); - } - - /** - * Display the downgrade view page. - * - * @return View|Factory|RedirectResponse - */ - public function downgrade() - { - $account = auth()->user()->account; - - if (! config('monica.requires_subscription')) { - return redirect()->route('settings.index'); - } - - $subscription = $account->getSubscribedPlan(); - if (! $account->isSubscribed() && ! $subscription) { - return redirect()->route('settings.index'); - } - - return view('settings.subscriptions.downgrade-checklist') - ->with('numberOfActiveContacts', $account->allContacts()->active()->count()) - ->with('numberOfPendingInvitations', $account->invitations()->count()) - ->with('numberOfUsers', $account->users()->count()) - ->with('accountHasLimitations', AccountHelper::hasLimitations($account)) - ->with('hasReachedContactLimit', ! AccountHelper::isBelowContactLimit($account)) - ->with('canDowngrade', AccountHelper::canDowngrade($account)); } - /** - * Process the downgrade process. - * - * @return RedirectResponse - */ - public function processDowngrade() + public function store(Request $request) { - $account = auth()->user()->account; - - if (! AccountHelper::canDowngrade($account)) { - return redirect()->route('settings.subscriptions.downgrade'); - } - - $subscription = $account->getSubscribedPlan(); - if (! $account->isSubscribed() && ! $subscription) { - return redirect()->route('settings.index'); - } - try { - $account->subscriptionCancel(); - } catch (StripeException $e) { + ActivateLicenceKey::dispatchSync([ + 'account_id' => auth()->user()->account_id, + 'licence_key' => $request->input('licence_key'), + ]); + } catch (ValidationException $e) { return back() ->withInput() - ->withErrors($e->getMessage()); - } - - return redirect()->route('settings.subscriptions.downgrade.success'); - } - - /** - * Process the upgrade payment. - * - * @param Request $request - * @return RedirectResponse - */ - public function processPayment(Request $request) - { - if (! config('monica.requires_subscription')) { - return redirect()->route('settings.index'); - } - - try { - auth()->user()->account - ->subscribe($request->input('payment_method'), $request->input('plan')); - } catch (IncompletePayment $e) { - return redirect()->route( - 'settings.subscriptions.confirm', - [$e->payment->asStripePaymentIntent()->id, 'redirect' => route('settings.subscriptions.upgrade.success')] - ); - } catch (StripeException $e) { + ->withErrors($e->validator); + } catch (Exception $e) { return back() ->withInput() ->withErrors($e->getMessage()); } - return redirect()->route('settings.subscriptions.upgrade.success'); - } - - /** - * Download the invoice as PDF. - * - * @param mixed $invoiceId - * @return \Symfony\Component\HttpFoundation\Response - */ - public function downloadInvoice($invoiceId) - { - return auth()->user()->account->downloadInvoice($invoiceId, [ - 'vendor' => 'Monica', - 'product' => trans('settings.subscriptions_pdf_title', ['name' => config('monica.paid_plan_monthly_friendly_name')]), - ]); - } - - /** - * Download the invoice as PDF. - * - * @param Request $request - * @return \Illuminate\Http\RedirectResponse|null - */ - public function forceCompletePaymentOnTesting(Request $request): ?RedirectResponse - { - if (App::environment('production')) { - return null; - } - $subscription = auth()->user()->account->getSubscribedPlan(); - $subscription->stripe_status = 'active'; - $subscription->save(); - - return redirect()->route('settings.subscriptions.index'); + return view('settings.subscriptions.success'); } } diff --git a/app/Jobs/Settings/CheckLicenceKeys.php b/app/Jobs/Settings/CheckLicenceKeys.php new file mode 100644 index 00000000000..b6365228591 --- /dev/null +++ b/app/Jobs/Settings/CheckLicenceKeys.php @@ -0,0 +1,44 @@ +whereNotNull('licence_key') + ->get(); + + foreach ($accounts as $account) { + ActivateLicenceKey::dispatch([ + 'account_id' => $account->id, + 'licence_key' => $account->licence_key, + ]); + } + } +} diff --git a/app/Models/Account/Account.php b/app/Models/Account/Account.php index 5dc12a7780c..c58643dacb8 100644 --- a/app/Models/Account/Account.php +++ b/app/Models/Account/Account.php @@ -73,6 +73,10 @@ class Account extends Model 'api_key', 'default_time_reminder_is_sent', 'default_gender_id', + 'licence_key', + 'valid_until_at', + 'purchaser_email', + 'is_on_stripe', ]; /** @@ -82,6 +86,16 @@ class Account extends Model */ protected $casts = [ 'has_access_to_paid_version_for_free' => 'boolean', + 'is_on_stripe' => 'boolean', + ]; + + /** + * The attributes that should be mutated to dates. + * + * @var array + */ + protected $dates = [ + 'valid_until_at', ]; /** diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 76604afcd06..b56501894ea 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,7 +3,6 @@ namespace App\Providers; use App\Helpers\DBHelper; -use Laravel\Cashier\Cashier; use Laravel\Passport\Passport; use Illuminate\Console\Command; use Illuminate\Support\Facades\App; @@ -87,8 +86,6 @@ public function boot() Schema::defaultStringLength(191); } - Cashier::useCustomerModel(\App\Models\Account\Account::class); - VerifyEmail::toMailUsing(function ($user, $verificationUrl) { return EmailMessaging::verifyEmailMail($user, $verificationUrl); }); @@ -122,12 +119,6 @@ public function boot() public function register() { Passport::ignoreMigrations(); - Cashier::ignoreMigrations(); - Cashier::formatCurrencyUsing(function ($amount, $currency) { - $currency = \App\Models\Settings\Currency::where('iso', strtoupper($currency ?? config('cashier.currency')))->first(); - - return \App\Helpers\MoneyHelper::format($amount, $currency); - }); } /** diff --git a/app/Providers/EncryptionServiceProvider.php b/app/Providers/EncryptionServiceProvider.php new file mode 100755 index 00000000000..279f2e6feda --- /dev/null +++ b/app/Providers/EncryptionServiceProvider.php @@ -0,0 +1,58 @@ +app->singleton('license.encrypter', function ($app) { + $config = $app->make('config')->get('monica'); + + return new Encrypter($this->parseKey($config), $config['licence_cipher']); + }); + } + + /** + * Parse the encryption key. + * + * @param array $config + * @return string + */ + protected function parseKey(array $config) + { + if (Str::startsWith($key = $this->key($config), $prefix = 'base64:')) { + $key = base64_decode(Str::after($key, $prefix)); + } + + return $key; + } + + /** + * Extract the encryption key from the given configuration. + * + * @param array $config + * @return string + * + * @throws \App\Exceptions\MissingPrivateKeyException + */ + protected function key(array $config) + { + return tap($config['licence_private_key'], function ($key) { + if (empty($key)) { + throw new MissingPrivateKeyException(); + } + }); + } +} diff --git a/app/Services/Account/Settings/DestroyAccount.php b/app/Services/Account/Settings/DestroyAccount.php index 4627b580cd8..90fd1458ac1 100644 --- a/app/Services/Account/Settings/DestroyAccount.php +++ b/app/Services/Account/Settings/DestroyAccount.php @@ -38,8 +38,6 @@ public function execute(array $data): void $this->destroyPhotos($account); - $this->cancelStripe($account); - $account->delete(); } @@ -68,19 +66,4 @@ private function destroyPhotos(Account $account) 'account_id' => $account->id, ]); } - - /** - * Cancel Stripe subscription. - * - * @param Account $account - * @return void - * - * @throws StripeException - */ - private function cancelStripe(Account $account) - { - if ($account->isSubscribed() && ! $account->has_access_to_paid_version_for_free) { - $account->subscriptionCancel(); - } - } } diff --git a/app/Services/Account/Subscription/ActivateLicenceKey.php b/app/Services/Account/Subscription/ActivateLicenceKey.php new file mode 100644 index 00000000000..c4b7dacdf34 --- /dev/null +++ b/app/Services/Account/Subscription/ActivateLicenceKey.php @@ -0,0 +1,113 @@ + 'required|integer|exists:accounts,id', + 'licence_key' => 'required|string:4096', + ]; + } + + /** + * Check if the licence key given by the user is a valid licence key. + * If it is, activate the licence key and set the valid_until_at date. + * + * @param array $data + * @return void + */ + public function handle(array $data): void + { + $this->validate($data); + $this->data = $data; + $this->account = Account::findOrFail($data['account_id']); + + $this->validateEnvVariables(); + $this->makeRequestToCustomerPortal(); + $this->checkResponseCode(); + $this->store(); + } + + private function validateEnvVariables(): void + { + if (config('monica.licence_private_key') === null) { + throw new MissingPrivateKeyException(); + } + } + + private function makeRequestToCustomerPortal(): void + { + $data = app(CustomerPortalCall::class)->execute([ + 'licence_key' => $this->data['licence_key'], + ]); + $this->status = $data['status']; + $this->response = $data['data']; + } + + private function checkResponseCode(): void + { + if ($this->status === 404) { + throw new LicenceKeyDontExistException; + } + + if ($this->status === 410) { + throw new LicenceKeyInvalidException; + } + + if ($this->status !== 200) { + throw new LicenceKeyErrorException; + } + } + + private function store(): void + { + $licenceKey = $this->decodeKey(); + + $this->account->licence_key = $this->data['licence_key']; + $this->account->valid_until_at = $this->response['next_check_at']; + $this->account->purchaser_email = $licenceKey['purchaser_email']; + switch ($licenceKey['frequency']) { + case 'month': + $this->account->frequency = 'monthly'; + break; + case 'year': + $this->account->frequency = 'annual'; + break; + default: + $this->account->frequency = $licenceKey['frequency']; + break; + } + $this->account->save(); + } + + private function decodeKey(): array + { + $encrypter = app('license.encrypter'); + + return $encrypter->decrypt($this->data['licence_key']); + } +} diff --git a/app/Services/Account/Subscription/CustomerPortalCall.php b/app/Services/Account/Subscription/CustomerPortalCall.php new file mode 100644 index 00000000000..ec898f9eda4 --- /dev/null +++ b/app/Services/Account/Subscription/CustomerPortalCall.php @@ -0,0 +1,94 @@ + 'required|string:4096', + ]; + } + + /** + * Check if the licence key given by the user is a valid licence key. + * If it is, activate the licence key and set the valid_until_at date. + * + * @param array $data + * @return array + */ + public function execute(array $data): array + { + $this->validate($data); + $this->data = $data; + + $this->validateEnvVariables(); + + $accessToken = $this->getAccessToken(); + $response = $this->makeRequestToCustomerPortal($accessToken); + + return [ + 'status' => $response->status(), + 'data' => $response->json(), + ]; + } + + private function validateEnvVariables(): void + { + if (config('monica.customer_portal_url') === '') { + throw new NoCustomerPortalSetException; + } + + if (config('monica.customer_portal_client_id') === null || config('monica.customer_portal_client_secret') === null) { + throw new NoCustomerPortalSecretsException; + } + } + + private function getAccessToken(): string + { + return Cache::remember('customer_portal.access_token', 31449600 /* 364 days */, function () { + $url = config('monica.customer_portal_url').'/oauth/token'; + + $response = Http::asForm()->post($url, [ + 'grant_type' => 'client_credentials', + 'client_id' => config('monica.customer_portal_client_id'), + 'client_secret' => config('monica.customer_portal_client_secret'), + 'scope' => 'manage-key', + ]); + + $json = $response->json(); + if ($response->failed() || ! isset($json['access_token'])) { + throw new CustomerPortalWrongCredentials(); + } + + return $json['access_token']; + }); + } + + private function makeRequestToCustomerPortal(string $accessToken): Response + { + $url = config('monica.customer_portal_url').'/api/validate'; + + return Http::withToken($accessToken) + ->acceptJson() + ->post($url, [ + 'licence_key' => $this->data['licence_key'], + ]); + } +} diff --git a/app/Traits/StripeCall.php b/app/Traits/StripeCall.php deleted file mode 100644 index 32ec47273fb..00000000000 --- a/app/Traits/StripeCall.php +++ /dev/null @@ -1,57 +0,0 @@ -getJsonBody(); - $err = $body['error']; - $errorMessage = trans('settings.stripe_error_card', ['message' => $err['message']]); - Log::error(__CLASS__.' '.__FUNCTION__.': Stripe card decline error: '.$e->getMessage(), ['body' => $e->getJsonBody(), $e]); - } catch (\Stripe\Exception\RateLimitException $e) { - // Too many requests made to the API too quickly - $errorMessage = trans('settings.stripe_error_rate_limit'); - Log::error(__CLASS__.' '.__FUNCTION__.': Stripe RateLimit error: '.$e->getMessage(), ['body' => $e->getJsonBody(), $e]); - } catch (\Stripe\Exception\InvalidRequestException $e) { - // Invalid parameters were supplied to Stripe's API - $errorMessage = trans('settings.stripe_error_invalid_request'); - Log::error(__CLASS__.' '.__FUNCTION__.': Stripe InvalidRequest error: '.$e->getMessage(), ['body' => $e->getJsonBody(), $e]); - } catch (\Stripe\Exception\AuthenticationException $e) { - // Authentication with Stripe's API failed - // (maybe you changed API keys recently) - $errorMessage = trans('settings.stripe_error_authentication'); - Log::error(__CLASS__.' '.__FUNCTION__.': Stripe Authentication error: '.$e->getMessage(), ['body' => $e->getJsonBody(), $e]); - } catch (\Stripe\Exception\ApiConnectionException $e) { - // Network communication with Stripe failed - $errorMessage = trans('settings.stripe_error_api_connection_error'); - Log::error(__CLASS__.' '.__FUNCTION__.': Stripe ApiConnection error: '.$e->getMessage(), ['body' => $e->getJsonBody(), $e]); - } catch (\Stripe\Exception\ApiErrorException $e) { - $errorMessage = $e->getMessage(); - Log::error(__CLASS__.' '.__FUNCTION__.': Stripe error: '.$e->getMessage(), ['body' => $e->getJsonBody(), $e]); - } catch (\Laravel\Cashier\Exceptions\IncompletePayment $e) { - throw $e; - } catch (\Exception $e) { - $errorMessage = $e->getMessage(); - Log::error(__CLASS__.' '.__FUNCTION__.': Stripe error: '.$e->getMessage(), [$e]); - } - - throw new StripeException($errorMessage); - } -} diff --git a/app/Traits/Subscription.php b/app/Traits/Subscription.php index 3d0d2b1aed3..f2a202bc0d0 100644 --- a/app/Traits/Subscription.php +++ b/app/Traits/Subscription.php @@ -2,70 +2,8 @@ namespace App\Traits; -use Laravel\Cashier\Billable; -use App\Helpers\InstanceHelper; - trait Subscription { - use Billable, StripeCall; - - /** - * Process the upgrade payment. - * - * @param string $payment_method - * @param string $planName - * @return bool|string - */ - public function subscribe(string $payment_method, string $planName) - { - $plan = InstanceHelper::getPlanInformationFromConfig($planName); - if ($plan === null) { - abort(404); - } - - return $this->stripeCall(function () use ($payment_method, $plan) { - $this->newSubscription($plan['name'], $plan['id']) - ->create($payment_method, [ - 'email' => auth()->user()->email, - ]); - - return true; - }); - } - - /** - * Update an existing subscription. - * - * @param string $planName - * @param \Laravel\Cashier\Subscription $subscription - * @return \Laravel\Cashier\Subscription - */ - public function updateSubscription(string $planName, \Laravel\Cashier\Subscription $subscription) - { - $oldPlan = $subscription->stripe_price; - $plan = InstanceHelper::getPlanInformationFromConfig($planName); - if ($plan === null) { - abort(404); - } - - if ($oldPlan === $planName) { - // No change - return $subscription; - } - - $subscription = $this->stripeCall(function () use ($subscription, $plan) { - return $subscription->swap($plan['id']); - }); - - if ($subscription->stripe_price !== $oldPlan && $subscription->stripe_price === $plan['id']) { - $subscription->forceFill([ - 'name' => $plan['name'], - ])->save(); - } - - return $subscription; - } - /** * Check if the account is currently subscribed to a plan. * @@ -77,70 +15,14 @@ public function isSubscribed() return true; } - return $this->getSubscribedPlan() !== null; - } - - /** - * Get the subscription the account is subscribed to. - * - * @return \Laravel\Cashier\Subscription|null - */ - public function getSubscribedPlan() - { - return $this->subscriptions()->recurring()->first(); - } - - /** - * Get the id of the plan the account is subscribed to. - * - * @return string - */ - public function getSubscribedPlanId() - { - $plan = $this->getSubscribedPlan(); - - return is_null($plan) ? '' : $plan->stripe_price; - } - - /** - * Get the friendly name of the plan the account is subscribed to. - * - * @return string|null - */ - public function getSubscribedPlanName(): ?string - { - $plan = $this->getSubscribedPlan(); - - return is_null($plan) ? null : $plan->name; - } - - /** - * Cancel the plan the account is subscribed to. - * - * @return bool|string - */ - public function subscriptionCancel() - { - $plan = $this->getSubscribedPlan(); - - if (! is_null($plan)) { - return $this->stripeCall(function () use ($plan) { - $plan->cancelNow(); - - return true; - }); + if (! $this->licence_key) { + return false; } - return false; - } + if ($this->valid_until_at->isPast()) { + return false; + } - /** - * Check if the account has invoices linked to this account. - * - * @return bool - */ - public function hasInvoices() - { - return $this->subscriptions()->count() > 0; + return true; } } diff --git a/composer.json b/composer.json index b7ae5191f04..234527a0dad 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,6 @@ "guzzlehttp/guzzle": "^7.2", "guzzlehttp/psr7": "^2.1", "intervention/image": "^2.3", - "laravel/cashier": "^13.0", "laravel/framework": "^9.0", "laravel/passport": "^11.0", "laravel/socialite": "^5.0", diff --git a/composer.lock b/composer.lock index 4efa98095c0..19f81f54d67 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3ebc28c0ab93392b49b68eaf2efbfa77", + "content-hash": "526181035cae5f96471f10b9c949c040", "packages": [ { "name": "asbiin/laravel-adorable", @@ -1319,68 +1319,6 @@ ], "time": "2022-02-28T11:07:21+00:00" }, - { - "name": "dompdf/dompdf", - "version": "v2.0.1", - "source": { - "type": "git", - "url": "https://github.com/dompdf/dompdf.git", - "reference": "c5310df0e22c758c85ea5288175fc6cd777bc085" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/dompdf/dompdf/zipball/c5310df0e22c758c85ea5288175fc6cd777bc085", - "reference": "c5310df0e22c758c85ea5288175fc6cd777bc085", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-mbstring": "*", - "masterminds/html5": "^2.0", - "phenx/php-font-lib": ">=0.5.4 <1.0.0", - "phenx/php-svg-lib": ">=0.3.3 <1.0.0", - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "ext-json": "*", - "ext-zip": "*", - "mockery/mockery": "^1.3", - "phpunit/phpunit": "^7.5 || ^8 || ^9", - "squizlabs/php_codesniffer": "^3.5" - }, - "suggest": { - "ext-gd": "Needed to process images", - "ext-gmagick": "Improves image processing performance", - "ext-imagick": "Improves image processing performance", - "ext-zlib": "Needed for pdf stream compression" - }, - "type": "library", - "autoload": { - "psr-4": { - "Dompdf\\": "src/" - }, - "classmap": [ - "lib/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL-2.1" - ], - "authors": [ - { - "name": "The Dompdf Community", - "homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md" - } - ], - "description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter", - "homepage": "https://github.com/dompdf/dompdf", - "support": { - "issues": "https://github.com/dompdf/dompdf/issues", - "source": "https://github.com/dompdf/dompdf/tree/v2.0.1" - }, - "time": "2022-09-22T13:43:41+00:00" - }, { "name": "dragonmantank/cron-expression", "version": "v3.3.2", @@ -2683,90 +2621,6 @@ }, "time": "2021-10-08T21:21:46+00:00" }, - { - "name": "laravel/cashier", - "version": "v13.16.0", - "source": { - "type": "git", - "url": "https://github.com/laravel/cashier-stripe.git", - "reference": "6d812bb63fd3f85bab37a3249bf39ce4e6e40d20" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laravel/cashier-stripe/zipball/6d812bb63fd3f85bab37a3249bf39ce4e6e40d20", - "reference": "6d812bb63fd3f85bab37a3249bf39ce4e6e40d20", - "shasum": "" - }, - "require": { - "dompdf/dompdf": "^1.2.1|^2.0", - "ext-json": "*", - "illuminate/console": "^8.37|^9.0", - "illuminate/contracts": "^8.37|^9.0", - "illuminate/database": "^8.37|^9.0", - "illuminate/http": "^8.37|^9.0", - "illuminate/log": "^8.37|^9.0", - "illuminate/notifications": "^8.37|^9.0", - "illuminate/routing": "^8.37|^9.0", - "illuminate/support": "^8.37|^9.0", - "illuminate/view": "^8.37|^9.0", - "moneyphp/money": "^3.2|^4.0", - "nesbot/carbon": "^2.0", - "php": "^7.3|^8.0", - "stripe/stripe-php": "^7.39|^8.0|^9.0", - "symfony/http-kernel": "^5.0|^6.0", - "symfony/polyfill-intl-icu": "^1.22.1" - }, - "require-dev": { - "mockery/mockery": "^1.0", - "orchestra/testbench": "^6.0|^7.0", - "phpunit/phpunit": "^9.0" - }, - "suggest": { - "ext-intl": "Allows for more locales besides the default \"en\" when formatting money values." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "13.x-dev" - }, - "laravel": { - "providers": [ - "Laravel\\Cashier\\CashierServiceProvider" - ] - } - }, - "autoload": { - "psr-4": { - "Laravel\\Cashier\\": "src/", - "Laravel\\Cashier\\Database\\Factories\\": "database/factories/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - }, - { - "name": "Dries Vints", - "email": "dries@laravel.com" - } - ], - "description": "Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.", - "keywords": [ - "billing", - "laravel", - "stripe" - ], - "support": { - "issues": "https://github.com/laravel/cashier/issues", - "source": "https://github.com/laravel/cashier" - }, - "time": "2022-08-30T15:04:16+00:00" - }, { "name": "laravel/framework", "version": "v9.35.1", @@ -4326,75 +4180,6 @@ }, "time": "2022-02-09T09:35:03+00:00" }, - { - "name": "masterminds/html5", - "version": "2.7.6", - "source": { - "type": "git", - "url": "https://github.com/Masterminds/html5-php.git", - "reference": "897eb517a343a2281f11bc5556d6548db7d93947" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/897eb517a343a2281f11bc5556d6548db7d93947", - "reference": "897eb517a343a2281f11bc5556d6548db7d93947", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "ext-dom": "*", - "ext-libxml": "*", - "php": ">=5.3.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.7-dev" - } - }, - "autoload": { - "psr-4": { - "Masterminds\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Matt Butcher", - "email": "technosophos@gmail.com" - }, - { - "name": "Matt Farina", - "email": "matt@mattfarina.com" - }, - { - "name": "Asmir Mustafic", - "email": "goetas@gmail.com" - } - ], - "description": "An HTML5 parser and serializer.", - "homepage": "http://masterminds.github.io/html5-php", - "keywords": [ - "HTML5", - "dom", - "html", - "parser", - "querypath", - "serializer", - "xml" - ], - "support": { - "issues": "https://github.com/Masterminds/html5-php/issues", - "source": "https://github.com/Masterminds/html5-php/tree/2.7.6" - }, - "time": "2022-08-18T16:18:26+00:00" - }, { "name": "matriphe/iso-639", "version": "1.2", @@ -5596,96 +5381,6 @@ }, "time": "2022-02-21T01:04:05+00:00" }, - { - "name": "phenx/php-font-lib", - "version": "0.5.4", - "source": { - "type": "git", - "url": "https://github.com/dompdf/php-font-lib.git", - "reference": "dd448ad1ce34c63d09baccd05415e361300c35b4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/dd448ad1ce34c63d09baccd05415e361300c35b4", - "reference": "dd448ad1ce34c63d09baccd05415e361300c35b4", - "shasum": "" - }, - "require": { - "ext-mbstring": "*" - }, - "require-dev": { - "symfony/phpunit-bridge": "^3 || ^4 || ^5" - }, - "type": "library", - "autoload": { - "psr-4": { - "FontLib\\": "src/FontLib" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL-3.0" - ], - "authors": [ - { - "name": "Fabien Ménager", - "email": "fabien.menager@gmail.com" - } - ], - "description": "A library to read, parse, export and make subsets of different types of font files.", - "homepage": "https://github.com/PhenX/php-font-lib", - "support": { - "issues": "https://github.com/dompdf/php-font-lib/issues", - "source": "https://github.com/dompdf/php-font-lib/tree/0.5.4" - }, - "time": "2021-12-17T19:44:54+00:00" - }, - { - "name": "phenx/php-svg-lib", - "version": "0.5.0", - "source": { - "type": "git", - "url": "https://github.com/dompdf/php-svg-lib.git", - "reference": "76876c6cf3080bcb6f249d7d59705108166a6685" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/76876c6cf3080bcb6f249d7d59705108166a6685", - "reference": "76876c6cf3080bcb6f249d7d59705108166a6685", - "shasum": "" - }, - "require": { - "ext-mbstring": "*", - "php": "^7.1 || ^8.0", - "sabberworm/php-css-parser": "^8.4" - }, - "require-dev": { - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5" - }, - "type": "library", - "autoload": { - "psr-4": { - "Svg\\": "src/Svg" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL-3.0" - ], - "authors": [ - { - "name": "Fabien Ménager", - "email": "fabien.menager@gmail.com" - } - ], - "description": "A library to read, parse and export to PDF SVG files.", - "homepage": "https://github.com/PhenX/php-svg-lib", - "support": { - "issues": "https://github.com/dompdf/php-svg-lib/issues", - "source": "https://github.com/dompdf/php-svg-lib/tree/0.5.0" - }, - "time": "2022-09-06T12:16:56+00:00" - }, { "name": "php-http/client-common", "version": "2.6.0", @@ -7306,59 +7001,6 @@ }, "time": "2022-05-01T03:39:46+00:00" }, - { - "name": "sabberworm/php-css-parser", - "version": "8.4.0", - "source": { - "type": "git", - "url": "https://github.com/sabberworm/PHP-CSS-Parser.git", - "reference": "e41d2140031d533348b2192a83f02d8dd8a71d30" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sabberworm/PHP-CSS-Parser/zipball/e41d2140031d533348b2192a83f02d8dd8a71d30", - "reference": "e41d2140031d533348b2192a83f02d8dd8a71d30", - "shasum": "" - }, - "require": { - "ext-iconv": "*", - "php": ">=5.6.20" - }, - "require-dev": { - "codacy/coverage": "^1.4", - "phpunit/phpunit": "^4.8.36" - }, - "suggest": { - "ext-mbstring": "for parsing UTF-8 CSS" - }, - "type": "library", - "autoload": { - "psr-4": { - "Sabberworm\\CSS\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Raphael Schweikert" - } - ], - "description": "Parser for CSS Files written in PHP", - "homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser", - "keywords": [ - "css", - "parser", - "stylesheet" - ], - "support": { - "issues": "https://github.com/sabberworm/PHP-CSS-Parser/issues", - "source": "https://github.com/sabberworm/PHP-CSS-Parser/tree/8.4.0" - }, - "time": "2021-12-11T13:40:54+00:00" - }, { "name": "sabre/dav", "version": "4.4.0", @@ -8398,67 +8040,6 @@ }, "time": "2022-05-12T13:11:31+00:00" }, - { - "name": "stripe/stripe-php", - "version": "v9.7.0", - "source": { - "type": "git", - "url": "https://github.com/stripe/stripe-php.git", - "reference": "13e8ab3c7fa6b146cb94618000fe01aabaf4158f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/stripe/stripe-php/zipball/13e8ab3c7fa6b146cb94618000fe01aabaf4158f", - "reference": "13e8ab3c7fa6b146cb94618000fe01aabaf4158f", - "shasum": "" - }, - "require": { - "ext-curl": "*", - "ext-json": "*", - "ext-mbstring": "*", - "php": ">=5.6.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "3.5.0", - "php-coveralls/php-coveralls": "^2.5", - "phpstan/phpstan": "^1.2", - "phpunit/phpunit": "^5.7 || ^9.0", - "squizlabs/php_codesniffer": "^3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "psr-4": { - "Stripe\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Stripe and contributors", - "homepage": "https://github.com/stripe/stripe-php/contributors" - } - ], - "description": "Stripe PHP Library", - "homepage": "https://stripe.com/", - "keywords": [ - "api", - "payment processing", - "stripe" - ], - "support": { - "issues": "https://github.com/stripe/stripe-php/issues", - "source": "https://github.com/stripe/stripe-php/tree/v9.7.0" - }, - "time": "2022-10-13T23:12:16+00:00" - }, { "name": "symfony/console", "version": "v6.1.6", @@ -9785,93 +9366,6 @@ ], "time": "2022-05-24T11:49:31+00:00" }, - { - "name": "symfony/polyfill-intl-icu", - "version": "v1.26.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-icu.git", - "reference": "e407643d610e5f2c8a4b14189150f68934bf5e48" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/e407643d610e5f2c8a4b14189150f68934bf5e48", - "reference": "e407643d610e5f2c8a4b14189150f68934bf5e48", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "suggest": { - "ext-intl": "For best performance and support of other locales than \"en\"" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.26-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Icu\\": "" - }, - "classmap": [ - "Resources/stubs" - ], - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's ICU-related data and classes", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "icu", - "intl", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.26.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-05-24T11:49:31+00:00" - }, { "name": "symfony/polyfill-intl-idn", "version": "v1.26.0", diff --git a/config/app.php b/config/app.php index 523c28e8b71..4784212a6d2 100644 --- a/config/app.php +++ b/config/app.php @@ -194,13 +194,13 @@ */ App\Providers\AppServiceProvider::class, App\Providers\AuthServiceProvider::class, + App\Providers\EncryptionServiceProvider::class, App\Providers\EventServiceProvider::class, App\Providers\MacroServiceProvider::class, Vluzrmos\LanguageDetector\Providers\LanguageDetectorServiceProvider::class, App\Providers\RouteServiceProvider::class, Laravel\Socialite\SocialiteServiceProvider::class, Intervention\Image\ImageServiceProvider::class, - Laravel\Cashier\CashierServiceProvider::class, Laravel\Passport\PassportServiceProvider::class, Creativeorange\Gravatar\GravatarServiceProvider::class, App\Providers\DAVServiceProvider::class, diff --git a/config/cashier.php b/config/cashier.php deleted file mode 100644 index 2825285d421..00000000000 --- a/config/cashier.php +++ /dev/null @@ -1,88 +0,0 @@ - env('STRIPE_KEY'), - - 'secret' => env('STRIPE_SECRET'), - - /* - |-------------------------------------------------------------------------- - | Cashier Path - |-------------------------------------------------------------------------- - | - | This is the base URI path where Cashier's views, such as the payment - | verification screen, will be available from. You're free to tweak - | this path according to your preferences and application design. - | - */ - - 'path' => env('CASHIER_PATH', 'stripe'), - - /* - |-------------------------------------------------------------------------- - | Stripe Webhooks - |-------------------------------------------------------------------------- - | - | Your Stripe webhook secret is used to prevent unauthorized requests to - | your Stripe webhook handling controllers. The tolerance setting will - | check the drift between the current time and the signed request's. - | - */ - - 'webhook' => [ - 'secret' => env('STRIPE_WEBHOOK_SECRET'), - 'tolerance' => env('STRIPE_WEBHOOK_TOLERANCE', 300), - ], - - /* - |-------------------------------------------------------------------------- - | Currency - |-------------------------------------------------------------------------- - | - | This is the default currency that will be used when generating charges - | from your application. Of course, you are welcome to use any of the - | various world currencies that are currently supported via Stripe. - | - */ - - 'currency' => env('CASHIER_CURRENCY', 'usd'), - - /* - |-------------------------------------------------------------------------- - | Currency Locale - |-------------------------------------------------------------------------- - | - | This is the default locale in which your money values are formatted in - | for display. To utilize other locales besides the default en locale - | verify you have the "intl" PHP extension installed on the system. - | - */ - - 'currency_locale' => env('CASHIER_CURRENCY_LOCALE', 'en'), - - /* - |-------------------------------------------------------------------------- - | Payment Confirmation Notification - |-------------------------------------------------------------------------- - | - | If this setting is enabled, Cashier will automatically notify customers - | whose payments require additional verification. You should listen to - | Stripe's webhooks in order for this feature to function correctly. - | - */ - - 'payment_notification' => env('CASHIER_PAYMENT_NOTIFICATION'), - -]; diff --git a/config/monica.php b/config/monica.php index 855cd170452..da5067f2a72 100644 --- a/config/monica.php +++ b/config/monica.php @@ -284,4 +284,35 @@ */ 'export_size' => (int) env('EXPORT_SIZE', 5), + /* + |-------------------------------------------------------------------------- + | Licence key server + |-------------------------------------------------------------------------- + | + | When REQUIRES_SUBSCRIPTION is set to true, we need to check if the user + | has a valid licence key to unlock paid features. Licence keys are managed + | on our own customer portal. + | + */ + 'customer_portal_url' => env('CUSTOMER_PORTAL_URL', ''), + + 'customer_portal_client_id' => env('CUSTOMER_PORTAL_CLIENT_ID'), + + 'customer_portal_client_secret' => env('CUSTOMER_PORTAL_CLIENT_SECRET'), + + 'customer_portal_stripe_url' => env('CUSTOMER_PORTAL_STRIPE_URL', ''), + + /* + |-------------------------------------------------------------------------- + | Licence key encryption key + |-------------------------------------------------------------------------- + | + | All licence keys are encrypted with this key on the customer portal when + | the key is generated. + | + */ + + 'licence_private_key' => env('LICENCE_PRIVATE_KEY'), + + 'licence_cipher' => 'AES-256-GCM', ]; diff --git a/database/factories/SettingsFactory.php b/database/factories/SettingsFactory.php index 4701c28b0db..5351b287312 100644 --- a/database/factories/SettingsFactory.php +++ b/database/factories/SettingsFactory.php @@ -16,14 +16,3 @@ 'symbol' => $faker->realText(10), ]; }); - -$factory->define(\Laravel\Cashier\Subscription::class, function (Faker\Generator $faker) { - return [ - 'account_id' => factory(App\Models\Account\Account::class)->create()->id, - 'name' => $faker->word(), - 'stripe_id' => $faker->word(), - 'stripe_price' => $faker->randomElement(['plan-1', 'plan-2', 'plan-3']), - 'quantity' => 1, - 'created_at' => now(), - ]; -}); diff --git a/database/migrations/2022_03_08_115818_add_licence_keys_to_accounts.php b/database/migrations/2022_03_08_115818_add_licence_keys_to_accounts.php new file mode 100644 index 00000000000..4c70be2656f --- /dev/null +++ b/database/migrations/2022_03_08_115818_add_licence_keys_to_accounts.php @@ -0,0 +1,43 @@ +string('licence_key', 4096)->after('uuid')->nullable(); + $table->datetime('valid_until_at')->after('licence_key')->nullable(); + $table->string('purchaser_email', 255)->after('valid_until_at')->nullable(); + $table->string('frequency', 15)->after('purchaser_email')->nullable(); + $table->boolean('is_on_stripe')->after('frequency')->default(false); + }); + + Account::whereNotNull('stripe_id')->update(['is_on_stripe' => true]); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('accounts', function (Blueprint $table) { + $table->dropColumn('licence_keys'); + $table->dropColumn('valid_until_at'); + $table->dropColumn('purchaser_email'); + $table->dropColumn('frequency'); + $table->dropColumn('is_on_stripe'); + }); + } +} diff --git a/phpstan.neon b/phpstan.neon index be6ad5c12af..5f09688594f 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -33,10 +33,6 @@ parameters: path: */Traits/Searchable.php - message: '#Access to an undefined property Faker\\Generator::\$state\.#' path: */Console/Commands/SetupTest.php - - message: '#Access to an undefined property Stripe\\Subscription::\$plan\.#' - path: */Helpers/InstanceHelper.php - - message: '#Call to an undefined method Illuminate\\Database\\Eloquent\\Relations\\HasMany::recurring\(\)\.#' - path: */Traits/Subscription.php - message: '#Function dns_get_record is unsafe to use\. It can return FALSE instead of throwing an exception\. Please add ''use function Safe\\dns_get_record;'' at the beginning of the file to use the variant provided by the ''thecodingmachine/safe'' library\.#' path: */Services/DavClient/Utils/Dav/ServiceUrlQuery.php - message: '#Access to an undefined property App\\Models\\Relationship\\Relationship::\$relationshipTypeLocalized\.#' diff --git a/resources/js/components/settings/Subscription.vue b/resources/js/components/settings/Subscription.vue deleted file mode 100644 index e2a06f11781..00000000000 --- a/resources/js/components/settings/Subscription.vue +++ /dev/null @@ -1,297 +0,0 @@ - - - diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php index 503ccdff15e..43f5b052bea 100644 --- a/resources/lang/en/settings.php +++ b/resources/lang/en/settings.php @@ -202,68 +202,22 @@ 'subscriptions_account_next_billing_title' => 'Next bill', 'subscriptions_account_next_billing' => 'Your subscription will auto-renew on :date.', - 'subscriptions_account_bill_monthly' => 'We’ll bill you :price for another month.', 'subscriptions_account_bill_annual' => 'We’ll bill you :price for another year.', 'subscriptions_account_change' => 'Change plan', + 'subscriptions_account_payment' => 'Which payment option fits you best?', 'subscriptions_account_cancel_title' => 'Cancel subscription', 'subscriptions_account_cancel_action' => 'Cancel subscription', 'subscriptions_account_cancel' => 'You can cancel your subscription at any time.', - 'subscriptions_account_free_plan' => 'You are on the free plan.', - 'subscriptions_account_free_plan_upgrade' => 'You can upgrade your account to the :name plan, which costs $:price per month. Here are the advantages:', - 'subscriptions_account_free_plan_benefits_users' => 'Unlimited number of users', - 'subscriptions_account_free_plan_benefits_reminders' => 'Reminders by email', - 'subscriptions_account_free_plan_benefits_import_data_vcard' => 'Import your contacts with vCard', - 'subscriptions_account_free_plan_benefits_support' => 'Support the project in the long run, so we can introduce more great features.', - 'subscriptions_account_upgrade' => 'Upgrade your account', 'subscriptions_account_upgrade_title' => 'Upgrade Monica today and have more meaningful relationships.', - 'subscriptions_account_upgrade_choice' => 'Pick a plan below and join over :customers persons who upgraded their Monica.', - 'subscriptions_account_update_title' => 'Update Monica subscription', - 'subscriptions_account_update_description' => 'You can change your subscription’s frequency here.', - 'subscriptions_account_update_information' => 'You will be billed immediately for the new amount. Your subscription will extend to the new period, depending on your choice.', - 'subscriptions_account_invoices' => 'Invoices', - 'subscriptions_account_invoices_download' => 'Download', - 'subscriptions_account_invoices_subscription' => 'Subscription from :startDate to :endDate', - 'subscriptions_account_payment' => 'Which payment option fits you best?', - 'subscriptions_account_confirm_payment' => 'Your payment is currently incomplete, please confirm your payment.', - 'subscriptions_downgrade_title' => 'Downgrade your account to the free plan', - 'subscriptions_downgrade_limitations' => 'The free plan has limitations. In order to be able to downgrade, you need to pass the checklist below:', - 'subscriptions_downgrade_rule_users' => 'You must have only 1 user in your account', - 'subscriptions_downgrade_rule_users_constraint' => 'You currently have 1 user in your account.|You currently have :count users in your account.', - 'subscriptions_downgrade_rule_invitations' => 'You must not have any pending invitations', - 'subscriptions_downgrade_rule_invitations_constraint' => 'You currently have 1 pending invitation.|You currently have :count pending invitations.', - 'subscriptions_downgrade_rule_contacts' => 'You must not have more than :number active contacts', - 'subscriptions_downgrade_rule_contacts_constraint' => 'You currently have 1 contact.|You currently have :count contacts.', - 'subscriptions_downgrade_rule_contacts_archive' => 'We can also archive all your contacts for you – that would clear this rule and let you proceed with your account’s downgrade process.', - 'subscriptions_downgrade_cta' => 'Downgrade', - 'subscriptions_downgrade_success' => 'You are back to the Free plan!', - 'subscriptions_downgrade_thanks' => 'Thanks so much for trying the paid plan. We keep adding new features on Monica all the time – so you might want to come back in the future to see if you might be interested in taking a subscription again.', + 'subscriptions_account_upgrade_choice' => 'Monica requires a licence key to be completely functional.', 'subscriptions_back' => 'Back to settings', - 'subscriptions_upgrade_title' => 'Upgrade your account', - 'subscriptions_upgrade_choose' => 'You picked the :plan plan.', - 'subscriptions_upgrade_infos' => 'We couldn’t be happier. Enter your payment info below.', - 'subscriptions_upgrade_name' => 'Name on card', - 'subscriptions_upgrade_zip' => 'ZIP or postal code', - 'subscriptions_upgrade_credit' => 'Credit or debit card', - 'subscriptions_upgrade_submit' => 'Pay {amount}', - 'subscriptions_upgrade_charge' => 'We’ll charge your card :price now. The next charge will be on :date. If you ever change your mind, you can cancel at any time, no questions asked.', - 'subscriptions_upgrade_charge_handled' => 'The payment is handled by Stripe. No card information touches our server.', 'subscriptions_upgrade_success' => 'Thank you! You are now subscribed.', 'subscriptions_upgrade_thanks' => 'Welcome to the community of people who try to make the world a better place.', - 'subscriptions_payment_confirm_title' => 'Confirm your :amount payment', - 'subscriptions_payment_confirm_information' => 'Extra confirmation is needed to process your payment. Please confirm your payment by filling out your payment details below.', - 'subscriptions_payment_succeeded_title' => 'Payment Successful', - 'subscriptions_payment_succeeded' => 'This payment was already successfully confirmed.', - 'subscriptions_payment_cancelled_title' => 'Payment Cancelled', - 'subscriptions_payment_cancelled' => 'This payment was cancelled.', - 'subscriptions_payment_error_name' => 'Please provide your name.', - 'subscriptions_payment_success' => 'The payment was successful.', - - 'subscriptions_pdf_title' => 'Your :name monthly subscription', 'subscriptions_plan_frequency_year' => ':amount / year', 'subscriptions_plan_frequency_month' => ':amount / month', - 'subscriptions_plan_choose' => 'Choose this plan', + 'subscriptions_plan_choose' => 'Get your licence key', 'subscriptions_plan_year_title' => 'Pay annually', 'subscriptions_plan_year_bonus' => 'Peace of mind for a whole year', 'subscriptions_plan_month_title' => 'Pay monthly', @@ -271,7 +225,11 @@ 'subscriptions_plan_include1' => 'Included with your upgrade:', 'subscriptions_plan_include2' => 'Unlimited number of contacts • Unlimited number of users • Reminders by email • Import with vCard • Personalization of the contact sheet', 'subscriptions_plan_include3' => '100% of the profits go the development of this great open source project.', + 'subscriptions_plan_question' => 'Do you have your licence key?', + 'subscriptions_key_paste' => 'Please paste your licence key here', 'subscriptions_help_title' => 'Additional details you may be curious about', + 'subscriptions_help_licencekey_title' => 'What is a licence key?', + 'subscriptions_help_licencekey_desc' => 'A licence key is an unique identifier that you will get once you purchase a plan. It will be used to unlock all the paid features in your account. Licence keys are managed on https://customers.monicahq.com and you will need to create a new account on this site to get your licence key, that you will need to paste here.', 'subscriptions_help_opensource_title' => 'What is an open source project?', 'subscriptions_help_opensource_desc' => 'Monica is an open source project. This means it is built by a community who wants to build a great tool for the greater good. Being open source means the code is publicly available on GitHub, and everyone can inspect it, modify it or enhance it. All the money we raise is dedicated to building better features, paying for more powerful servers, and paying other costs. Thanks for your help. We couldn’t do it without you.', 'subscriptions_help_limits_title' => 'Is there a limit to the number of contacts we can have on the free plan?', @@ -280,12 +238,12 @@ 'subscriptions_help_discounts_desc' => 'We do! Monica is free for students, and free for non-profits and charities. Just contact the support with a proof of your status and we’ll apply this special status in your account.', 'subscriptions_help_change_title' => 'What if I change my mind?', 'subscriptions_help_change_desc' => 'You can cancel anytime, no questions asked, and all by yourself – no need to contact support. However, you will not be refunded for the current period.', - - 'stripe_error_card' => 'Your card was declined. Decline message is: :message', - 'stripe_error_api_connection' => 'Network communication with Stripe failed. Try again later.', - 'stripe_error_rate_limit' => 'Too many requests with Stripe right now. Try again later.', - 'stripe_error_invalid_request' => 'Invalid parameters. Try again later.', - 'stripe_error_authentication' => 'Wrong authentication with Stripe', + 'subscriptions_licence_key_does_not_exist' => 'The licence key does not appear to exist in our system. Please try again.', + 'subscriptions_licence_key_invalid' => 'This licence key is not valid anymore. Please renew your licence.', + 'subscriptions_licence_key_problem' => 'There is a problem with the system.', + 'subscriptions_licence_key_frequency_monthly' => 'Monthly', + 'subscriptions_licence_key_frequency_annual' => 'Annual', + 'subscriptions_account_invoices' => 'Your invoices are available on the customer portal.', 'import_title' => 'Import contacts in your account', 'import_cta' => 'Upload contacts', diff --git a/resources/sass/stripe.scss b/resources/sass/stripe.scss deleted file mode 100644 index 73c2a547e4c..00000000000 --- a/resources/sass/stripe.scss +++ /dev/null @@ -1,22 +0,0 @@ -.StripeElement { - background-color: #ffffff; - height: 45px; - width: 100%; - padding: 13px 12px; - border-radius: 4px; - border: 1px solid #999999; - -webkit-transition: box-shadow 150ms ease; - transition: box-shadow 150ms ease; -} - -.StripeElement--focus { - box-shadow: 0 1px 3px 0 #d0d0d0; -} - -.StripeElement--invalid { - border-color: #d9534f; -} - -.StripeElement--webkit-autofill { - background-color: #fffacd !important; -} diff --git a/resources/views/layouts/skeleton.blade.php b/resources/views/layouts/skeleton.blade.php index 028015e3865..3679ddf386a 100644 --- a/resources/views/layouts/skeleton.blade.php +++ b/resources/views/layouts/skeleton.blade.php @@ -15,10 +15,6 @@ - {{-- Required only for the Upgrade account page --}} - @if (Route::currentRouteName() == 'settings.subscriptions.upgrade' || Route::currentRouteName() == 'settings.subscriptions.confirm') - - @endif @@ -42,30 +38,19 @@
- @if (Route::currentRouteName() != 'settings.subscriptions.confirm') - @include('partials.header') - @include('partials.subscription') - @endif + @include('partials.header') @yield('content')
- @if (Route::currentRouteName() != 'settings.subscriptions.confirm') - @include('partials.footer') - @endif + @include('partials.footer') {{-- THE JS FILE OF THE APP --}} @push('scripts') + @endpush - {{-- Load everywhere except on the Upgrade account page --}} - @if (Route::currentRouteName() != 'settings.subscriptions.upgrade' && Route::currentRouteName() != 'settings.subscriptions.confirm') - @push('scripts') - - @endpush - @endif - @stack('scripts') diff --git a/resources/views/partials/subscription.blade.php b/resources/views/partials/subscription.blade.php deleted file mode 100644 index e30097ac04b..00000000000 --- a/resources/views/partials/subscription.blade.php +++ /dev/null @@ -1,16 +0,0 @@ -@if (($subscription = auth()->user()->account->getSubscribedPlan()) && $subscription->hasIncompletePayment()) - -
- {!! trans('settings.subscriptions_account_confirm_payment', ['url' => route('settings.subscriptions.confirm', $subscription->latestPayment() ? $subscription->latestPayment()->id : '')]) !!} -
- -@if (! app()->environment('production')) -

- - {{-- No translation needed --}} - Force payment success (test). - -

-@endif - -@endif diff --git a/resources/views/settings/subscriptions/account.blade.php b/resources/views/settings/subscriptions/account.blade.php index ecf9e3678e3..70a94df5edf 100644 --- a/resources/views/settings/subscriptions/account.blade.php +++ b/resources/views/settings/subscriptions/account.blade.php @@ -33,13 +33,13 @@
-
+
-

{{ trans('settings.subscriptions_account_current_plan') }}

+

{{ trans('settings.subscriptions_account_current_plan') }}

-

{{ trans('settings.subscriptions_account_current_paid_plan', ['name' => $planInformation['name']]) }}

+

{{ trans('settings.subscriptions_account_current_paid_plan', ['name' => $planInformation]) }}

- @include('partials.subscription') +

{!! trans('settings.subscriptions_account_invoices', ['url' => $customerPortalUrl]) !!}

@@ -50,15 +50,14 @@
- {!! trans('settings.subscriptions_account_next_billing', ['date' => $planInformation['nextBillingDate']]) !!} -
-
- {!! trans('settings.subscriptions_account_bill_' . $planInformation['type'], ['price' => $planInformation['friendlyPrice']]) !!} + {!! trans('settings.subscriptions_account_next_billing', ['date' => $nextBillingDate]) !!}
@@ -76,7 +75,7 @@
- - {{-- Only display invoices if the subscription exists or existed --}} - @if ($hasInvoices) -
-

{{ trans('settings.subscriptions_account_invoices') }}

- -
- @endif -
diff --git a/resources/views/settings/subscriptions/archive.blade.php b/resources/views/settings/subscriptions/archive.blade.php deleted file mode 100644 index 2f664bbac43..00000000000 --- a/resources/views/settings/subscriptions/archive.blade.php +++ /dev/null @@ -1,53 +0,0 @@ -@extends('layouts.skeleton') - -@section('content') - -
- - {{-- Breadcrumb --}} - - -
-
-
-
- @include('partials.errors') - -
-
-

{{ trans('settings.archive_title') }}

- -

{{ trans('settings.archive_desc') }}

- -
- @csrf - -

- -
- -
-
-
-
-
- - @endsection diff --git a/resources/views/settings/subscriptions/blank.blade.php b/resources/views/settings/subscriptions/blank.blade.php index 266b52bdc5d..3e302a4909a 100644 --- a/resources/views/settings/subscriptions/blank.blade.php +++ b/resources/views/settings/subscriptions/blank.blade.php @@ -28,22 +28,21 @@
-
+

{{ trans('settings.subscriptions_account_upgrade_title') }}

-

{{ trans('settings.subscriptions_account_upgrade_choice', ['customers' => $numberOfCustomers]) }}

+

{{ trans('settings.subscriptions_account_upgrade_choice') }}

-
+
-

{{ trans('settings.subscriptions_account_payment') }}

{{ trans('settings.subscriptions_plan_year_title') }}

- {{ trans('settings.subscriptions_plan_choose') }} + {{ trans('settings.subscriptions_plan_choose') }}

{{ trans('settings.subscriptions_plan_frequency_year', ['amount' => \App\Helpers\InstanceHelper::getPlanInformationFromConfig('annual')['friendlyPrice']]) }} @@ -68,7 +67,7 @@

{{ trans('settings.subscriptions_plan_month_title') }}

- {{ trans('settings.subscriptions_plan_choose') }} + {{ trans('settings.subscriptions_plan_choose') }}

{{ trans('settings.subscriptions_plan_frequency_month', ['amount' => \App\Helpers\InstanceHelper::getPlanInformationFromConfig('monthly')['friendlyPrice']]) }} @@ -90,13 +89,40 @@

-

{{ trans('settings.subscriptions_plan_include1') }}

+ + +

{{ trans('settings.subscriptions_plan_include1') }}

{{ trans('settings.subscriptions_plan_include2') }}

{{ trans('settings.subscriptions_plan_include3') }}

+ +
+ +

Steps to have a subscription

+ +

1. Go to the customer portal to get a licence key

+

2. Subscribe and obtain your licence key

+

3. Paste your licence key below.

+ + @include('partials.errors') + +
+ @csrf + +
+ + +
+
+
+
+

{{ trans('settings.subscriptions_help_title') }}

+

{{ trans('settings.subscriptions_help_licencekey_title') }}

+

{{ trans('settings.subscriptions_help_licencekey_desc') }}

+

{{ trans('settings.subscriptions_help_opensource_title') }}

{{ trans('settings.subscriptions_help_opensource_desc') }}

diff --git a/resources/views/settings/subscriptions/confirm.blade.php b/resources/views/settings/subscriptions/confirm.blade.php deleted file mode 100644 index 4e2b2b61ca9..00000000000 --- a/resources/views/settings/subscriptions/confirm.blade.php +++ /dev/null @@ -1,36 +0,0 @@ -@extends('layouts.skeleton') - -@section('content') - -
- -
-
- @if (! $payment->isSucceeded() && ! $payment->isCancelled()) -

{{ trans('settings.subscriptions_payment_confirm_title', ['amount' => $payment->amount()]) }}

-

{{ trans('settings.subscriptions_payment_confirm_information') }}

- @endif - - @include('partials.errors') - - -
-
-
- -@endsection - -@push('scripts') - - -@endpush diff --git a/resources/views/settings/subscriptions/downgrade-checklist.blade.php b/resources/views/settings/subscriptions/downgrade-checklist.blade.php deleted file mode 100644 index 2e828aaaed2..00000000000 --- a/resources/views/settings/subscriptions/downgrade-checklist.blade.php +++ /dev/null @@ -1,80 +0,0 @@ -@extends('layouts.skeleton') - -@section('content') - -
- - {{-- Breadcrumb --}} - - -
-
-
-
- @include('partials.errors') - -

{{ trans('settings.subscriptions_downgrade_title') }}

- -

{{ trans('settings.subscriptions_downgrade_limitations') }}

- -
    - -
  • - - {{ trans('settings.subscriptions_downgrade_rule_users') }} - {!! trans_choice('settings.subscriptions_downgrade_rule_users_constraint', $numberOfUsers, ['url' => route('settings.users.index'), 'count' => $numberOfUsers]) !!} -
  • - -
  • - - {{ trans('settings.subscriptions_downgrade_rule_invitations') }} - {!! trans_choice('settings.subscriptions_downgrade_rule_invitations_constraint', $numberOfPendingInvitations, ['url' => route('settings.users.index'), 'count' => $numberOfPendingInvitations]) !!} -
  • - -
  • - - {{ trans('settings.subscriptions_downgrade_rule_contacts', ['number' => config('monica.number_of_allowed_contacts_free_account')]) }} - {!! trans_choice('settings.subscriptions_downgrade_rule_contacts_constraint', $numberOfActiveContacts, ['url' => '/people', 'count' => $numberOfActiveContacts]) !!} - @if ($hasReachedContactLimit) - {!! trans('settings.subscriptions_downgrade_rule_contacts_archive', ['url' => route('settings.subscriptions.archive')]) !!} - @endif -
  • - -
- -
- @csrf - - @if ($canDowngrade) -

- @else -

- @endif - -
- -
-
-
-
-
- -@endsection diff --git a/resources/views/settings/subscriptions/downgrade-success.blade.php b/resources/views/settings/subscriptions/downgrade-success.blade.php deleted file mode 100644 index 06781b9d1d7..00000000000 --- a/resources/views/settings/subscriptions/downgrade-success.blade.php +++ /dev/null @@ -1,70 +0,0 @@ -@extends('layouts.skeleton') - -@section('content') - -
- - {{-- Breadcrumb --}} - - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -

{{ trans('settings.subscriptions_downgrade_success') }}

-

{{ trans('settings.subscriptions_downgrade_thanks') }}

-

{{trans('settings.subscriptions_back') }}

-
-
-
- -@endsection diff --git a/resources/views/settings/subscriptions/stripe.blade.php b/resources/views/settings/subscriptions/stripe.blade.php new file mode 100644 index 00000000000..85195f23874 --- /dev/null +++ b/resources/views/settings/subscriptions/stripe.blade.php @@ -0,0 +1,60 @@ +@extends('layouts.skeleton') + +@section('content') + +
+ + {{-- Breadcrumb --}} + + +
+
+ + @include('settings._sidebar') + +
+ +
+
+ +

{{ trans('settings.subscriptions_account_current_plan') }}

+ +

Thanks for being a subscriber.

+

You can edit/cancel your plan or update your credit card information below.

+ +

You will need to login using the email address you've used at the time you've subscried to Monica. Once you enter your email address, you will receive a unique code to your email address to confirm your identity. From there, you'll be able to manage your subscriptions.

+ +

+ + Manage your subscription + +

+ +

Reminder: everything billing related is handled by Stripe, and we have no access to the credit card information at all.

+
+
+ +
+
+
+
+ +@endsection diff --git a/resources/views/settings/subscriptions/success.blade.php b/resources/views/settings/subscriptions/success.blade.php index b35674160d3..31fcd39dcdb 100644 --- a/resources/views/settings/subscriptions/success.blade.php +++ b/resources/views/settings/subscriptions/success.blade.php @@ -14,7 +14,7 @@ {{ trans('app.breadcrumb_dashboard') }}
  • - {{ trans('app.breadcrumb_settings') }} + {{ trans('app.breadcrumb_settings') }}
  • {{ trans('app.breadcrumb_settings_subscriptions') }} diff --git a/resources/views/settings/subscriptions/update.blade.php b/resources/views/settings/subscriptions/update.blade.php deleted file mode 100644 index 594cd6dec2a..00000000000 --- a/resources/views/settings/subscriptions/update.blade.php +++ /dev/null @@ -1,73 +0,0 @@ -@extends('layouts.skeleton') - -@section('content') - -
    - - {{-- Breadcrumb --}} - - -
    -
    -
    -
    - -

    {{ trans('settings.subscriptions_account_update_title') }}

    - -

    {{ trans('settings.subscriptions_account_update_description') }}

    - - @if ($legacyPlan) -
    - - -
    - @endif - -
    - @csrf - - @foreach ($plans as $plan) -
    - - -
    - @endforeach - -

    {{ trans('settings.subscriptions_account_update_information') }}

    - -
    - - {{ trans('app.cancel') }} -
    -
    - -
    -
    -
    -
    -
    - -@endsection diff --git a/resources/views/settings/subscriptions/upgrade.blade.php b/resources/views/settings/subscriptions/upgrade.blade.php deleted file mode 100644 index 7d75752e3bf..00000000000 --- a/resources/views/settings/subscriptions/upgrade.blade.php +++ /dev/null @@ -1,55 +0,0 @@ -@extends('layouts.skeleton') - -@section('content') - -
    - - {{-- Breadcrumb --}} - - -
    -
    -

    {{ trans('settings.subscriptions_upgrade_choose', ['plan' => $planInformation['type']]) }}

    -

    {{ trans('settings.subscriptions_upgrade_infos') }}

    - - @include('partials.errors') - - -

    {{ trans('settings.subscriptions_upgrade_charge', ['price' => $planInformation['friendlyPrice'], 'date' => $nextTheoriticalBillingDate]) }}

    -

    {!! trans('settings.subscriptions_upgrade_charge_handled', ['url' => 'https://stripe.com']) !!}

    -
    -
    -
    - -@endsection - -@push('scripts') - - -@endpush diff --git a/routes/web.php b/routes/web.php index 126473ebbe1..5ac4958d552 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,6 +1,5 @@ group(function () { Route::get('/settings/subscriptions', 'Settings\\SubscriptionsController@index')->name('index'); - Route::get('/settings/subscriptions/upgrade', 'Settings\\SubscriptionsController@upgrade')->name('upgrade'); - Route::get('/settings/subscriptions/upgrade/success', 'Settings\\SubscriptionsController@upgradeSuccess')->name('upgrade.success'); - Route::get('/settings/subscriptions/update', 'Settings\\SubscriptionsController@update')->name('update'); - Route::post('/settings/subscriptions/update', 'Settings\\SubscriptionsController@processUpdate'); - Route::get('/settings/subscriptions/confirmPayment/{id}', 'Settings\\SubscriptionsController@confirmPayment')->name('confirm'); - Route::post('/settings/subscriptions/processPayment', 'Settings\\SubscriptionsController@processPayment')->name('payment'); - Route::get('/settings/subscriptions/invoice/{invoice}', 'Settings\\SubscriptionsController@downloadInvoice')->name('invoice'); - Route::get('/settings/subscriptions/downgrade', 'Settings\\SubscriptionsController@downgrade')->name('downgrade'); - Route::post('/settings/subscriptions/downgrade', 'Settings\\SubscriptionsController@processDowngrade'); - Route::get('/settings/subscriptions/archive', 'Settings\\SubscriptionsController@archive')->name('archive'); - Route::post('/settings/subscriptions/archive', 'Settings\\SubscriptionsController@processArchive'); - Route::get('/settings/subscriptions/downgrade/success', 'Settings\\SubscriptionsController@downgradeSuccess')->name('downgrade.success'); - if (! App::environment('production')) { - Route::get('/settings/subscriptions/forceCompletePaymentOnTesting', 'Settings\\SubscriptionsController@forceCompletePaymentOnTesting')->name('forceCompletePaymentOnTesting'); - } + Route::post('/settings/subscriptions', 'Settings\\SubscriptionsController@store')->name('store'); }); Route::get('/settings/auditlogs', 'Settings\\AuditLogController@index')->name('auditlog.index'); diff --git a/tests/Feature/AccountSubscriptionTest.php b/tests/Feature/AccountSubscriptionTest.php deleted file mode 100644 index dff5e59353f..00000000000 --- a/tests/Feature/AccountSubscriptionTest.php +++ /dev/null @@ -1,304 +0,0 @@ -markTestSkipped('Set STRIPE_SECRET to run this test.'); - } else { - config([ - 'services.stripe.secret' => env('STRIPE_SECRET'), - 'monica.requires_subscription' => true, - 'monica.paid_plan_monthly_friendly_name' => 'Monthly', - 'monica.paid_plan_monthly_id' => 'monthly', - 'monica.paid_plan_monthly_price' => 100, - 'monica.paid_plan_annual_friendly_name' => 'Annual', - 'monica.paid_plan_annual_id' => 'annual', - 'monica.paid_plan_annual_price' => 500, - ]); - } - } - - public static function setUpBeforeClass(): void - { - if (empty(env('STRIPE_SECRET'))) { - return; - } - - Stripe::setApiVersion('2019-03-14'); - Stripe::setApiKey(env('STRIPE_SECRET')); - - static::$productId = static::$stripePrefix.'product-'.Str::random(10); - static::$monthlyPlanId = static::$stripePrefix.'monthly-'.Str::random(10); - static::$annualPlanId = static::$stripePrefix.'annual-'.Str::random(10); - - Product::create([ - 'id' => static::$productId, - 'name' => 'Monica Test Product', - 'type' => 'service', - ]); - - Plan::create([ - 'id' => static::$monthlyPlanId, - 'nickname' => 'Monthly', - 'currency' => 'USD', - 'interval' => 'month', - 'billing_scheme' => 'per_unit', - 'amount' => 100, - 'product' => static::$productId, - ]); - Plan::create([ - 'id' => static::$annualPlanId, - 'nickname' => 'Annual', - 'currency' => 'USD', - 'interval' => 'year', - 'billing_scheme' => 'per_unit', - 'amount' => 500, - 'product' => static::$productId, - ]); - } - - public static function tearDownAfterClass(): void - { - parent::tearDownAfterClass(); - - if (static::$monthlyPlanId) { - static::deleteStripeResource(new Plan(static::$monthlyPlanId)); - static::$monthlyPlanId = null; - } - if (static::$annualPlanId) { - static::deleteStripeResource(new Plan(static::$annualPlanId)); - static::$annualPlanId = null; - } - if (static::$productId) { - static::deleteStripeResource(new Product(static::$productId)); - static::$productId = null; - } - } - - protected static function deleteStripeResource($resource) - { - try { - if (method_exists($resource, 'delete')) { - $resource->delete(); - } - } catch (\Stripe\Exception\ApiErrorException $e) { - // - } - } - - public function test_it_throw_an_error_on_subscribe() - { - $user = $this->signin(); - $user->email = 'test_it_throw_an_error_on_subscribe@monica-test.com'; - $user->save(); - - $this->expectException(\App\Exceptions\StripeException::class); - $user->account->subscribe('xxx', 'annual'); - } - - public function test_it_sees_the_plan_names() - { - $user = $this->signin(); - - $response = $this->get('/settings/subscriptions'); - - $response->assertSee('Pick a plan below and join over 0 persons who upgraded their Monica.'); - } - - public function test_it_get_the_plan_name() - { - $user = $this->signin(); - - factory(Subscription::class)->create([ - 'account_id' => $user->account_id, - 'name' => 'Annual', - 'stripe_price' => 'annual', - 'stripe_id' => 'test', - 'quantity' => 1, - ]); - - $this->assertEquals('Annual', $user->account->getSubscribedPlanName()); - } - - public function test_it_throw_an_error_on_cancel() - { - $user = $this->signin(); - - factory(Subscription::class)->create([ - 'account_id' => $user->account_id, - 'name' => 'Annual', - 'stripe_price' => 'annual', - 'stripe_id' => 'test', - 'quantity' => 1, - ]); - - $this->expectException(\App\Exceptions\StripeException::class); - $user->account->subscriptionCancel(); - } - - public function test_it_get_subscription_page() - { - $user = $this->signin(); - - factory(Subscription::class)->create([ - 'account_id' => $user->account_id, - 'name' => 'Annual', - 'stripe_price' => 'annual', - 'stripe_id' => 'sub_X', - 'quantity' => 1, - ]); - - $response = $this->get('/settings/subscriptions'); - - $response->assertSee('You are on the Annual plan. Thanks so much for being a subscriber.'); - } - - public function test_it_get_upgrade_page() - { - $user = $this->signin(); - - $response = $this->get('/settings/subscriptions/upgrade?plan=annual'); - - $response->assertSee('You picked the annual plan.'); - } - - public function test_it_subscribe() - { - $user = $this->signin(); - $user->email = 'test_it_subscribe@monica-test.com'; - $user->save(); - - $response = $this->post('/settings/subscriptions/processPayment', [ - 'payment_method' => 'pm_card_visa', - 'plan' => 'annual', - ]); - - $response->assertRedirect('/settings/subscriptions/upgrade/success'); - } - - // public function test_it_subscribe_with_2nd_auth() - // { - // $user = $this->signin(); - // $user->email = 'test_it_subscribe_with_2nd_auth@monica-test.com'; - // $user->save(); - - // $response = $this->followingRedirects()->post('/settings/subscriptions/processPayment', [ - // 'payment_method' => 'pm_card_threeDSecure2Required', - // 'plan' => 'annual', - // ]); - - // $response->assertSee('Extra confirmation is needed to process your payment.'); - // } - - public function test_it_subscribe_with_error() - { - $user = $this->signin(); - $user->email = 'test_it_subscribe_with_error@monica-test.com'; - $user->save(); - - $response = $this->post('/settings/subscriptions/processPayment', [ - 'payment_method' => 'error', - 'plan' => 'annual', - ], [ - 'HTTP_REFERER' => 'back', - ]); - - $response->assertRedirect('/back'); - } - - public function test_it_does_not_subscribe() - { - $user = $this->signin(); - $user->email = 'test_it_does_not_subscribe@monica-test.com'; - $user->save(); - - try { - $user->account->subscribe('pm_card_chargeDeclined', 'annual'); - } catch (\App\Exceptions\StripeException $e) { - $this->assertEquals('Your card was declined. Decline message is: Your card was declined.', $e->getMessage()); - - return; - } - $this->fail(); - } - - public function test_it_get_blank_page_on_update_if_not_subscribed() - { - $this->signin(); - - $response = $this->get('/settings/subscriptions/update'); - - $response->assertSee('Upgrade Monica today and have more meaningful relationships.'); - } - - public function test_it_get_subscription_update() - { - $user = $this->signin(); - $user->email = 'test_it_subscribe@monica-test.com'; - $user->save(); - - $response = $this->post('/settings/subscriptions/processPayment', [ - 'payment_method' => 'pm_card_visa', - 'plan' => 'annual', - ]); - - $response = $this->get('/settings/subscriptions/update'); - - $response->assertSee('Monthly – $1.00'); - $response->assertSee('Annual – $5.00'); - } - - public function test_it_process_subscription_update() - { - $user = $this->signin(); - $user->email = 'test_it_subscribe@monica-test.com'; - $user->save(); - - $response = $this->post('/settings/subscriptions/processPayment', [ - 'payment_method' => 'pm_card_visa', - 'plan' => 'monthly', - ]); - - $response = $this->followingRedirects()->post('/settings/subscriptions/update', [ - 'frequency' => 'annual', - ]); - - $response->assertSee('You are on the Annual plan.'); - } -} diff --git a/tests/Unit/Helpers/DateHelperTest.php b/tests/Unit/Helpers/DateHelperTest.php index 9a4e0c7d360..297bb4e304a 100644 --- a/tests/Unit/Helpers/DateHelperTest.php +++ b/tests/Unit/Helpers/DateHelperTest.php @@ -450,26 +450,6 @@ public function test_get_month_and_year() ); } - public function test_it_gets_date_one_month_from_now() - { - Carbon::setTestNow(Carbon::create(2017, 1, 1)); - - $this->assertEquals( - '2017-02-01', - DateHelper::getNextTheoriticalBillingDate('monthly')->toDateString() - ); - } - - public function test_it_gets_date_one_year_from_now() - { - Carbon::setTestNow(Carbon::create(2017, 1, 1)); - - $this->assertEquals( - '2018-01-01', - DateHelper::getNextTheoriticalBillingDate('yearly')->toDateString() - ); - } - public function test_it_returns_a_list_with_years() { $user = $this->signIn(); diff --git a/tests/Unit/Helpers/InstanceHelperTest.php b/tests/Unit/Helpers/InstanceHelperTest.php index be1cfb90eb2..ae12557894e 100644 --- a/tests/Unit/Helpers/InstanceHelperTest.php +++ b/tests/Unit/Helpers/InstanceHelperTest.php @@ -2,7 +2,6 @@ namespace Tests\Unit\Helpers; -use Mockery; use Tests\TestCase; use function Safe\json_decode; use App\Helpers\InstanceHelper; @@ -14,141 +13,6 @@ class InstanceHelperTest extends TestCase { use DatabaseTransactions; - /** @test */ - public function it_gets_the_number_of_paid_subscribers() - { - factory(Account::class)->create(['stripe_id' => 'id292839']); - factory(Account::class)->create(); - factory(Account::class)->create(['stripe_id' => 'id2sdf92839']); - - $this->assertEquals( - 2, - InstanceHelper::getNumberOfPaidSubscribers() - ); - } - - /** @test */ - public function it_fetches_the_monthly_plan_information() - { - config(['monica.paid_plan_monthly_friendly_name' => 'Monthly']); - config(['monica.paid_plan_monthly_id' => 'monthly']); - config(['monica.paid_plan_monthly_price' => 1000]); - - $this->assertEquals( - 'monthly', - InstanceHelper::getPlanInformationFromConfig('monthly')['type'] - ); - - $this->assertEquals( - 'Monthly', - InstanceHelper::getPlanInformationFromConfig('monthly')['name'] - ); - - $this->assertEquals( - 'monthly', - InstanceHelper::getPlanInformationFromConfig('monthly')['id'] - ); - - $this->assertEquals( - 1000, - InstanceHelper::getPlanInformationFromConfig('monthly')['price'] - ); - - $this->assertEquals( - '$10.00', - InstanceHelper::getPlanInformationFromConfig('monthly')['friendlyPrice'] - ); - } - - /** @test */ - public function it_fetches_the_annually_plan_information() - { - config(['monica.paid_plan_annual_friendly_name' => 'Annual']); - config(['monica.paid_plan_annual_id' => 'annual']); - config(['monica.paid_plan_annual_price' => 1000]); - - $this->assertEquals( - 'annual', - InstanceHelper::getPlanInformationFromConfig('annual')['type'] - ); - - $this->assertEquals( - 'Annual', - InstanceHelper::getPlanInformationFromConfig('annual')['name'] - ); - - $this->assertEquals( - 'annual', - InstanceHelper::getPlanInformationFromConfig('annual')['id'] - ); - - $this->assertEquals( - 1000, - InstanceHelper::getPlanInformationFromConfig('annual')['price'] - ); - - $this->assertEquals( - '$10.00', - InstanceHelper::getPlanInformationFromConfig('annual')['friendlyPrice'] - ); - } - - /** @test */ - public function it_fetches_subscription_information() - { - $stripeSubscription = (object) [ - 'plan' => (object) [ - 'currency' => 'USD', - 'amount' => 500, - 'interval' => 'month', - 'id' => 'monthly', - ], - 'current_period_end' => 1629976560, - ]; - - $subscription = Mockery::mock('\Laravel\Cashier\Subscription'); - $subscription->shouldReceive('asStripeSubscription') - ->andReturn($stripeSubscription); - $subscription->shouldReceive('getAttribute') - ->with('name') - ->andReturn('Monthly'); - - $this->assertEquals( - 'monthly', - InstanceHelper::getPlanInformationFromSubscription($subscription)['type'] - ); - - $this->assertEquals( - 'Monthly', - InstanceHelper::getPlanInformationFromSubscription($subscription)['name'] - ); - - $this->assertEquals( - 'monthly', - InstanceHelper::getPlanInformationFromSubscription($subscription)['id'] - ); - - $this->assertEquals( - 500, - InstanceHelper::getPlanInformationFromSubscription($subscription)['price'] - ); - - $this->assertEquals( - '$5.00', - InstanceHelper::getPlanInformationFromSubscription($subscription)['friendlyPrice'] - ); - } - - /** @test */ - public function it_returns_null_when_fetching_an_unknown_plan_information() - { - $account = new Account; - - $this->assertNull( - InstanceHelper::getPlanInformationFromConfig('unknown_plan') - ); - } - /** @test */ public function it_gets_latest_changelog_entries() { diff --git a/tests/Unit/Models/AccountTest.php b/tests/Unit/Models/AccountTest.php index db6652689ed..d67f8f02ce6 100644 --- a/tests/Unit/Models/AccountTest.php +++ b/tests/Unit/Models/AccountTest.php @@ -2,6 +2,7 @@ namespace Tests\Unit\Models; +use Carbon\Carbon; use App\Models\User\User; use Tests\FeatureTestCase; use App\Models\User\Module; @@ -276,6 +277,7 @@ public function user_is_subscribed_returns_false_if_not_subcribed() { $account = factory(Account::class)->make([ 'has_access_to_paid_version_for_free' => false, + 'licence_key' => null, ]); $this->assertFalse( @@ -284,126 +286,33 @@ public function user_is_subscribed_returns_false_if_not_subcribed() } /** @test */ - public function user_is_subscribed_returns_true_if_monthly_plan_is_set() + public function user_is_subscribed_returns_true_if_there_is_a_valid_licence_key() { - $account = factory(Account::class)->create(); - - $plan = factory(\Laravel\Cashier\Subscription::class)->create([ - 'account_id' => $account->id, - 'stripe_price' => 'chandler_5', - 'stripe_id' => 'sub_C0R444pbxddhW7', - 'name' => 'fakePlan', + Carbon::setTestNow(Carbon::create(2018, 1, 1)); + $account = factory(Account::class)->create([ + 'licence_key' => '123', + 'valid_until_at' => '2022-01-01', ]); - config(['monica.paid_plan_monthly_friendly_name' => 'fakePlan']); - $this->assertTrue( $account->isSubscribed() ); } /** @test */ - public function user_is_subscribed_returns_true_if_annual_plan_is_set() + public function user_is_subscribed_returns_false_if_there_is_a_valid_key_but_expired() { - $account = factory(Account::class)->create(); - - $plan = factory(\Laravel\Cashier\Subscription::class)->create([ - 'account_id' => $account->id, - 'stripe_price' => 'chandler_annual', - 'stripe_id' => 'sub_C0R444pbxddhW7', - 'name' => 'annualPlan', + Carbon::setTestNow(Carbon::create(2018, 1, 1)); + $account = factory(Account::class)->create([ + 'licence_key' => '123', + 'valid_until_at' => '1999-01-01', ]); - config(['monica.paid_plan_annual_friendly_name' => 'annualPlan']); - - $this->assertTrue( - $account->isSubscribed() - ); - } - - /** @test */ - public function user_is_subscribed_returns_false_if_no_plan_is_set() - { - $account = factory(Account::class)->create(); - $this->assertFalse( $account->isSubscribed() ); } - /** @test */ - public function has_invoices_returns_true_if_a_plan_exists() - { - $account = factory(Account::class)->create(); - - $plan = factory(\Laravel\Cashier\Subscription::class)->create([ - 'account_id' => $account->id, - 'stripe_price' => 'chandler_5', - 'stripe_id' => 'sub_C0R444pbxddhW7', - 'name' => 'fakePlan', - ]); - - $this->assertTrue($account->hasInvoices()); - } - - /** @test */ - public function has_invoices_returns_false_if_a_plan_does_not_exist() - { - $account = factory(Account::class)->create(); - - $this->assertFalse($account->hasInvoices()); - } - - /** @test */ - public function it_gets_the_id_of_the_subscribed_plan() - { - config([ - 'monica.paid_plan_annual_friendly_name' => 'fakePlan', - 'monica.paid_plan_annual_id' => 'chandler_5', - ]); - - $user = $this->signIn(); - - $account = $user->account; - - $plan = factory(\Laravel\Cashier\Subscription::class)->create([ - 'account_id' => $account->id, - 'stripe_price' => 'chandler_5', - 'stripe_id' => 'sub_C0R444pbxddhW7', - 'name' => 'fakePlan', - ]); - - $this->assertEquals( - 'chandler_5', - $account->getSubscribedPlanId() - ); - } - - /** @test */ - public function it_gets_the_friendly_name_of_the_subscribed_plan() - { - config([ - 'monica.paid_plan_annual_friendly_name' => 'fakePlan', - 'monica.paid_plan_annual_id' => 'chandler_5', - ]); - - $user = $this->signIn(); - - $account = $user->account; - - $plan = factory(\Laravel\Cashier\Subscription::class)->create([ - 'account_id' => $account->id, - 'stripe_price' => 'chandler_5', - 'stripe_id' => 'sub_C0R444pbxddhW7', - 'name' => 'fakePlan', - ]); - - $this->assertEquals( - 'fakePlan', - $account->getSubscribedPlanName() - ); - } - /** @test */ public function it_populates_the_account_with_three_default_genders() { diff --git a/tests/Unit/Services/Account/Subscription/ActivateLicenceKeyTest.php b/tests/Unit/Services/Account/Subscription/ActivateLicenceKeyTest.php new file mode 100644 index 00000000000..13b2173e04f --- /dev/null +++ b/tests/Unit/Services/Account/Subscription/ActivateLicenceKeyTest.php @@ -0,0 +1,170 @@ + 'base64:CiZYhXuxFaXsYWOTw8o6C82rqiZkphLg+N6fVep2l0M=']); + + $key = 'eyJpdiI6IjJ5clV4N3hlaThIMGtsckgiLCJ2YWx1ZSI6IkdIUWd4aFM4OWlHL2V3SHF0M1VOazVYTjBaN2c4RGpRUDZtN0VNejhGL0YzZGF6bmFBNnBzK3lUT0VVVXFIaE80SlZ3RmRRK3J4UTRBQU5XU2lUS3JhRHQ0d1paYUIrNGM0VUg2ZzBNU3Y4MjlzQ0d4N2pTZGlPY3E5UWFMRGJCSXdZSnN6a1MwYVg5RFBaQ01jMGtpMzhubnVFbmV5TXB3Zz09IiwibWFjIjoiIiwidGFnIjoiWVZlUjgrQU8yUlBCd1BaTDUxb1JJZz09In0='; + $account = factory(Account::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'licence_key' => $key, + ]; + + $this->mock(CustomerPortalCall::class, function (MockInterface $mock) use ($key) { + $mock->shouldReceive('execute') + ->once() + ->with(['licence_key' => $key]) + ->andReturn(200); + }); + + app(ActivateLicenceKey::class)->handle($request); + + $this->assertDatabaseHas('accounts', [ + 'id' => $account->id, + 'licence_key' => $key, + 'valid_until_at' => '2022-04-03', + 'purchaser_email' => 'admin@admin.com', + 'frequency' => 'monthly', + ]); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $request = []; + + $this->expectException(ValidationException::class); + app(ActivateLicenceKey::class)->handle($request); + } + + /** @test */ + public function it_fails_if_the_licence_key_is_empty() + { + $this->expectException(ValidationException::class); + + $account = factory(Account::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'licence_key' => '', + ]; + + app(ActivateLicenceKey::class)->handle($request); + } + + /** @test */ + public function it_fails_if_private_key_is_not_set() + { + config(['monica.licence_private_key' => null]); + + $this->expectException(MissingPrivateKeyException::class); + + $account = factory(Account::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'licence_key' => 'test', + ]; + + app(ActivateLicenceKey::class)->handle($request); + } + + /** @test */ + public function it_fails_if_the_licence_key_does_not_exist() + { + config(['monica.licence_private_key' => 'x']); + + $this->expectException(LicenceKeyDontExistException::class); + + $key = 'x'; + + $this->mock(CustomerPortalCall::class, function (MockInterface $mock) use ($key) { + $mock->shouldReceive('execute') + ->once() + ->with(['licence_key' => $key]) + ->andReturn(404); + }); + + $account = factory(Account::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'licence_key' => $key, + ]; + + app(ActivateLicenceKey::class)->handle($request); + } + + /** @test */ + public function it_fails_if_the_licence_key_is_not_valid_anymore() + { + config(['monica.licence_private_key' => 'x']); + + $this->expectException(LicenceKeyInvalidException::class); + + $key = 'x'; + + $this->mock(CustomerPortalCall::class, function (MockInterface $mock) use ($key) { + $mock->shouldReceive('execute') + ->once() + ->with(['licence_key' => $key]) + ->andReturn(410); + }); + + $account = factory(Account::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'licence_key' => $key, + ]; + + app(ActivateLicenceKey::class)->handle($request); + } + + /** @test */ + public function it_fails_if_there_is_an_error_during_validation() + { + config(['monica.licence_private_key' => 'x']); + + $this->expectException(LicenceKeyErrorException::class); + + $key = 'x'; + + $this->mock(CustomerPortalCall::class, function (MockInterface $mock) use ($key) { + $mock->shouldReceive('execute') + ->once() + ->with(['licence_key' => $key]) + ->andReturn(500); + }); + + $account = factory(Account::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'licence_key' => $key, + ]; + + app(ActivateLicenceKey::class)->handle($request); + } +} diff --git a/tests/Unit/Services/Account/Subscription/CustomerPortalCallTest.php b/tests/Unit/Services/Account/Subscription/CustomerPortalCallTest.php new file mode 100644 index 00000000000..7a0478a2c2d --- /dev/null +++ b/tests/Unit/Services/Account/Subscription/CustomerPortalCallTest.php @@ -0,0 +1,203 @@ + 'https://fake.test']); + config(['monica.customer_portal_client_id' => '1']); + config(['monica.customer_portal_client_secret' => '1']); + + Cache::flush(); + + Http::fake([ + 'https://fake.test/oauth/token' => Http::response(['access_token' => '123']), + 'https://fake.test/api/validate' => Http::response(['data' => 'ok'], 200), + ]); + + $request = [ + 'licence_key' => 'key', + ]; + + $response = app(CustomerPortalCall::class)->execute($request); + + $this->assertEquals(200, $response['status']); + $this->assertEquals(['data' => 'ok'], $response['data']); + + Http::assertSentInOrder([ + function ($request, $response) { + $this->assertEquals('POST', $request->method()); + $this->assertEquals('https://fake.test/oauth/token', $request->url()); + $this->assertEquals('grant_type=client_credentials&client_id=1&client_secret=1&scope=manage-key', $request->body()); + $this->assertEquals('{"access_token":"123"}', $response->body()); + + return true; + }, + function ($request, $response) { + $this->assertEquals('POST', $request->method()); + $this->assertEquals('https://fake.test/api/validate', $request->url()); + $this->assertEquals('{"licence_key":"key"}', $request->body()); + $this->assertEquals(['Bearer 123'], $request->header('Authorization')); + $this->assertEquals(200, $response->status()); + + return true; + }, + ]); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $request = []; + + $this->expectException(ValidationException::class); + app(CustomerPortalCall::class)->execute($request); + } + + /** @test */ + public function it_fails_if_the_licence_key_is_empty() + { + $this->expectException(ValidationException::class); + + $request = [ + 'licence_key' => '', + ]; + + app(CustomerPortalCall::class)->execute($request); + } + + /** @test */ + public function it_fails_if_the_customer_portal_is_not_set() + { + config(['monica.customer_portal_url' => '']); + config(['monica.customer_portal_client_id' => '1']); + config(['monica.customer_portal_client_secret' => '1']); + + $this->expectException(NoCustomerPortalSetException::class); + + $request = [ + 'licence_key' => 'test', + ]; + + app(CustomerPortalCall::class)->execute($request); + } + + /** @test */ + public function it_fails_if_the_client_id_is_not_set() + { + config(['monica.customer_portal_url' => 'https://fake.test']); + config(['monica.customer_portal_client_id' => null]); + config(['monica.customer_portal_client_secret' => '1']); + + $this->expectException(NoCustomerPortalSecretsException::class); + + $request = [ + 'licence_key' => 'test', + ]; + + app(CustomerPortalCall::class)->execute($request); + } + + /** @test */ + public function it_fails_if_the_client_secret_is_not_set() + { + config(['monica.customer_portal_url' => 'https://fake.test']); + config(['monica.customer_portal_client_id' => '1']); + config(['monica.customer_portal_client_secret' => null]); + + $this->expectException(NoCustomerPortalSecretsException::class); + + $request = [ + 'licence_key' => 'test', + ]; + + app(CustomerPortalCall::class)->execute($request); + } + + /** @test */ + public function it_stores_access_token_into_cache() + { + config(['monica.customer_portal_url' => 'https://fake.test']); + config(['monica.customer_portal_client_id' => '1']); + config(['monica.customer_portal_client_secret' => '1']); + + Cache::flush(); + + $this->assertFalse(Cache::has('customer_portal.access_token')); + + Http::fake([ + 'https://fake.test/oauth/token' => Http::response(['access_token' => '123']), + 'https://fake.test/api/validate' => Http::response([], 200), + ]); + + $request = [ + 'licence_key' => 'key', + ]; + + app(CustomerPortalCall::class)->execute($request); + + $this->assertTrue(Cache::has('customer_portal.access_token')); + } + + /** @test */ + public function it_throw_an_exception_if_no_access_token() + { + config(['monica.customer_portal_url' => 'https://fake.test']); + config(['monica.customer_portal_client_id' => '1']); + config(['monica.customer_portal_client_secret' => '1']); + + Cache::flush(); + + $this->assertFalse(Cache::has('customer_portal.access_token')); + + Http::fake([ + 'https://fake.test/oauth/token' => Http::response([]), + 'https://fake.test/api/validate' => Http::response([], 200), + ]); + + $request = [ + 'licence_key' => 'key', + ]; + + $this->expectException(CustomerPortalWrongCredentials::class); + app(CustomerPortalCall::class)->execute($request); + } + + /** @test */ + public function it_throw_an_exception_if_oauth_token_send_bad_status() + { + config(['monica.customer_portal_url' => 'https://fake.test']); + config(['monica.customer_portal_client_id' => '1']); + config(['monica.customer_portal_client_secret' => '1']); + + Cache::flush(); + + $this->assertFalse(Cache::has('customer_portal.access_token')); + + Http::fake([ + 'https://fake.test/oauth/token' => Http::response([], 500), + ]); + + $request = [ + 'licence_key' => 'key', + ]; + + $this->expectException(CustomerPortalWrongCredentials::class); + app(CustomerPortalCall::class)->execute($request); + } +} diff --git a/webpack.mix.js b/webpack.mix.js index 990acb0315a..c1c35a5de08 100644 --- a/webpack.mix.js +++ b/webpack.mix.js @@ -44,10 +44,6 @@ mix.js('resources/js/app.js', 'public/js').vue() .sass('resources/sass/app-ltr.scss', 'public/css') .sass('resources/sass/app-rtl.scss', 'public/css') - // stripe - .js('resources/js/stripe.js', 'public/js') - .sass('resources/sass/stripe.scss', 'public/css') - .alias({ vue$: path.join(__dirname, 'node_modules/vue/dist/vue.esm.js'), })