1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-15 07:33:04 +01:00
invoiceninja/app/Livewire/BillingPortalPurchase.php

607 lines
17 KiB
PHP
Raw Normal View History

2021-03-18 01:58:10 +01:00
<?php
2021-03-18 01:53:08 +01:00
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
2024-04-12 06:15:41 +02:00
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
2021-03-18 01:53:08 +01:00
*
2021-06-16 08:58:16 +02:00
* @license https://www.elastic.co/licensing/elastic-license
2021-03-18 01:53:08 +01:00
*/
2021-03-18 01:58:10 +01:00
2023-12-13 17:52:49 +01:00
namespace App\Livewire;
2021-03-18 01:58:10 +01:00
2023-03-18 08:24:56 +01:00
use App\DataMapper\ClientSettings;
2023-03-09 23:22:16 +01:00
use App\Factory\ClientFactory;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
2023-03-18 08:24:56 +01:00
use App\Libraries\MultiDB;
2023-03-09 23:22:16 +01:00
use App\Mail\ContactPasswordlessLogin;
2023-03-18 08:24:56 +01:00
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Invoice;
use App\Models\Subscription;
2023-03-09 23:22:16 +01:00
use App\Repositories\ClientContactRepository;
2023-03-18 08:24:56 +01:00
use App\Repositories\ClientRepository;
use App\Utils\Ninja;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use Livewire\Component;
2021-03-18 01:58:10 +01:00
class BillingPortalPurchase extends Component
{
2021-03-18 14:14:10 +01:00
/**
* Random hash generated by backend to handle the tracking of state.
*
* @var string
*/
2021-03-18 01:58:10 +01:00
public $hash;
2021-03-18 14:14:10 +01:00
/**
* Top level text on the left side of billing page.
*
* @var string
*/
public $heading_text;
2021-03-18 01:58:10 +01:00
2021-03-18 14:14:10 +01:00
/**
* E-mail address model for user input.
*
* @var string
*/
2021-03-18 01:58:10 +01:00
public $email;
2021-03-18 14:14:10 +01:00
/**
* Password model for user input.
*
* @var string
*/
2021-03-18 01:58:10 +01:00
public $password;
2021-03-18 14:14:10 +01:00
/**
2023-08-13 23:30:38 +02:00
* This arrives as an int and we resolve in the mount method
2021-03-18 14:14:10 +01:00
*
2023-08-13 23:30:38 +02:00
* @var int|Subscription
2021-03-18 14:14:10 +01:00
*/
2023-08-13 23:30:38 +02:00
public $subscription;
2021-03-18 01:58:10 +01:00
2021-03-18 14:14:10 +01:00
/**
* Instance of client contact.
*
* @var null|ClientContact
*/
2021-03-18 01:58:10 +01:00
public $contact;
2021-03-18 14:14:10 +01:00
/**
* Rules for validating the form.
*
*/
2021-03-18 01:58:10 +01:00
protected $rules = [
'email' => ['required', 'email'],
];
2021-03-18 14:14:10 +01:00
/**
* Id for CompanyGateway record.
*
* @var string|integer
*/
2021-03-18 01:58:10 +01:00
public $company_gateway_id;
2021-03-18 14:14:10 +01:00
/**
* Id for GatewayType.
*
* @var string|integer
*/
2021-03-18 01:58:10 +01:00
public $payment_method_id;
2022-08-10 05:35:47 +02:00
private $user_coupon;
2021-03-18 14:14:10 +01:00
/**
* List of steps that frontend form follows.
*
* @var array
*/
2021-03-18 01:58:10 +01:00
public $steps = [
'passed_email' => false,
'existing_user' => false,
'check_rff' => false,
2021-03-18 01:58:10 +01:00
'fetched_payment_methods' => false,
'fetched_client' => false,
2021-03-18 14:14:10 +01:00
'show_start_trial' => false,
2021-03-31 18:10:44 +02:00
'passwordless_login_sent' => false,
'started_payment' => false,
2021-04-01 17:02:11 +02:00
'discount_applied' => false,
'show_loading_bar' => false,
'not_eligible' => null,
2021-04-10 13:56:29 +02:00
'not_eligible_message' => null,
2021-04-14 06:41:04 +02:00
'payment_required' => true,
2021-03-18 01:58:10 +01:00
];
2021-03-18 14:14:10 +01:00
/**
* List of payment methods fetched from client.
*
* @var array
*/
2021-03-18 01:58:10 +01:00
public $methods = [];
2021-03-18 14:14:10 +01:00
/**
* Instance of \App\Models\Invoice
*
* @var Invoice
*/
2021-03-18 01:58:10 +01:00
public $invoice;
2021-03-18 14:14:10 +01:00
/**
* Coupon model for user input
*
* @var string
*/
2021-03-18 01:58:10 +01:00
public $coupon;
2021-03-18 15:22:22 +01:00
/**
* Quantity for seats
*
* @var int
*/
2022-07-06 08:18:16 +02:00
public $quantity;
2021-03-18 15:22:22 +01:00
2021-03-18 15:41:26 +01:00
/**
* First-hit request data (queries, locales...).
*
* @var array
*/
public $request_data;
2021-03-29 15:50:36 +02:00
/**
2023-08-08 11:44:52 +02:00
* @var float
2021-03-29 15:50:36 +02:00
*/
public $price;
2021-03-31 18:10:44 +02:00
/**
* Disabled state of passwordless login button.
*
* @var bool
*/
public $passwordless_login_btn = false;
2021-06-03 01:50:31 +02:00
/**
* Instance of company.
*
2023-03-09 23:22:16 +01:00
* @var \App\Models\Company
2021-06-03 01:50:31 +02:00
*/
2021-06-03 03:43:14 +02:00
public $company;
2021-06-03 01:50:31 +02:00
2022-12-22 06:44:36 +01:00
public $db;
/**
* Campaign reference.
*
* @var string|null
*/
public $campaign;
public ?string $contact_first_name;
public ?string $contact_last_name;
public ?string $contact_email;
2024-07-28 00:51:24 +02:00
public ?string $client_city;
public ?string $client_postal_code;
2021-03-29 15:50:36 +02:00
public function mount()
{
2022-12-22 06:44:36 +01:00
MultiDB::setDb($this->db);
2023-08-07 06:33:47 +02:00
$this->subscription = Subscription::query()->with('company')->find($this->subscription);
2022-12-22 06:53:48 +01:00
2022-12-22 06:44:36 +01:00
$this->company = $this->subscription->company;
2021-06-03 01:50:31 +02:00
2022-07-06 08:18:16 +02:00
$this->quantity = 1;
$this->price = $this->subscription->price;
2021-04-01 16:20:31 +02:00
if (request()->query('coupon')) {
$this->coupon = request()->query('coupon');
$this->handleCoupon();
2024-07-28 00:51:24 +02:00
} elseif (strlen($this->subscription->promo_code ?? '') == 0 && $this->subscription->promo_discount > 0) {
2021-11-17 19:53:01 +01:00
$this->price = $this->subscription->promo_price;
}
2023-01-15 07:40:11 +01:00
/* Leave this here, otherwise a logged in user will need to reauth... painfully */
2023-02-16 02:36:09 +01:00
if (Auth::guard('contact')->check()) {
2023-01-15 07:40:11 +01:00
return $this->getPaymentMethods(auth()->guard('contact')->user());
}
2021-03-29 15:50:36 +02:00
}
2021-03-18 14:14:10 +01:00
/**
* Handle user authentication
*
* @return $this|bool|void
*/
2021-03-18 01:58:10 +01:00
public function authenticate()
{
$this->validate();
2021-05-24 00:25:14 +02:00
$contact = ClientContact::where('email', $this->email)
->where('company_id', $this->subscription->company_id)
->first();
2021-03-18 01:58:10 +01:00
if ($contact && $this->steps['existing_user'] === false) {
return $this->steps['existing_user'] = true;
}
if ($contact && $this->steps['existing_user']) {
2021-05-24 00:25:14 +02:00
$attempt = Auth::guard('contact')->attempt(['email' => $this->email, 'password' => $this->password, 'company_id' => $this->subscription->company_id]);
2021-03-18 01:58:10 +01:00
return $attempt
? $this->getPaymentMethods($contact)
: session()->flash('message', 'These credentials do not match our records.');
}
$this->steps['existing_user'] = false;
2022-12-22 06:53:48 +01:00
$this->contact = $this->createBlankClient();
2021-03-18 01:58:10 +01:00
2022-12-22 06:53:48 +01:00
if ($this->contact && $this->contact instanceof ClientContact) {
$this->getPaymentMethods($this->contact);
2021-03-18 01:58:10 +01:00
}
}
2021-03-18 14:14:10 +01:00
/**
* Create a blank client. Used for new customers purchasing.
*
* @return mixed
* @throws \Laracasts\Presenter\Exceptions\PresenterException
*/
2021-03-18 01:58:10 +01:00
protected function createBlankClient()
{
$company = $this->subscription->company;
$user = $this->subscription->user;
2022-03-29 23:05:42 +02:00
$user->setCompany($company);
2023-12-15 02:29:47 +01:00
2021-03-18 01:58:10 +01:00
$client_repo = new ClientRepository(new ClientContactRepository());
$data = [
2021-06-29 11:46:40 +02:00
'name' => '',
2021-03-18 01:58:10 +01:00
'contacts' => [
['email' => $this->email],
],
2021-11-08 07:35:15 +01:00
'client_hash' => Str::random(40),
'settings' => ClientSettings::defaults(),
];
foreach ($this->request_data as $field => $value) {
if (in_array($field, Client::$subscriptions_fillable)) {
$data[$field] = $value;
}
if (in_array($field, ClientContact::$subscription_fillable)) {
$data['contacts'][0][$field] = $value;
}
}
2023-02-16 02:36:09 +01:00
if (array_key_exists('currency_id', $this->request_data)) {
2024-08-22 08:45:06 +02:00
/** @var \Illuminate\Support\Collection<\App\Models\Currency> */
$currencies = app('currencies');
$currency = $currencies->first(function ($item) {
return $item->id == $this->request_data['currency_id'];
});
2023-02-16 02:36:09 +01:00
if ($currency) {
$data['settings']->currency_id = $currency->id;
2023-02-16 02:36:09 +01:00
}
} elseif ($this->subscription->group_settings && property_exists($this->subscription->group_settings->settings, 'currency_id')) {
/** @var \Illuminate\Support\Collection<\App\Models\Currency> */
$currencies = app('currencies');
$currency = $currencies->first(function ($item) {
2022-08-15 01:24:47 +02:00
return $item->id == $this->subscription->group_settings->settings->currency_id;
});
2022-08-15 01:24:47 +02:00
2023-02-16 02:36:09 +01:00
if ($currency) {
2022-08-15 01:24:47 +02:00
$data['settings']->currency_id = $currency->id;
2023-02-16 02:36:09 +01:00
}
}
if (array_key_exists('locale', $this->request_data)) {
$request = $this->request_data;
2024-08-22 08:45:06 +02:00
/** @var \Illuminate\Support\Collection<\App\Models\Language> */
$languages = app('languages');
$record = $languages->first(function ($item) use ($request) {
return $item->locale == $request['locale'];
});
if ($record) {
$data['settings']['language_id'] = (string)$record->id;
}
}
$client = $client_repo->save($data, ClientFactory::create($company->id, $user->id));
2021-03-18 01:58:10 +01:00
2021-10-13 00:10:44 +02:00
return $client->fresh()->contacts->first();
2021-03-18 01:58:10 +01:00
}
2021-03-18 14:14:10 +01:00
/**
* Fetching payment methods from the client.
*
* @param ClientContact $contact
* @return $this
*/
2021-03-18 01:58:10 +01:00
protected function getPaymentMethods(ClientContact $contact): self
{
2021-03-29 12:25:29 +02:00
$this->contact = $contact;
Auth::guard('contact')->loginUsingId($contact->id, true);
if ($this->subscription->trial_enabled) {
2021-03-18 14:14:10 +01:00
$this->heading_text = ctrans('texts.plan_trial');
$this->steps['show_start_trial'] = true;
return $this;
}
2023-02-16 02:36:09 +01:00
if ((int)$this->price == 0) {
2021-04-14 06:41:04 +02:00
$this->steps['payment_required'] = false;
$this->steps['fetched_payment_methods'] = false;
$this->heading_text = ctrans('texts.payment_methods');
return $this;
2023-02-16 02:36:09 +01:00
} else {
2024-07-28 00:51:24 +02:00
// $this->steps['fetched_payment_methods'] = true;
2023-02-16 02:36:09 +01:00
}
2021-03-18 01:58:10 +01:00
2021-04-07 18:08:26 +02:00
$this->methods = $contact->client->service()->getPaymentMethods($this->price);
2021-03-18 01:58:10 +01:00
2024-08-06 10:57:19 +02:00
$method_values = array_column($this->methods, 'is_paypal');
$is_paypal = in_array('1', $method_values);
2024-08-22 08:45:06 +02:00
if($is_paypal && !$this->steps['check_rff']) {
2024-08-06 10:57:19 +02:00
$this->rff();
2024-08-22 08:45:06 +02:00
} elseif(!$this->steps['check_rff']) {
2024-08-06 10:57:19 +02:00
$this->steps['fetched_payment_methods'] = true;
2024-08-22 08:45:06 +02:00
}
2021-03-18 14:14:10 +01:00
$this->heading_text = ctrans('texts.payment_methods');
2021-03-18 01:58:10 +01:00
return $this;
}
protected function rff()
{
$this->contact_first_name = $this->contact->first_name;
$this->contact_last_name = $this->contact->last_name;
$this->contact_email = $this->contact->email;
2024-07-28 00:51:24 +02:00
$this->client_city = $this->contact->client->city;
$this->client_postal_code = $this->contact->client->postal_code;
$this->steps['check_rff'] = true;
return $this;
}
public function handleRff()
{
$validated = $this->validate([
'contact_first_name' => ['required'],
'contact_last_name' => ['required'],
2024-07-28 00:51:24 +02:00
'client_city' => ['required'],
'client_postal_code' => ['required'],
'contact_email' => ['required', 'email'],
]);
$this->contact->first_name = $validated['contact_first_name'];
$this->contact->last_name = $validated['contact_last_name'];
$this->contact->email = $validated['contact_email'];
2024-07-28 00:51:24 +02:00
$this->contact->client->postal_code = $validated['client_postal_code'];
$this->contact->client->city = $validated['client_city'];
$this->contact->pushQuietly();
$this->steps['fetched_payment_methods'] = true;
return $this->getPaymentMethods($this->contact);
}
2021-03-18 14:14:10 +01:00
/**
* Middle method between selecting payment method &
* submitting the from to the backend.
*
* @param $company_gateway_id
* @param $gateway_type_id
*/
2024-07-28 00:51:24 +02:00
public function handleMethodSelectingEvent($company_gateway_id, $gateway_type_id, $is_paypal = false)
2021-03-18 01:58:10 +01:00
{
$this->company_gateway_id = $company_gateway_id;
$this->payment_method_id = $gateway_type_id;
$this->handleBeforePaymentEvents();
2024-08-22 08:45:06 +02:00
2021-03-18 01:58:10 +01:00
}
2021-03-18 14:14:10 +01:00
/**
* Method to handle events before payments.
*
* @return void
*/
2021-03-18 01:58:10 +01:00
public function handleBeforePaymentEvents()
{
$this->steps['started_payment'] = true;
$this->steps['show_loading_bar'] = true;
2021-03-18 01:58:10 +01:00
$data = [
'client_id' => $this->contact->client->id,
'date' => now()->format('Y-m-d'),
'invitations' => [[
'key' => '',
'client_contact_id' => $this->contact->hashed_id,
]],
'user_input_promo_code' => $this->coupon,
'coupon' => empty($this->subscription->promo_code) ? '' : $this->coupon,
2021-03-18 15:22:22 +01:00
'quantity' => $this->quantity,
2021-03-18 01:58:10 +01:00
];
$is_eligible = $this->subscription->service()->isEligible($this->contact);
2021-06-03 23:57:24 +02:00
if (is_array($is_eligible) && $is_eligible['message'] != 'Success') {
$this->steps['not_eligible'] = true;
2021-06-03 23:57:24 +02:00
$this->steps['not_eligible_message'] = $is_eligible['message'];
$this->steps['show_loading_bar'] = false;
return;
}
2021-11-25 01:16:47 +01:00
$this->invoice = $this->subscription
->service()
2022-07-06 09:12:29 +02:00
->createInvoice($data, $this->quantity)
2021-11-25 01:16:47 +01:00
->service()
->markSent()
->fillDefaults()
2022-06-08 12:40:26 +02:00
->adjustInventory()
2021-11-25 01:16:47 +01:00
->save();
2023-03-09 23:22:16 +01:00
$context = 'purchase';
2023-03-18 08:24:56 +01:00
if (Ninja::isHosted() && $this->subscription->service()->recurring_products()->first()?->product_key == 'whitelabel') {
2023-03-09 23:22:16 +01:00
$context = 'whitelabel';
}
2021-03-18 01:58:10 +01:00
Cache::put($this->hash, [
'subscription_id' => $this->subscription->hashed_id,
'email' => $this->email ?? $this->contact->email,
'client_id' => $this->contact->client->hashed_id,
'invoice_id' => $this->invoice->hashed_id,
2023-03-09 23:22:16 +01:00
'context' => $context,
'campaign' => $this->campaign,
], now()->addMinutes(60));
2021-03-18 01:58:10 +01:00
2023-12-13 17:52:49 +01:00
$this->dispatch('beforePaymentEventsCompleted');
2021-03-18 01:58:10 +01:00
}
2021-03-18 14:14:10 +01:00
/**
* Proxy method for starting the trial.
*
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function handleTrial()
{
return $this->subscription->service()->startTrial([
2021-03-18 14:14:10 +01:00
'email' => $this->email ?? $this->contact->email,
2021-03-22 14:03:32 +01:00
'quantity' => $this->quantity,
2023-10-13 23:34:34 +02:00
'contact_id' => $this->contact->hashed_id,
'client_id' => $this->contact->client->hashed_id,
2021-04-14 06:41:04 +02:00
]);
}
public function handlePaymentNotRequired()
{
$is_eligible = $this->subscription->service()->isEligible($this->contact);
2023-12-15 02:29:47 +01:00
2021-06-03 23:43:22 +02:00
if ($is_eligible['status_code'] != 200) {
2021-04-14 06:41:04 +02:00
$this->steps['not_eligible'] = true;
2021-06-03 23:43:22 +02:00
$this->steps['not_eligible_message'] = $is_eligible['message'];
2021-04-14 06:41:04 +02:00
$this->steps['show_loading_bar'] = false;
return;
}
return $this->subscription->service()->handleNoPaymentRequired([
'email' => $this->email ?? $this->contact->email,
'quantity' => $this->quantity,
'contact_id' => $this->contact->id,
'client_id' => $this->contact->client->id,
'coupon' => $this->coupon,
2023-04-29 08:04:42 +02:00
'campaign' => $this->campaign,
2021-03-18 14:14:10 +01:00
]);
}
2021-03-18 02:00:01 +01:00
2021-03-18 15:22:22 +01:00
/**
* Update quantity property.
*
* @param string $option
* @return int
*/
public function updateQuantity(string $option): int
2021-03-18 01:58:10 +01:00
{
2022-08-10 05:35:47 +02:00
$this->handleCoupon();
2021-03-18 15:22:22 +01:00
if ($this->quantity == 1 && $option == 'decrement') {
2022-08-10 05:35:47 +02:00
$this->price = $this->price * 1;
2021-03-18 15:22:22 +01:00
return $this->quantity;
}
2022-08-10 05:35:47 +02:00
if ($this->quantity > $this->subscription->max_seats_limit && $option == 'increment') {
$this->price = $this->price * $this->subscription->max_seats_limit;
2021-03-18 15:22:22 +01:00
return $this->quantity;
}
2021-03-22 13:53:34 +01:00
if ($option == 'increment') {
$this->quantity++;
2022-08-10 05:35:47 +02:00
$this->price = $this->price * $this->quantity;
2022-07-06 08:18:16 +02:00
return $this->quantity;
2021-03-22 13:53:34 +01:00
}
2023-02-16 02:36:09 +01:00
$this->quantity--;
$this->price = $this->price * $this->quantity;
2021-03-22 13:53:34 +01:00
2023-02-16 02:36:09 +01:00
return $this->quantity;
2021-03-18 01:58:10 +01:00
}
2021-03-29 15:50:36 +02:00
public function handleCoupon()
{
2023-02-16 02:36:09 +01:00
if ($this->steps['discount_applied']) {
2022-08-10 05:35:47 +02:00
$this->price = $this->subscription->promo_price;
return;
}
2021-03-29 15:50:36 +02:00
if ($this->coupon == $this->subscription->promo_code) {
$this->price = $this->subscription->promo_price;
2022-08-10 05:35:47 +02:00
$this->quantity = 1;
2021-04-01 17:02:11 +02:00
$this->steps['discount_applied'] = true;
2023-02-16 02:36:09 +01:00
} else {
2022-08-10 05:35:47 +02:00
$this->price = $this->subscription->price;
2023-02-16 02:36:09 +01:00
}
2021-03-29 15:50:36 +02:00
}
2021-03-31 18:10:44 +02:00
public function passwordlessLogin()
{
$this->passwordless_login_btn = true;
$contact = ClientContact::query()
->where('email', $this->email)
2022-01-24 00:29:47 +01:00
->where('company_id', $this->subscription->company_id)
2021-03-31 18:10:44 +02:00
->first();
$mailer = new NinjaMailerObject();
2021-06-09 17:07:41 +02:00
$mailer->mailable = new ContactPasswordlessLogin($this->email, $this->subscription->company, (string)route('client.subscription.purchase', $this->subscription->hashed_id) . '?coupon=' . $this->coupon);
2021-03-31 18:10:44 +02:00
$mailer->company = $this->subscription->company;
$mailer->settings = $this->subscription->company->settings;
$mailer->to_user = $contact;
2022-09-07 06:15:27 +02:00
NinjaMailerJob::dispatch($mailer);
2021-03-31 18:10:44 +02:00
$this->steps['passwordless_login_sent'] = true;
$this->passwordless_login_btn = false;
}
2021-03-18 01:58:10 +01:00
public function render()
{
if (array_key_exists('email', $this->request_data)) {
$this->email = $this->request_data['email'];
}
2021-03-18 01:58:10 +01:00
if ($this->contact instanceof ClientContact) {
$this->getPaymentMethods($this->contact);
}
return render('components.livewire.billing-portal-purchase');
}
}