mirror of
				https://github.com/invoiceninja/invoiceninja.git
				synced 2025-11-03 22:47:32 -05:00 
			
		
		
		
	Merge pull request #5382 from beganovich/v5-0804-billing-portal
(v5) 0804: Billing portal
This commit is contained in:
		
						commit
						e7ad82df48
					
				@ -24,14 +24,14 @@ class SubscriptionPlanSwitchController extends Controller
 | 
			
		||||
     *
 | 
			
		||||
     * @param ShowPlanSwitchRequest $request
 | 
			
		||||
     * @param Subscription $subscription
 | 
			
		||||
     * @param string $target_subscription
 | 
			
		||||
     * @param string $target
 | 
			
		||||
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
 | 
			
		||||
     */
 | 
			
		||||
    public function index(ShowPlanSwitchRequest $request, Subscription $subscription, Subscription $target_subscription)
 | 
			
		||||
    public function index(ShowPlanSwitchRequest $request, Subscription $subscription, Subscription $target)
 | 
			
		||||
    {
 | 
			
		||||
        return render('subscriptions.switch', [
 | 
			
		||||
            'subscription' => $subscription,
 | 
			
		||||
            'target_subscription' => $target_subscription,
 | 
			
		||||
            'target' => $target,
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										38
									
								
								app/Http/Livewire/RecurringInvoiceCancellation.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								app/Http/Livewire/RecurringInvoiceCancellation.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,38 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Invoice Ninja (https://invoiceninja.com).
 | 
			
		||||
 *
 | 
			
		||||
 * @link https://github.com/invoiceninja/invoiceninja source repository
 | 
			
		||||
 *
 | 
			
		||||
 * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
 | 
			
		||||
 *
 | 
			
		||||
 * @license https://opensource.org/licenses/AAL
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace App\Http\Livewire;
 | 
			
		||||
 | 
			
		||||
use App\Models\RecurringInvoice;
 | 
			
		||||
use Livewire\Component;
 | 
			
		||||
 | 
			
		||||
class RecurringInvoiceCancellation extends Component
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * @var RecurringInvoice
 | 
			
		||||
     */
 | 
			
		||||
    public $invoice;
 | 
			
		||||
 | 
			
		||||
    public function processCancellation()
 | 
			
		||||
    {
 | 
			
		||||
        if ($this->invoice->subscription) {
 | 
			
		||||
            return $this->invoice->subscription->service()->handleCancellation();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return redirect()->route('client.recurring_invoices.request_cancellation', ['recurring_invoice' => $this->invoice->hashed_id]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function render()
 | 
			
		||||
    {
 | 
			
		||||
        return render('components.livewire.recurring-invoice-cancellation');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -12,30 +12,90 @@
 | 
			
		||||
 | 
			
		||||
namespace App\Http\Livewire;
 | 
			
		||||
 | 
			
		||||
use App\Models\ClientContact;
 | 
			
		||||
use App\Models\Subscription;
 | 
			
		||||
use Illuminate\Support\Str;
 | 
			
		||||
use Livewire\Component;
 | 
			
		||||
 | 
			
		||||
class SubscriptionPlanSwitch extends Component
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Subscription
 | 
			
		||||
     */
 | 
			
		||||
    public $subscription;
 | 
			
		||||
 | 
			
		||||
    public $target_subscription;
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Subscription
 | 
			
		||||
     */
 | 
			
		||||
    public $target;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var ClientContact
 | 
			
		||||
     */
 | 
			
		||||
    public $contact;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var array
 | 
			
		||||
     */
 | 
			
		||||
    public $methods = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var string
 | 
			
		||||
     */
 | 
			
		||||
    public $total;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var array
 | 
			
		||||
     */
 | 
			
		||||
    public $state = [
 | 
			
		||||
        'payment_initialised' => false,
 | 
			
		||||
        'show_loading_bar' => false,
 | 
			
		||||
        'invoice' => null,
 | 
			
		||||
        'company_gateway_id' => null,
 | 
			
		||||
        'payment_method_id' => null,
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var mixed|string
 | 
			
		||||
     */
 | 
			
		||||
    public $hash;
 | 
			
		||||
 | 
			
		||||
    public function mount()
 | 
			
		||||
    {
 | 
			
		||||
        $this->total = $this->subscription->service()->getPriceBetweenSubscriptions($this->subscription, $this->target);
 | 
			
		||||
 | 
			
		||||
        $this->methods = $this->contact->client->service()->getPaymentMethods(100);
 | 
			
		||||
 | 
			
		||||
        $this->total = 1;
 | 
			
		||||
        $this->hash = Str::uuid()->toString();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function handleBeforePaymentEvents()
 | 
			
		||||
    public function handleBeforePaymentEvents(): void
 | 
			
		||||
    {
 | 
			
		||||
        // ..
 | 
			
		||||
        $this->state['show_loading_bar'] = true;
 | 
			
		||||
 | 
			
		||||
        $this->state['invoice'] = $this->subscription->service()->createChangePlanInvoice([
 | 
			
		||||
            'subscription' => $this->subscription,
 | 
			
		||||
            'target' => $this->target,
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $this->state['payment_initialised'] = true;
 | 
			
		||||
 | 
			
		||||
        $this->emit('beforePaymentEventsCompleted');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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->state['company_gateway_id'] = $company_gateway_id;
 | 
			
		||||
        $this->state['payment_method_id'] = $gateway_type_id;
 | 
			
		||||
 | 
			
		||||
        $this->handleBeforePaymentEvents();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function render()
 | 
			
		||||
 | 
			
		||||
@ -184,7 +184,7 @@ class SubscriptionService
 | 
			
		||||
 | 
			
		||||
    public function createChangePlanInvoice($data)
 | 
			
		||||
    {
 | 
			
		||||
        
 | 
			
		||||
        return Invoice::where('status_id', Invoice::STATUS_SENT)->first();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function createInvoice($data): ?\App\Models\Invoice
 | 
			
		||||
@ -275,9 +275,9 @@ class SubscriptionService
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the single charge products for the 
 | 
			
		||||
     * Get the single charge products for the
 | 
			
		||||
     * subscription
 | 
			
		||||
     * 
 | 
			
		||||
     *
 | 
			
		||||
     * @return ?Product Collection
 | 
			
		||||
     */
 | 
			
		||||
    public function products()
 | 
			
		||||
@ -286,9 +286,9 @@ class SubscriptionService
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the recurring products for the 
 | 
			
		||||
     * Get the recurring products for the
 | 
			
		||||
     * subscription
 | 
			
		||||
     * 
 | 
			
		||||
     *
 | 
			
		||||
     * @return ?Product Collection
 | 
			
		||||
     */
 | 
			
		||||
    public function recurring_products()
 | 
			
		||||
@ -317,6 +317,21 @@ class SubscriptionService
 | 
			
		||||
 | 
			
		||||
    public function handleCancellation()
 | 
			
		||||
    {
 | 
			
		||||
        dd('Cancelling using SubscriptionService');
 | 
			
		||||
 | 
			
		||||
        // ..
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get pro rata calculation between subscriptions.
 | 
			
		||||
     *
 | 
			
		||||
     * @param Subscription $current
 | 
			
		||||
     * @param Subscription $target
 | 
			
		||||
     */
 | 
			
		||||
    public function getPriceBetweenSubscriptions(Subscription $current, Subscription $target): int
 | 
			
		||||
    {
 | 
			
		||||
        // Calculate the pro rata. Return negative value if credits needed.
 | 
			
		||||
 | 
			
		||||
        return 1;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -41,7 +41,7 @@
 | 
			
		||||
 | 
			
		||||
                    @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">{{ $product->product_key }}</div>
 | 
			
		||||
                            <div class="text-sm">{{ $product->notes }}</div>
 | 
			
		||||
                            <div data-ref="price-and-quantity-container">
 | 
			
		||||
                                <span
 | 
			
		||||
                                    data-ref="price">{{ \App\Utils\Number::formatMoney($product->price, $subscription->company) }}</span>
 | 
			
		||||
@ -136,9 +136,6 @@
 | 
			
		||||
                @elseif($steps['show_start_trial'])
 | 
			
		||||
                    <form wire:submit.prevent="handleTrial" class="mt-8">
 | 
			
		||||
                        @csrf
 | 
			
		||||
                        <p class="mb-4">Some text about the trial goes here. Details about the days, etc.</p>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                        <button class="px-3 py-2 border rounded mr-4 hover:border-blue-600">
 | 
			
		||||
                            {{ ctrans('texts.trial_call_to_action') }}
 | 
			
		||||
                        </button>
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,3 @@
 | 
			
		||||
<button wire:click="processCancellation" class="button button-danger button-block">
 | 
			
		||||
    {{ ctrans('texts.confirm') }}
 | 
			
		||||
</button>
 | 
			
		||||
@ -7,24 +7,58 @@
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div class="relative flex justify-center text-sm leading-5">
 | 
			
		||||
                <h1 class="text-2xl font-bold tracking-wide bg-gray-100 px-6 py-0">
 | 
			
		||||
                    {{ ctrans('texts.total') }}: {{ \App\Utils\Number::formatMoney($total, $subscription->company) }}
 | 
			
		||||
                    {{--                    <small class="ml-1 line-through text-gray-500">{{ \App\Utils\Number::formatMoney($subscription->price, $subscription->company) }}</small>--}}
 | 
			
		||||
                </h1>
 | 
			
		||||
                <span class="font-bold tracking-wide bg-gray-100 px-6 py-0">Select a payment method:</span>
 | 
			
		||||
                {{--                <h1 class="text-2xl font-bold tracking-wide bg-gray-100 px-6 py-0">--}}
 | 
			
		||||
                {{--                    {{ ctrans('texts.total') }}: {{ \App\Utils\Number::formatMoney($total, $subscription->company) }}--}}
 | 
			
		||||
                {{--                    <small class="ml-1 line-through text-gray-500">{{ \App\Utils\Number::formatMoney($subscription->price, $subscription->company) }}</small>--}}
 | 
			
		||||
                {{--                </h1>--}}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <!-- Payment methods -->
 | 
			
		||||
        @if($state['invoice'])
 | 
			
		||||
            <form action="{{ route('client.payments.process', ['hash' => $hash, 'sidebar' => 'hidden']) }}"
 | 
			
		||||
                  method="post" id="payment-method-form">
 | 
			
		||||
                @csrf
 | 
			
		||||
 | 
			
		||||
                @if($state['invoice'] instanceof \App\Models\Invoice)
 | 
			
		||||
                    <input type="hidden" name="invoices[]" value="{{ $state['invoice']->hashed_id }}">
 | 
			
		||||
                    <input type="hidden" name="payable_invoices[0][amount]"
 | 
			
		||||
                           value="{{ $state['invoice']->partial > 0 ? \App\Utils\Number::formatValue($state['invoice']->partial, $state['invoice']->client->currency()) : \App\Utils\Number::formatValue($state['invoice']->balance, $state['invoice']->client->currency()) }}">
 | 
			
		||||
                    <input type="hidden" name="payable_invoices[0][invoice_id]"
 | 
			
		||||
                           value="{{ $state['invoice']->hashed_id }}">
 | 
			
		||||
                @endif
 | 
			
		||||
 | 
			
		||||
                <input type="hidden" name="action" value="payment">
 | 
			
		||||
                <input type="hidden" name="company_gateway_id" value="{{ $state['company_gateway_id'] }}"/>
 | 
			
		||||
                <input type="hidden" name="payment_method_id" value="{{ $state['payment_method_id'] }}"/>
 | 
			
		||||
            </form>
 | 
			
		||||
    @endif
 | 
			
		||||
 | 
			
		||||
    <!-- Payment methods -->
 | 
			
		||||
        <div class="mt-8 flex flex-col items-center">
 | 
			
		||||
            <small class="block mb-4">Select a payment method:</small>
 | 
			
		||||
            <div>
 | 
			
		||||
                @foreach($this->methods as $method)
 | 
			
		||||
                    <button
 | 
			
		||||
                        {{--                        wire:click="handleMethodSelectingEvent('{{ $method['company_gateway_id'] }}', '{{ $method['gateway_type_id'] }}')"--}}
 | 
			
		||||
                        class="px-3 py-2 border bg-white rounded mr-4 hover:border-blue-600">
 | 
			
		||||
                        {{ $method['label'] }}
 | 
			
		||||
                    </button>
 | 
			
		||||
                @endforeach
 | 
			
		||||
 | 
			
		||||
                @if(!$state['payment_initialised'])
 | 
			
		||||
                    @foreach($this->methods as $method)
 | 
			
		||||
                        <button
 | 
			
		||||
                            wire:click="handleMethodSelectingEvent('{{ $method['company_gateway_id'] }}', '{{ $method['gateway_type_id'] }}')"
 | 
			
		||||
                            class="px-3 py-2 border bg-white rounded mr-4 hover:border-blue-600">
 | 
			
		||||
                            {{ $method['label'] }}
 | 
			
		||||
                        </button>
 | 
			
		||||
                    @endforeach
 | 
			
		||||
                @endif
 | 
			
		||||
 | 
			
		||||
                @if($state['show_loading_bar'])
 | 
			
		||||
                    <div class="flex justify-center">
 | 
			
		||||
                        <svg class="animate-spin h-8 w-8 text-primary" xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
                             fill="none" viewBox="0 0 24 24">
 | 
			
		||||
                            <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
 | 
			
		||||
                                    stroke-width="4"></circle>
 | 
			
		||||
                            <path class="opacity-75" fill="currentColor"
 | 
			
		||||
                                  d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
 | 
			
		||||
                        </svg>
 | 
			
		||||
                    </div>
 | 
			
		||||
                @endif
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
<div x-show="open" class="fixed bottom-0 inset-x-0 px-4 pb-4 sm:inset-0 sm:flex sm:items-center sm:justify-center" style="display:none;">
 | 
			
		||||
<div x-show="open" class="fixed bottom-0 inset-x-0 px-4 pb-4 sm:inset-0 sm:flex sm:items-center sm:justify-center"
 | 
			
		||||
     style="display:none;">
 | 
			
		||||
    <div x-show="open" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0"
 | 
			
		||||
         x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200"
 | 
			
		||||
         x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
 | 
			
		||||
@ -33,10 +34,7 @@
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
 | 
			
		||||
            <div class="flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto">
 | 
			
		||||
                <a href="{{ route('client.recurring_invoices.request_cancellation',['recurring_invoice' => $invoice->hashed_id]) }}"
 | 
			
		||||
                   class="button button-danger button-block">
 | 
			
		||||
                    {{ ctrans('texts.confirm') }}
 | 
			
		||||
                </a>
 | 
			
		||||
                @livewire('recurring-invoice-cancellation', ['invoice' => $invoice])
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto">
 | 
			
		||||
                <button @click="open = false" type="button" class="button button-secondary button-block">
 | 
			
		||||
 | 
			
		||||
@ -85,14 +85,14 @@
 | 
			
		||||
            </div>
 | 
			
		||||
        @endif
 | 
			
		||||
 | 
			
		||||
        @if($invoice->subscription->allow_plan_changes)
 | 
			
		||||
        @if($invoice->subscription && $invoice->subscription->allow_plan_changes)
 | 
			
		||||
            <div class="bg-white shadow overflow-hidden px-4 py-5 lg:rounded-lg">
 | 
			
		||||
                <h3 class="text-lg leading-6 font-medium text-gray-900">Switch Plans:</h3>
 | 
			
		||||
                <p class="mt-1 max-w-2xl text-sm leading-5 text-gray-500">Upgrade or downgrade your current plan.</p>
 | 
			
		||||
 | 
			
		||||
                <div class="flex mt-4">
 | 
			
		||||
                    @foreach($invoice->subscription->service()->getPlans() as $subscription)
 | 
			
		||||
                        <a href="{{ route('client.subscription.plan_switch', ['subscription' => $invoice->subscription->hashed_id, 'target_subscription' => $subscription->hashed_id]) }}" class="border rounded px-5 py-2 hover:border-gray-800 text-sm cursor-pointer">{{ $subscription->name }}</a>
 | 
			
		||||
                        <a href="{{ route('client.subscription.plan_switch', ['subscription' => $invoice->subscription->hashed_id, 'target' => $subscription->hashed_id]) }}" class="border rounded px-5 py-2 hover:border-gray-800 text-sm cursor-pointer">{{ $subscription->name }}</a>
 | 
			
		||||
                    @endforeach
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
@ -42,6 +42,12 @@
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <!-- Payment box -->
 | 
			
		||||
        @livewire('subscription-plan-switch', compact('subscription', 'target_subscription', 'contact'))
 | 
			
		||||
        @livewire('subscription-plan-switch', compact('subscription', 'target', 'contact'))
 | 
			
		||||
    </div>
 | 
			
		||||
@endsection
 | 
			
		||||
 | 
			
		||||
@push('footer')
 | 
			
		||||
    <script>
 | 
			
		||||
        Livewire.on('beforePaymentEventsCompleted', () => document.getElementById('payment-method-form').submit());
 | 
			
		||||
    </script>
 | 
			
		||||
@endpush
 | 
			
		||||
 | 
			
		||||
@ -72,7 +72,7 @@ Route::group(['middleware' => ['auth:contact', 'locale', 'check_client_existence
 | 
			
		||||
    Route::get('documents/{document}/download', 'ClientPortal\DocumentController@download')->name('documents.download');
 | 
			
		||||
    Route::resource('documents', 'ClientPortal\DocumentController')->only(['index', 'show']);
 | 
			
		||||
 | 
			
		||||
    Route::get('subscriptions/{subscription}/plan_switch/{target_subscription}', 'ClientPortal\SubscriptionPlanSwitchController@index')->name('subscription.plan_switch');
 | 
			
		||||
    Route::get('subscriptions/{subscription}/plan_switch/{target}', 'ClientPortal\SubscriptionPlanSwitchController@index')->name('subscription.plan_switch');
 | 
			
		||||
 | 
			
		||||
    Route::resource('subscriptions', 'ClientPortal\SubscriptionController')->only(['index']);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user