mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-05-24 02:14:21 -04:00
Merge branch 'v5-develop' into bank_rules
This commit is contained in:
commit
428f42c723
@ -42,7 +42,7 @@ class S3Cleanup extends Command
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if (! Ninja::isHosted()) {
|
||||
if (!Ninja::isHosted()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -41,6 +41,25 @@ class SubscriptionPurchaseController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function upgrade(Subscription $subscription, Request $request)
|
||||
{
|
||||
/* Make sure the contact is logged into the correct company for this subscription */
|
||||
if (auth()->guard('contact')->user() && auth()->guard('contact')->user()->company_id != $subscription->company_id) {
|
||||
auth()->guard('contact')->logout();
|
||||
$request->session()->invalidate();
|
||||
}
|
||||
|
||||
if ($request->has('locale')) {
|
||||
$this->setLocale($request->query('locale'));
|
||||
}
|
||||
|
||||
return view('billing-portal.purchasev2', [
|
||||
'subscription' => $subscription,
|
||||
'hash' => Str::uuid()->toString(),
|
||||
'request_data' => $request->all(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set locale for incoming request.
|
||||
*
|
||||
@ -56,4 +75,7 @@ class SubscriptionPurchaseController extends Controller
|
||||
App::setLocale($record->locale);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ use App\Http\Requests\Invoice\StoreInvoiceRequest;
|
||||
use App\Http\Requests\Invoice\UpdateInvoiceRequest;
|
||||
use App\Http\Requests\Invoice\UpdateReminderRequest;
|
||||
use App\Http\Requests\Invoice\UploadInvoiceRequest;
|
||||
use App\Jobs\Cron\AutoBill;
|
||||
use App\Jobs\Entity\EmailEntity;
|
||||
use App\Jobs\Invoice\BulkInvoiceJob;
|
||||
use App\Jobs\Invoice\StoreInvoice;
|
||||
@ -696,11 +697,14 @@ class InvoiceController extends BaseController
|
||||
{
|
||||
/*If we are using bulk actions, we don't want to return anything */
|
||||
switch ($action) {
|
||||
case 'auto_bill':
|
||||
$invoice = AutoBill::dispatch($invoice->id, $invoice->company->db);
|
||||
return $this->itemResponse($invoice);
|
||||
|
||||
case 'clone_to_invoice':
|
||||
$invoice = CloneInvoiceFactory::create($invoice, auth()->user()->id);
|
||||
|
||||
return $this->itemResponse($invoice);
|
||||
break;
|
||||
|
||||
case 'clone_to_quote':
|
||||
$quote = CloneInvoiceToQuoteFactory::create($invoice, auth()->user()->id);
|
||||
|
||||
|
528
app/Http/Livewire/BillingPortalPurchasev2.php
Normal file
528
app/Http/Livewire/BillingPortalPurchasev2.php
Normal file
@ -0,0 +1,528 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Http\Livewire;
|
||||
|
||||
use App\Factory\ClientFactory;
|
||||
use App\Jobs\Mail\NinjaMailerJob;
|
||||
use App\Jobs\Mail\NinjaMailerObject;
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Mail\ContactPasswordlessLogin;
|
||||
use App\Models\Client;
|
||||
use App\Models\ClientContact;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Subscription;
|
||||
use App\Repositories\ClientContactRepository;
|
||||
use App\Repositories\ClientRepository;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use App\DataMapper\ClientSettings;
|
||||
use Livewire\Component;
|
||||
|
||||
class BillingPortalPurchasev2 extends Component
|
||||
{
|
||||
/**
|
||||
* Random hash generated by backend to handle the tracking of state.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $hash;
|
||||
|
||||
/**
|
||||
* Top level text on the left side of billing page.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $heading_text;
|
||||
|
||||
|
||||
/**
|
||||
* E-mail address model for user input.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $email;
|
||||
|
||||
/**
|
||||
* Password model for user input.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $password;
|
||||
|
||||
/**
|
||||
* Instance of subscription.
|
||||
*
|
||||
* @var Subscription
|
||||
*/
|
||||
public $subscription;
|
||||
|
||||
/**
|
||||
* Instance of client contact.
|
||||
*
|
||||
* @var null|ClientContact
|
||||
*/
|
||||
public $contact;
|
||||
|
||||
/**
|
||||
* Rules for validating the form.
|
||||
*
|
||||
* @var \string[][]
|
||||
*/
|
||||
protected $rules = [
|
||||
'email' => ['required', 'email'],
|
||||
];
|
||||
|
||||
/**
|
||||
* Id for CompanyGateway record.
|
||||
*
|
||||
* @var string|integer
|
||||
*/
|
||||
public $company_gateway_id;
|
||||
|
||||
/**
|
||||
* Id for GatewayType.
|
||||
*
|
||||
* @var string|integer
|
||||
*/
|
||||
public $payment_method_id;
|
||||
|
||||
private $user_coupon;
|
||||
|
||||
/**
|
||||
* List of steps that frontend form follows.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $steps = [
|
||||
'passed_email' => false,
|
||||
'existing_user' => false,
|
||||
'fetched_payment_methods' => false,
|
||||
'fetched_client' => false,
|
||||
'show_start_trial' => false,
|
||||
'passwordless_login_sent' => false,
|
||||
'started_payment' => false,
|
||||
'discount_applied' => false,
|
||||
'show_loading_bar' => false,
|
||||
'not_eligible' => null,
|
||||
'not_eligible_message' => null,
|
||||
'payment_required' => true,
|
||||
];
|
||||
|
||||
/**
|
||||
* List of payment methods fetched from client.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $methods = [];
|
||||
|
||||
/**
|
||||
* Instance of \App\Models\Invoice
|
||||
*
|
||||
* @var Invoice
|
||||
*/
|
||||
public $invoice;
|
||||
|
||||
/**
|
||||
* Coupon model for user input
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $coupon;
|
||||
|
||||
/**
|
||||
* Quantity for seats
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $quantity;
|
||||
|
||||
/**
|
||||
* First-hit request data (queries, locales...).
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $request_data;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $price;
|
||||
|
||||
/**
|
||||
* Disabled state of passwordless login button.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $passwordless_login_btn = false;
|
||||
|
||||
/**
|
||||
* Instance of company.
|
||||
*
|
||||
* @var Company
|
||||
*/
|
||||
public $company;
|
||||
|
||||
/**
|
||||
* Campaign reference.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $campaign;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
MultiDB::setDb($this->company->db);
|
||||
|
||||
$this->quantity = 1;
|
||||
|
||||
$this->price = $this->subscription->price;
|
||||
|
||||
if (request()->query('coupon')) {
|
||||
$this->coupon = request()->query('coupon');
|
||||
$this->handleCoupon();
|
||||
}
|
||||
elseif(strlen($this->subscription->promo_code) == 0 && $this->subscription->promo_discount > 0){
|
||||
$this->price = $this->subscription->promo_price;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle user authentication
|
||||
*
|
||||
* @return $this|bool|void
|
||||
*/
|
||||
public function authenticate()
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
$contact = ClientContact::where('email', $this->email)
|
||||
->where('company_id', $this->subscription->company_id)
|
||||
->first();
|
||||
|
||||
if ($contact && $this->steps['existing_user'] === false) {
|
||||
return $this->steps['existing_user'] = true;
|
||||
}
|
||||
|
||||
if ($contact && $this->steps['existing_user']) {
|
||||
$attempt = Auth::guard('contact')->attempt(['email' => $this->email, 'password' => $this->password, 'company_id' => $this->subscription->company_id]);
|
||||
|
||||
return $attempt
|
||||
? $this->getPaymentMethods($contact)
|
||||
: session()->flash('message', 'These credentials do not match our records.');
|
||||
}
|
||||
|
||||
$this->steps['existing_user'] = false;
|
||||
|
||||
$contact = $this->createBlankClient();
|
||||
|
||||
if ($contact && $contact instanceof ClientContact) {
|
||||
$this->getPaymentMethods($contact);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a blank client. Used for new customers purchasing.
|
||||
*
|
||||
* @return mixed
|
||||
* @throws \Laracasts\Presenter\Exceptions\PresenterException
|
||||
*/
|
||||
protected function createBlankClient()
|
||||
{
|
||||
$company = $this->subscription->company;
|
||||
$user = $this->subscription->user;
|
||||
$user->setCompany($company);
|
||||
|
||||
$client_repo = new ClientRepository(new ClientContactRepository());
|
||||
|
||||
$data = [
|
||||
'name' => '',
|
||||
'contacts' => [
|
||||
['email' => $this->email],
|
||||
],
|
||||
'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;
|
||||
}
|
||||
}
|
||||
|
||||
// nlog($this->subscription->group_settings->settings);
|
||||
// nlog($this->subscription->group_settings->settings->currency_id);
|
||||
|
||||
if(array_key_exists('currency_id', $this->request_data)) {
|
||||
|
||||
$currency = Cache::get('currencies')->filter(function ($item){
|
||||
return $item->id == $this->request_data['currency_id'];
|
||||
})->first();
|
||||
|
||||
if($currency)
|
||||
$data['settings']->currency_id = $currency->id;
|
||||
|
||||
}
|
||||
elseif($this->subscription->group_settings && property_exists($this->subscription->group_settings->settings, 'currency_id')) {
|
||||
|
||||
$currency = Cache::get('currencies')->filter(function ($item){
|
||||
return $item->id == $this->subscription->group_settings->settings->currency_id;
|
||||
})->first();
|
||||
|
||||
if($currency)
|
||||
$data['settings']->currency_id = $currency->id;
|
||||
|
||||
}
|
||||
|
||||
if (array_key_exists('locale', $this->request_data)) {
|
||||
$request = $this->request_data;
|
||||
|
||||
$record = Cache::get('languages')->filter(function ($item) use ($request) {
|
||||
return $item->locale == $request['locale'];
|
||||
})->first();
|
||||
|
||||
if ($record) {
|
||||
$data['settings']['language_id'] = (string)$record->id;
|
||||
}
|
||||
}
|
||||
|
||||
$client = $client_repo->save($data, ClientFactory::create($company->id, $user->id));
|
||||
|
||||
return $client->fresh()->contacts->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetching payment methods from the client.
|
||||
*
|
||||
* @param ClientContact $contact
|
||||
* @return $this
|
||||
*/
|
||||
protected function getPaymentMethods(ClientContact $contact): self
|
||||
{
|
||||
Auth::guard('contact')->loginUsingId($contact->id, true);
|
||||
|
||||
$this->contact = $contact;
|
||||
|
||||
if ($this->subscription->trial_enabled) {
|
||||
$this->heading_text = ctrans('texts.plan_trial');
|
||||
$this->steps['show_start_trial'] = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ((int)$this->price == 0)
|
||||
$this->steps['payment_required'] = false;
|
||||
else
|
||||
$this->steps['fetched_payment_methods'] = true;
|
||||
|
||||
$this->methods = $contact->client->service()->getPaymentMethods($this->price);
|
||||
|
||||
$this->heading_text = ctrans('texts.payment_methods');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Middle method between selecting payment method &
|
||||
* submitting the from to the backend.
|
||||
*
|
||||
* @param $company_gateway_id
|
||||
* @param $gateway_type_id
|
||||
*/
|
||||
public function handleMethodSelectingEvent($company_gateway_id, $gateway_type_id)
|
||||
{
|
||||
$this->company_gateway_id = $company_gateway_id;
|
||||
$this->payment_method_id = $gateway_type_id;
|
||||
|
||||
$this->handleBeforePaymentEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to handle events before payments.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handleBeforePaymentEvents()
|
||||
{
|
||||
$this->steps['started_payment'] = true;
|
||||
$this->steps['show_loading_bar'] = true;
|
||||
|
||||
$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,
|
||||
'quantity' => $this->quantity,
|
||||
];
|
||||
|
||||
$is_eligible = $this->subscription->service()->isEligible($this->contact);
|
||||
|
||||
if (is_array($is_eligible) && $is_eligible['message'] != 'Success') {
|
||||
$this->steps['not_eligible'] = true;
|
||||
$this->steps['not_eligible_message'] = $is_eligible['message'];
|
||||
$this->steps['show_loading_bar'] = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->invoice = $this->subscription
|
||||
->service()
|
||||
->createInvoice($data, $this->quantity)
|
||||
->service()
|
||||
->markSent()
|
||||
->fillDefaults()
|
||||
->adjustInventory()
|
||||
->save();
|
||||
|
||||
Cache::put($this->hash, [
|
||||
'subscription_id' => $this->subscription->id,
|
||||
'email' => $this->email ?? $this->contact->email,
|
||||
'client_id' => $this->contact->client->id,
|
||||
'invoice_id' => $this->invoice->id,
|
||||
'context' => 'purchase',
|
||||
'campaign' => $this->campaign,
|
||||
], now()->addMinutes(60));
|
||||
|
||||
$this->emit('beforePaymentEventsCompleted');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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([
|
||||
'email' => $this->email ?? $this->contact->email,
|
||||
'quantity' => $this->quantity,
|
||||
'contact_id' => $this->contact->id,
|
||||
'client_id' => $this->contact->client->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function handlePaymentNotRequired()
|
||||
{
|
||||
|
||||
$is_eligible = $this->subscription->service()->isEligible($this->contact);
|
||||
|
||||
if ($is_eligible['status_code'] != 200) {
|
||||
$this->steps['not_eligible'] = true;
|
||||
$this->steps['not_eligible_message'] = $is_eligible['message'];
|
||||
$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,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update quantity property.
|
||||
*
|
||||
* @param string $option
|
||||
* @return int
|
||||
*/
|
||||
public function updateQuantity(string $option): int
|
||||
{
|
||||
$this->handleCoupon();
|
||||
|
||||
if ($this->quantity == 1 && $option == 'decrement') {
|
||||
$this->price = $this->price * 1;
|
||||
return $this->quantity;
|
||||
}
|
||||
|
||||
if ($this->quantity > $this->subscription->max_seats_limit && $option == 'increment') {
|
||||
$this->price = $this->price * $this->subscription->max_seats_limit;
|
||||
return $this->quantity;
|
||||
}
|
||||
|
||||
if ($option == 'increment') {
|
||||
$this->quantity++;
|
||||
$this->price = $this->price * $this->quantity;
|
||||
return $this->quantity;
|
||||
}
|
||||
|
||||
$this->quantity--;
|
||||
$this->price = $this->price * $this->quantity;
|
||||
|
||||
return $this->quantity;
|
||||
}
|
||||
|
||||
public function handleCoupon()
|
||||
{
|
||||
|
||||
if($this->steps['discount_applied']){
|
||||
$this->price = $this->subscription->promo_price;
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->coupon == $this->subscription->promo_code) {
|
||||
$this->price = $this->subscription->promo_price;
|
||||
$this->quantity = 1;
|
||||
$this->steps['discount_applied'] = true;
|
||||
}
|
||||
else
|
||||
$this->price = $this->subscription->price;
|
||||
}
|
||||
|
||||
public function passwordlessLogin()
|
||||
{
|
||||
$this->passwordless_login_btn = true;
|
||||
|
||||
$contact = ClientContact::query()
|
||||
->where('email', $this->email)
|
||||
->where('company_id', $this->subscription->company_id)
|
||||
->first();
|
||||
|
||||
$mailer = new NinjaMailerObject();
|
||||
$mailer->mailable = new ContactPasswordlessLogin($this->email, $this->subscription->company, (string)route('client.subscription.purchase', $this->subscription->hashed_id) . '?coupon=' . $this->coupon);
|
||||
$mailer->company = $this->subscription->company;
|
||||
$mailer->settings = $this->subscription->company->settings;
|
||||
$mailer->to_user = $contact;
|
||||
|
||||
NinjaMailerJob::dispatch($mailer);
|
||||
|
||||
$this->steps['passwordless_login_sent'] = true;
|
||||
$this->passwordless_login_btn = false;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
if (array_key_exists('email', $this->request_data)) {
|
||||
$this->email = $this->request_data['email'];
|
||||
}
|
||||
|
||||
if ($this->contact instanceof ClientContact) {
|
||||
$this->getPaymentMethods($this->contact);
|
||||
}
|
||||
|
||||
return render('components.livewire.billing-portal-purchasev2');
|
||||
}
|
||||
}
|
@ -24,7 +24,7 @@ class GenerateSmsRequest extends Request
|
||||
*/
|
||||
public function authorize() : bool
|
||||
{
|
||||
return auth()->user()->isAdmin();
|
||||
return auth()->user();
|
||||
}
|
||||
|
||||
|
||||
|
@ -54,6 +54,9 @@ class Subscription extends BaseModel
|
||||
'price',
|
||||
'name',
|
||||
'currency_id',
|
||||
'registration_required',
|
||||
'optional_product_ids',
|
||||
'optional_recurring_product_ids',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
|
@ -67,6 +67,7 @@ class BankTransactionTransformer extends EntityTransformer
|
||||
'invoice_ids' => (string) $bank_transaction->invoice_ids ?: '',
|
||||
'expense_id'=> (string) $this->encodePrimaryKey($bank_transaction->expense_id) ?: '',
|
||||
'vendor_id'=> (string) $this->encodePrimaryKey($bank_transaction->vendor_id) ?: '',
|
||||
'bank_rule_id' => (string) $this->encodePrimaryKey($bank_transaction->bank_rule_id) ?: '',
|
||||
'is_deleted' => (bool) $bank_transaction->is_deleted,
|
||||
'created_at' => (int) $bank_transaction->created_at,
|
||||
'updated_at' => (int) $bank_transaction->updated_at,
|
||||
|
@ -27,6 +27,15 @@ return new class extends Migration
|
||||
{
|
||||
$table->bigInteger('bank_rule_id')->nullable();
|
||||
});
|
||||
|
||||
Schema::table('subscriptions', function (Blueprint $table)
|
||||
{
|
||||
$table->boolean('registration_required')->default(false);
|
||||
$table->text('optional_product_ids')->nullable();
|
||||
$table->text('optional_recurring_product_ids')->nullable();
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
65
package-lock.json
generated
65
package-lock.json
generated
@ -5,6 +5,8 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@tailwindcss/forms": "^0.3.4",
|
||||
"@tailwindcss/line-clamp": "^0.3.1",
|
||||
"autoprefixer": "^10.3.7",
|
||||
"axios": "^0.25",
|
||||
"card-js": "^1.0.13",
|
||||
@ -25,6 +27,7 @@
|
||||
"devDependencies": {
|
||||
"@babel/compat-data": "7.15.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.14.5",
|
||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||
"laravel-mix-purgecss": "^6.0.0",
|
||||
"vue-template-compiler": "^2.6.14"
|
||||
}
|
||||
@ -1682,6 +1685,34 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/aspect-ratio": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/aspect-ratio/-/aspect-ratio-0.4.2.tgz",
|
||||
"integrity": "sha512-8QPrypskfBa7QIMuKHg2TA7BqES6vhBrDLOv8Unb6FcFyd3TjKbc6lcmb9UPQHxfl24sXoJ41ux/H7qQQvfaSQ==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/forms": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.3.4.tgz",
|
||||
"integrity": "sha512-vlAoBifNJUkagB+PAdW4aHMe4pKmSLroH398UPgIogBFc91D2VlHUxe4pjxQhiJl0Nfw53sHSJSQBSTQBZP3vA==",
|
||||
"dependencies": {
|
||||
"mini-svg-data-uri": "^1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tailwindcss": ">=2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/line-clamp": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.3.1.tgz",
|
||||
"integrity": "sha512-pNr0T8LAc3TUx/gxCfQZRe9NB2dPEo/cedPHzUGIPxqDMhgjwNm6jYxww4W5l0zAsAddxr+XfZcqttGiFDgrGg==",
|
||||
"peerDependencies": {
|
||||
"tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@trysound/sax": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
|
||||
@ -5742,6 +5773,14 @@
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/mini-svg-data-uri": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
|
||||
"integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==",
|
||||
"bin": {
|
||||
"mini-svg-data-uri": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/minimalistic-assert": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||
@ -10285,6 +10324,27 @@
|
||||
"fastq": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"@tailwindcss/aspect-ratio": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/aspect-ratio/-/aspect-ratio-0.4.2.tgz",
|
||||
"integrity": "sha512-8QPrypskfBa7QIMuKHg2TA7BqES6vhBrDLOv8Unb6FcFyd3TjKbc6lcmb9UPQHxfl24sXoJ41ux/H7qQQvfaSQ==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@tailwindcss/forms": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.3.4.tgz",
|
||||
"integrity": "sha512-vlAoBifNJUkagB+PAdW4aHMe4pKmSLroH398UPgIogBFc91D2VlHUxe4pjxQhiJl0Nfw53sHSJSQBSTQBZP3vA==",
|
||||
"requires": {
|
||||
"mini-svg-data-uri": "^1.2.3"
|
||||
}
|
||||
},
|
||||
"@tailwindcss/line-clamp": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.3.1.tgz",
|
||||
"integrity": "sha512-pNr0T8LAc3TUx/gxCfQZRe9NB2dPEo/cedPHzUGIPxqDMhgjwNm6jYxww4W5l0zAsAddxr+XfZcqttGiFDgrGg==",
|
||||
"requires": {}
|
||||
},
|
||||
"@trysound/sax": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
|
||||
@ -13399,6 +13459,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mini-svg-data-uri": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
|
||||
"integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg=="
|
||||
},
|
||||
"minimalistic-assert": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||
|
@ -11,10 +11,13 @@
|
||||
"devDependencies": {
|
||||
"@babel/compat-data": "7.15.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.14.5",
|
||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||
"laravel-mix-purgecss": "^6.0.0",
|
||||
"vue-template-compiler": "^2.6.14"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/line-clamp": "^0.3.1",
|
||||
"@tailwindcss/forms": "^0.3.4",
|
||||
"autoprefixer": "^10.3.7",
|
||||
"axios": "^0.25",
|
||||
"card-js": "^1.0.13",
|
||||
|
2
public/css/app.css
vendored
2
public/css/app.css
vendored
File diff suppressed because one or more lines are too long
2
public/js/app.js
vendored
2
public/js/app.js
vendored
File diff suppressed because one or more lines are too long
2
public/js/setup/setup.js
vendored
2
public/js/setup/setup.js
vendored
File diff suppressed because one or more lines are too long
@ -1,5 +1,5 @@
|
||||
{
|
||||
"/js/app.js": "/js/app.js?id=384185bf9d293949134d09b890c81369",
|
||||
"/js/app.js": "/js/app.js?id=19300612c6880925e8043b61e8d49632",
|
||||
"/js/clients/payment_methods/authorize-authorize-card.js": "/js/clients/payment_methods/authorize-authorize-card.js?id=9fb77e87fe0f85a367050e08f79ec9df",
|
||||
"/js/clients/payments/authorize-credit-card-payment.js": "/js/clients/payments/authorize-credit-card-payment.js?id=803182f668c39d631ca5c55437876da4",
|
||||
"/js/clients/payments/forte-credit-card-payment.js": "/js/clients/payments/forte-credit-card-payment.js?id=6e9f466c5504d3753f9b4ffc6f947095",
|
||||
@ -15,7 +15,7 @@
|
||||
"/js/clients/quotes/action-selectors.js": "/js/clients/quotes/action-selectors.js?id=6fb63bae43d077b5061f4dadfe8dffc8",
|
||||
"/js/clients/quotes/approve.js": "/js/clients/quotes/approve.js?id=cdc76607aaf0b47a5a4e554e4177713d",
|
||||
"/js/clients/payments/stripe-credit-card.js": "/js/clients/payments/stripe-credit-card.js?id=809de47258a681f0ffebe787dd6a9a93",
|
||||
"/js/setup/setup.js": "/js/setup/setup.js?id=87367cce4927b42a92defdbae7a64711",
|
||||
"/js/setup/setup.js": "/js/setup/setup.js?id=27560b012f166f8b9417ced2188aab70",
|
||||
"/js/clients/payments/card-js.min.js": "/js/clients/payments/card-js.min.js?id=8ce33c3deae058ad314fb8357e5be63b",
|
||||
"/js/clients/shared/pdf.js": "/js/clients/shared/pdf.js?id=be5307abc990bb44f2f92628103b1d98",
|
||||
"/js/clients/shared/multiple-downloads.js": "/js/clients/shared/multiple-downloads.js?id=c2caa29f753ad1f3a12ca45acddacd72",
|
||||
@ -42,7 +42,7 @@
|
||||
"/js/clients/payments/stripe-przelewy24.js": "/js/clients/payments/stripe-przelewy24.js?id=3d53d2f7d0291d9f92cf7414dd2d351c",
|
||||
"/js/clients/payments/stripe-browserpay.js": "/js/clients/payments/stripe-browserpay.js?id=db71055862995fd6ae21becfc587a3de",
|
||||
"/js/clients/payments/stripe-fpx.js": "/js/clients/payments/stripe-fpx.js?id=914a6846ad1e5584635e7430fef76875",
|
||||
"/css/app.css": "/css/app.css?id=6bafb560444b3b12f8d1ce59bd7fd703",
|
||||
"/css/app.css": "/css/app.css?id=2c1ff2517e9909ca83760beb295535be",
|
||||
"/css/card-js.min.css": "/css/card-js.min.css?id=62afeb675235451543ada60afcedcb7c",
|
||||
"/vendor/clipboard.min.js": "/vendor/clipboard.min.js?id=15f52a1ee547f2bdd46e56747332ca2d"
|
||||
}
|
||||
|
17
resources/views/billing-portal/purchasev2.blade.php
Normal file
17
resources/views/billing-portal/purchasev2.blade.php
Normal file
@ -0,0 +1,17 @@
|
||||
@extends('portal.ninja2020.layout.clean')
|
||||
@section('meta_title', ctrans('texts.purchase'))
|
||||
|
||||
@section('body')
|
||||
@livewire('billing-portal-purchasev2', ['subscription' => $subscription, 'company' => $subscription->company, 'contact' => auth()->guard('contact')->user(), 'hash' => $hash, 'request_data' => $request_data, 'campaign' => request()->query('campaign') ?? null])
|
||||
@stop
|
||||
|
||||
@push('footer')
|
||||
<script>
|
||||
function updateGatewayFields(companyGatewayId, paymentMethodId) {
|
||||
document.getElementById('company_gateway_id').value = companyGatewayId;
|
||||
document.getElementById('payment_method_id').value = paymentMethodId;
|
||||
}
|
||||
|
||||
Livewire.on('beforePaymentEventsCompleted', () => document.getElementById('payment-method-form').submit());
|
||||
</script>
|
||||
@endpush
|
@ -0,0 +1,152 @@
|
||||
<style type="text/css">
|
||||
|
||||
</style>
|
||||
|
||||
<div class="grid grid-cols-12">
|
||||
<div class="col-span-12 xl:col-span-8 bg-gray-50 flex flex-col max-h-100px items-center">
|
||||
<div class="w-full p-8 md:max-w-3xl">
|
||||
<img class="object-scale-down" style="max-height: 100px;"src="{{ $subscription->company->present()->logo }}" alt="{{ $subscription->company->present()->name }}">
|
||||
|
||||
<h1 id="billing-page-company-logo" class="text-3xl font-bold tracking-wide mt-6">
|
||||
{{ $subscription->name }}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="w-full p-4 md:max-w-3xl">
|
||||
@if(!empty($subscription->recurring_product_ids))
|
||||
<p
|
||||
class="mb-4 uppercase leading-4 tracking-wide inline-flex items-center rounded-full text-xs font-medium">
|
||||
{{ ctrans('texts.recurring_purchases') }}
|
||||
</p>
|
||||
<ul role="list" class="divide-y divide-gray-200 bg-white">
|
||||
@foreach($subscription->service()->recurring_products() as $product)
|
||||
<li>
|
||||
<a href="#" class="block hover:bg-gray-50">
|
||||
<div class="px-4 py-4 sm:px-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="ml-2 flex flex-shrink-0">
|
||||
<p class="inline-flex rounded-full bg-green-100 px-2 text-xs font-semibold leading-5 text-green-800"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-0 sm:flex sm:justify-between">
|
||||
<div class="sm:flex">
|
||||
<p class="text-sm font-medium text-gray-900 mt-0">{!! nl2br($product->notes) !!}</p>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center text-sm text-gray-500 sm:mt-0">
|
||||
<span data-ref="price">{{ \App\Utils\Number::formatMoney($product->price, $subscription->company) }} / {{ App\Models\RecurringInvoice::frequencyForKey($subscription->frequency_id) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
</div>
|
||||
<div class="w-full p-4 md:max-w-3xl">
|
||||
|
||||
@if(!empty($subscription->product_ids))
|
||||
<p
|
||||
class="mb-4 uppercase leading-4 tracking-wide inline-flex items-center rounded-full text-xs font-medium">
|
||||
{{ ctrans('texts.one_time_purchases') }}
|
||||
</p>
|
||||
<ul role="list" class="divide-y divide-gray-200 bg-white">
|
||||
@foreach($subscription->service()->products() as $product)
|
||||
<li>
|
||||
<a href="#" class="block hover:bg-gray-50">
|
||||
<div class="px-4 py-4 sm:px-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="truncate text-sm font-medium text-gray-600"></p>
|
||||
<div class="ml-2 flex flex-shrink-0">
|
||||
<p class="inline-flex rounded-full bg-green-100 px-2 text-xs font-semibold leading-5 text-green-800"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 sm:flex sm:justify-between">
|
||||
<div class="sm:flex">
|
||||
<p class="text-sm font-medium text-gray-900 mt-2">{!! nl2br($product->notes) !!}</p>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center text-sm text-gray-500 sm:mt-0">
|
||||
<span data-ref="price">{{ \App\Utils\Number::formatMoney($product->price, $subscription->company) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="relative mt-8">
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<div class="w-full border-t border-gray-300"></div>
|
||||
</div>
|
||||
|
||||
<div class="relative flex justify-center text-sm leading-5">
|
||||
<h1 class="text-2xl font-bold tracking-wide bg-gray-50 px-6 py-0">Optional products</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full p-4 md:max-w-3xl">
|
||||
@if(!empty($subscription->recurring_product_ids))
|
||||
@foreach($subscription->service()->recurring_products() as $product)
|
||||
<div class="flex items-center justify-between mb-4 bg-white rounded px-6 py-4 shadow-sm border">
|
||||
<div class="text-sm">{!! nl2br($product->notes) !!}</div>
|
||||
<div data-ref="price-and-quantity-container">
|
||||
<span data-ref="price">{{ \App\Utils\Number::formatMoney($product->price, $subscription->company) }} / {{ App\Models\RecurringInvoice::frequencyForKey($subscription->frequency_id) }}</span>
|
||||
{{-- <span data-ref="quantity" class="text-sm">(1x)</span>--}}
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
<div class="w-full p-4 md:max-w-3xl">
|
||||
|
||||
@if(!empty($subscription->product_ids))
|
||||
@foreach($subscription->service()->products() as $product)
|
||||
<div class="flex items-center justify-between mb-4 bg-white rounded px-6 py-4 shadow-sm border">
|
||||
<div class="text-sm">{!! nl2br($product->notes) !!}</div>
|
||||
<div data-ref="price-and-quantity-container">
|
||||
<span
|
||||
data-ref="price">{{ \App\Utils\Number::formatMoney($product->price, $subscription->company) }}</span>
|
||||
{{-- <span data-ref="quantity" class="text-sm">(1x)</span>--}}
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-span-12 xl:col-span-4 bg-blue-500 flex flex-col item-center ">
|
||||
<div class="w-full p-4 md:max-w-3xl">
|
||||
<div id="summary" class="w-1/4 px-8 text-white">
|
||||
<h1 class="font-semibold text-2xl border-b pb-8 text-white">Order Summary</h1>
|
||||
<div class="flex justify-between mt-10 mb-5">
|
||||
<span class="font-semibold text-sm uppercase">Items 3</span>
|
||||
<span class="font-semibold text-sm">590$</span>
|
||||
</div>
|
||||
<div>
|
||||
<label class="font-medium inline-block mb-3 text-sm uppercase">Shipping</label>
|
||||
<select class="block p-2 text-white w-full text-sm">
|
||||
<option>Standard shipping - $10.00</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="py-10">
|
||||
<label for="promo" class="font-semibold inline-block mb-3 text-sm uppercase">Promo Code</label>
|
||||
<input type="text" id="promo" placeholder="Enter your code" class="p-2 text-sm w-full">
|
||||
</div>
|
||||
<button class="bg-white hover:bg-gray-600 px-5 py-2 text-sm text-blue-500 uppercase">Apply</button>
|
||||
<div class="border-t mt-8">
|
||||
<div class="flex font-semibold justify-between py-6 text-sm uppercase">
|
||||
<span>Total cost</span>
|
||||
<span>$600</span>
|
||||
</div>
|
||||
<button class="bg-white font-semibold hover:bg-gray-600 py-3 text-sm text-blue-500 uppercase w-full">Checkout</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
@ -115,6 +115,8 @@ Route::post('payments/process/response', [App\Http\Controllers\ClientPortal\Paym
|
||||
Route::get('payments/process/response', [App\Http\Controllers\ClientPortal\PaymentController::class, 'response'])->name('client.payments.response.get')->middleware(['locale', 'domain_db', 'verify_hash']);
|
||||
|
||||
Route::get('client/subscriptions/{subscription}/purchase', [App\Http\Controllers\ClientPortal\SubscriptionPurchaseController::class, 'index'])->name('client.subscription.purchase')->middleware('domain_db');
|
||||
Route::get('client/subscriptions/{subscription}/purchase/v2', [App\Http\Controllers\ClientPortal\SubscriptionPurchaseController::class, 'upgrade'])->name('client.subscription.upgrade')->middleware('domain_db');
|
||||
|
||||
|
||||
Route::group(['middleware' => ['invite_db'], 'prefix' => 'client', 'as' => 'client.'], function () {
|
||||
/*Invitation catches*/
|
||||
|
6
tailwind.config.js
vendored
6
tailwind.config.js
vendored
@ -17,5 +17,9 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
variants: {},
|
||||
plugins: []
|
||||
plugins: [
|
||||
require('@tailwindcss/line-clamp'),
|
||||
require('@tailwindcss/forms')
|
||||
]
|
||||
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user