mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-11-10 21:22:58 +01:00
Merge pull request #5118 from beganovich/v5-1103-billing-landing-page
(v5) Billing page
This commit is contained in:
commit
28adccbb4c
47
app/DataMapper/Billing/WebhookConfiguration.php
Normal file
47
app/DataMapper/Billing/WebhookConfiguration.php
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?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\DataMapper\Billing;
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookConfiguration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $return_url = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $post_purchase_url = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public $post_purchase_headers = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $post_purchase_body = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public static $casts = [
|
||||||
|
'return_url' => 'string',
|
||||||
|
'post_purchase_url' => 'string',
|
||||||
|
'post_purchase_headers' => 'array',
|
||||||
|
'post_purchase_body' => 'object',
|
||||||
|
];
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
<?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\Controllers\ClientPortal;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\BillingSubscription;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class BillingSubscriptionPurchaseController extends Controller
|
||||||
|
{
|
||||||
|
public function index(BillingSubscription $billing_subscription)
|
||||||
|
{
|
||||||
|
return view('billing-portal.purchase', [
|
||||||
|
'billing_subscription' => $billing_subscription,
|
||||||
|
'hash' => Str::uuid()->toString(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -29,6 +29,7 @@ use App\Utils\Traits\MakesHash;
|
|||||||
use Illuminate\Contracts\View\Factory;
|
use Illuminate\Contracts\View\Factory;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
@ -237,11 +238,18 @@ class PaymentController extends Controller
|
|||||||
->get();
|
->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$hash_data = ['invoices' => $payable_invoices->toArray(), 'credits' => $credit_totals];
|
||||||
|
|
||||||
|
if ($request->query('hash')) {
|
||||||
|
$hash_data['billing_context'] = Cache::get($request->query('hash'));
|
||||||
|
}
|
||||||
|
|
||||||
$payment_hash = new PaymentHash;
|
$payment_hash = new PaymentHash;
|
||||||
$payment_hash->hash = Str::random(128);
|
$payment_hash->hash = Str::random(128);
|
||||||
$payment_hash->data = ['invoices' => $payable_invoices->toArray(), 'credits' => $credit_totals];
|
$payment_hash->data = $hash_data;
|
||||||
$payment_hash->fee_total = $fee_totals;
|
$payment_hash->fee_total = $fee_totals;
|
||||||
$payment_hash->fee_invoice_id = $first_invoice->id;
|
$payment_hash->fee_invoice_id = $first_invoice->id;
|
||||||
|
|
||||||
$payment_hash->save();
|
$payment_hash->save();
|
||||||
|
|
||||||
$totals = [
|
$totals = [
|
||||||
|
158
app/Http/Livewire/BillingPortalPurchase.php
Normal file
158
app/Http/Livewire/BillingPortalPurchase.php
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Livewire;
|
||||||
|
|
||||||
|
use App\Factory\ClientFactory;
|
||||||
|
use App\Models\ClientContact;
|
||||||
|
use App\Repositories\ClientContactRepository;
|
||||||
|
use App\Repositories\ClientRepository;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
class BillingPortalPurchase extends Component
|
||||||
|
{
|
||||||
|
public $hash;
|
||||||
|
|
||||||
|
public $heading_text = 'Log in';
|
||||||
|
|
||||||
|
public $email;
|
||||||
|
|
||||||
|
public $password;
|
||||||
|
|
||||||
|
public $billing_subscription;
|
||||||
|
|
||||||
|
public $contact;
|
||||||
|
|
||||||
|
protected $rules = [
|
||||||
|
'email' => ['required', 'email'],
|
||||||
|
];
|
||||||
|
|
||||||
|
public $company_gateway_id;
|
||||||
|
|
||||||
|
public $payment_method_id;
|
||||||
|
|
||||||
|
public $steps = [
|
||||||
|
'passed_email' => false,
|
||||||
|
'existing_user' => false,
|
||||||
|
'fetched_payment_methods' => false,
|
||||||
|
'fetched_client' => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
public $methods = [];
|
||||||
|
|
||||||
|
public $invoice;
|
||||||
|
|
||||||
|
public $coupon;
|
||||||
|
|
||||||
|
public function authenticate()
|
||||||
|
{
|
||||||
|
$this->validate();
|
||||||
|
|
||||||
|
$contact = ClientContact::where('email', $this->email)->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]);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createBlankClient()
|
||||||
|
{
|
||||||
|
$company = $this->billing_subscription->company;
|
||||||
|
$user = $this->billing_subscription->user;
|
||||||
|
|
||||||
|
$client_repo = new ClientRepository(new ClientContactRepository());
|
||||||
|
|
||||||
|
$client = $client_repo->save([
|
||||||
|
'name' => 'Client Name',
|
||||||
|
'contacts' => [
|
||||||
|
['email' => $this->email],
|
||||||
|
]
|
||||||
|
], ClientFactory::create($company->id, $user->id));
|
||||||
|
|
||||||
|
return $client->contacts->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getPaymentMethods(ClientContact $contact): self
|
||||||
|
{
|
||||||
|
$this->steps['fetched_payment_methods'] = true;
|
||||||
|
|
||||||
|
$this->methods = $contact->client->service()->getPaymentMethods(1000);
|
||||||
|
|
||||||
|
$this->heading_text = 'Pick a payment method';
|
||||||
|
|
||||||
|
Auth::guard('contact')->login($contact);
|
||||||
|
|
||||||
|
$this->contact = $contact;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleBeforePaymentEvents()
|
||||||
|
{
|
||||||
|
$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,
|
||||||
|
'quantity' => 1, // Option to increase quantity
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->invoice = $this->billing_subscription
|
||||||
|
->service()
|
||||||
|
->createInvoice($data)
|
||||||
|
->service()
|
||||||
|
->markSent()
|
||||||
|
->save();
|
||||||
|
|
||||||
|
Cache::put($this->hash, [
|
||||||
|
'email' => $this->email ?? $this->contact->email,
|
||||||
|
'client_id' => $this->contact->client->id,
|
||||||
|
'invoice_id' => $this->invoice->id],
|
||||||
|
now()->addMinutes(60)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->emit('beforePaymentEventsCompleted');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applyCouponCode()
|
||||||
|
{
|
||||||
|
dd('Applying coupon code: ' . $this->coupon);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
if ($this->contact instanceof ClientContact) {
|
||||||
|
$this->getPaymentMethods($this->contact);
|
||||||
|
}
|
||||||
|
|
||||||
|
return render('components.livewire.billing-portal-purchase');
|
||||||
|
}
|
||||||
|
}
|
@ -73,5 +73,4 @@ class BillingSubscription extends BaseModel
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(Product::class);
|
return $this->belongsTo(Product::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Notifications\Notification;
|
use Illuminate\Notifications\Notification;
|
||||||
use Laracasts\Presenter\PresentableTrait;
|
use Laracasts\Presenter\PresentableTrait;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
class Company extends BaseModel
|
class Company extends BaseModel
|
||||||
{
|
{
|
||||||
@ -286,7 +287,7 @@ class Company extends BaseModel
|
|||||||
*/
|
*/
|
||||||
public function country()
|
public function country()
|
||||||
{
|
{
|
||||||
//return $this->belongsTo(Country::class);
|
// return $this->belongsTo(Country::class);
|
||||||
return Country::find($this->settings->country_id);
|
return Country::find($this->settings->country_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -342,12 +343,13 @@ class Company extends BaseModel
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return BelongsTo
|
|
||||||
*/
|
|
||||||
public function currency()
|
public function currency()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Currency::class);
|
$currencies = Cache::get('currencies');
|
||||||
|
|
||||||
|
return $currencies->filter(function ($item) {
|
||||||
|
return $item->id == $this->settings->currency_id;
|
||||||
|
})->first();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -30,6 +30,7 @@ use App\Models\Invoice;
|
|||||||
use App\Models\Payment;
|
use App\Models\Payment;
|
||||||
use App\Models\PaymentHash;
|
use App\Models\PaymentHash;
|
||||||
use App\Models\SystemLog;
|
use App\Models\SystemLog;
|
||||||
|
use App\Services\BillingSubscription\BillingSubscriptionService;
|
||||||
use App\Utils\Ninja;
|
use App\Utils\Ninja;
|
||||||
use App\Utils\Traits\MakesHash;
|
use App\Utils\Traits\MakesHash;
|
||||||
use App\Utils\Traits\SystemLogTrait;
|
use App\Utils\Traits\SystemLogTrait;
|
||||||
@ -207,7 +208,7 @@ class BaseDriver extends AbstractPaymentDriver
|
|||||||
public function createPayment($data, $status = Payment::STATUS_COMPLETED): Payment
|
public function createPayment($data, $status = Payment::STATUS_COMPLETED): Payment
|
||||||
{
|
{
|
||||||
$this->confirmGatewayFee();
|
$this->confirmGatewayFee();
|
||||||
|
|
||||||
$payment = PaymentFactory::create($this->client->company->id, $this->client->user->id);
|
$payment = PaymentFactory::create($this->client->company->id, $this->client->user->id);
|
||||||
$payment->client_id = $this->client->id;
|
$payment->client_id = $this->client->id;
|
||||||
$payment->company_gateway_id = $this->company_gateway->id;
|
$payment->company_gateway_id = $this->company_gateway->id;
|
||||||
@ -240,6 +241,8 @@ class BaseDriver extends AbstractPaymentDriver
|
|||||||
|
|
||||||
event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars()));
|
event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars()));
|
||||||
|
|
||||||
|
BillingSubscriptionService::completePurchase($this->payment_hash);
|
||||||
|
|
||||||
return $payment->service()->applyNumber()->save();
|
return $payment->service()->applyNumber()->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -345,8 +348,8 @@ class BaseDriver extends AbstractPaymentDriver
|
|||||||
}
|
}
|
||||||
else if ($e instanceof Exception) {
|
else if ($e instanceof Exception) {
|
||||||
$error = $e->getMessage();
|
$error = $e->getMessage();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
$error = $e->getMessage();
|
$error = $e->getMessage();
|
||||||
|
|
||||||
PaymentFailureMailer::dispatch(
|
PaymentFailureMailer::dispatch(
|
||||||
|
@ -23,7 +23,7 @@ use Illuminate\Support\Str;
|
|||||||
class ClientContactRepository extends BaseRepository
|
class ClientContactRepository extends BaseRepository
|
||||||
{
|
{
|
||||||
public $is_primary;
|
public $is_primary;
|
||||||
|
|
||||||
public function save(array $data, Client $client) : void
|
public function save(array $data, Client $client) : void
|
||||||
{
|
{
|
||||||
if (isset($data['contacts'])) {
|
if (isset($data['contacts'])) {
|
||||||
@ -37,6 +37,7 @@ class ClientContactRepository extends BaseRepository
|
|||||||
});
|
});
|
||||||
|
|
||||||
$this->is_primary = true;
|
$this->is_primary = true;
|
||||||
|
|
||||||
/* Set first record to primary - always */
|
/* Set first record to primary - always */
|
||||||
$contacts = $contacts->sortByDesc('is_primary')->map(function ($contact) {
|
$contacts = $contacts->sortByDesc('is_primary')->map(function ($contact) {
|
||||||
$contact['is_primary'] = $this->is_primary;
|
$contact['is_primary'] = $this->is_primary;
|
||||||
|
@ -15,12 +15,13 @@ use App\DataMapper\InvoiceItem;
|
|||||||
use App\Factory\InvoiceFactory;
|
use App\Factory\InvoiceFactory;
|
||||||
use App\Models\BillingSubscription;
|
use App\Models\BillingSubscription;
|
||||||
use App\Models\ClientSubscription;
|
use App\Models\ClientSubscription;
|
||||||
|
use App\Models\PaymentHash;
|
||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
use App\Repositories\InvoiceRepository;
|
use App\Repositories\InvoiceRepository;
|
||||||
|
|
||||||
class BillingSubscriptionService
|
class BillingSubscriptionService
|
||||||
{
|
{
|
||||||
|
/** @var BillingSubscription */
|
||||||
private $billing_subscription;
|
private $billing_subscription;
|
||||||
|
|
||||||
public function __construct(BillingSubscription $billing_subscription)
|
public function __construct(BillingSubscription $billing_subscription)
|
||||||
@ -28,9 +29,8 @@ class BillingSubscriptionService
|
|||||||
$this->billing_subscription = $billing_subscription;
|
$this->billing_subscription = $billing_subscription;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createInvoice($data)
|
public function createInvoice($data): ?\App\Models\Invoice
|
||||||
{
|
{
|
||||||
|
|
||||||
$invoice_repo = new InvoiceRepository();
|
$invoice_repo = new InvoiceRepository();
|
||||||
|
|
||||||
// $data = [
|
// $data = [
|
||||||
@ -39,31 +39,31 @@ class BillingSubscriptionService
|
|||||||
// 'invitations' => [
|
// 'invitations' => [
|
||||||
// 'client_contact_id' => hashed_id
|
// 'client_contact_id' => hashed_id
|
||||||
// ],
|
// ],
|
||||||
// 'line_items' => [],
|
// 'line_items' => [],
|
||||||
// ];
|
// ];
|
||||||
|
$data['line_items'] = $this->createLineItems($data['quantity']);
|
||||||
|
|
||||||
$invoice = $invoice_repo->save($data, InvoiceFactory::create($this->billing_subscription->company_id, $this->billing_subscription->user_id));
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
If trial_enabled -> return early
|
If trial_enabled -> return early
|
||||||
|
|
||||||
-- what we need to know that we don't already
|
-- what we need to know that we don't already
|
||||||
-- Has a promo code been entered, and does it match
|
-- Has a promo code been entered, and does it match
|
||||||
-- Is this a recurring subscription
|
-- Is this a recurring subscription
|
||||||
--
|
--
|
||||||
|
|
||||||
1. Is this a recurring product?
|
1. Is this a recurring product?
|
||||||
2. What is the quantity? ie is this a multi seat product ( does this mean we need this value stored in the client sub?)
|
2. What is the quantity? ie is this a multi seat product ( does this mean we need this value stored in the client sub?)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
return $invoice;
|
return $invoice_repo->save($data, InvoiceFactory::create($this->billing_subscription->company_id, $this->billing_subscription->user_id));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createLineItems($quantity)
|
private function createLineItems($quantity): array
|
||||||
{
|
{
|
||||||
$line_items = [];
|
$line_items = [];
|
||||||
|
|
||||||
$product = $this->billing_subscription->product;
|
$product = $this->billing_subscription->product;
|
||||||
|
|
||||||
$item = new InvoiceItem;
|
$item = new InvoiceItem;
|
||||||
@ -89,7 +89,7 @@ class BillingSubscriptionService
|
|||||||
public function createClientSubscription($payment_hash, $recurring_invoice_id = null)
|
public function createClientSubscription($payment_hash, $recurring_invoice_id = null)
|
||||||
{
|
{
|
||||||
//create the client sub record
|
//create the client sub record
|
||||||
|
|
||||||
//?trial enabled?
|
//?trial enabled?
|
||||||
$cs = new ClientSubscription();
|
$cs = new ClientSubscription();
|
||||||
$cs->subscription_id = $this->billing_subscription->id;
|
$cs->subscription_id = $this->billing_subscription->id;
|
||||||
@ -108,4 +108,14 @@ class BillingSubscriptionService
|
|||||||
{
|
{
|
||||||
//scan for any notification we are required to send
|
//scan for any notification we are required to send
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function completePurchase(PaymentHash $payment_hash)
|
||||||
|
{
|
||||||
|
if (!property_exists($payment_hash, 'billing_context')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point we have some state carried from the billing page
|
||||||
|
// to this, available as $payment_hash->data->billing_context. Make something awesome ⭐
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,6 +57,7 @@ class BillingSubscriptionTransformer extends EntityTransformer
|
|||||||
'plan_map' => (string)$billing_subscription->plan_map,
|
'plan_map' => (string)$billing_subscription->plan_map,
|
||||||
'refund_period' => (int)$billing_subscription->refund_period,
|
'refund_period' => (int)$billing_subscription->refund_period,
|
||||||
'webhook_configuration' => (string)$billing_subscription->webhook_configuration,
|
'webhook_configuration' => (string)$billing_subscription->webhook_configuration,
|
||||||
|
'purchase_page' => (string)route('client.subscription.purchase', $billing_subscription->hashed_id),
|
||||||
'is_deleted' => (bool)$billing_subscription->is_deleted,
|
'is_deleted' => (bool)$billing_subscription->is_deleted,
|
||||||
'created_at' => (int)$billing_subscription->created_at,
|
'created_at' => (int)$billing_subscription->created_at,
|
||||||
'updated_at' => (int)$billing_subscription->updated_at,
|
'updated_at' => (int)$billing_subscription->updated_at,
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
namespace App\Utils;
|
namespace App\Utils;
|
||||||
|
|
||||||
|
use App\Models\Company;
|
||||||
use App\Models\Currency;
|
use App\Models\Currency;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -83,17 +84,17 @@ class Number
|
|||||||
|
|
||||||
return floatval($value);
|
return floatval($value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats a given value based on the clients currency AND country.
|
* Formats a given value based on the clients currency AND country.
|
||||||
*
|
*
|
||||||
* @param floatval $value The number to be formatted
|
* @param floatval $value The number to be formatted
|
||||||
* @param $client
|
* @param $entity
|
||||||
* @return string The formatted value
|
* @return string The formatted value
|
||||||
*/
|
*/
|
||||||
public static function formatMoney($value, $client) :string
|
public static function formatMoney($value, $entity) :string
|
||||||
{
|
{
|
||||||
$currency = $client->currency();
|
$currency = $entity->currency();
|
||||||
|
|
||||||
$thousand = $currency->thousand_separator;
|
$thousand = $currency->thousand_separator;
|
||||||
$decimal = $currency->decimal_separator;
|
$decimal = $currency->decimal_separator;
|
||||||
@ -101,29 +102,38 @@ class Number
|
|||||||
$code = $currency->code;
|
$code = $currency->code;
|
||||||
$swapSymbol = $currency->swap_currency_symbol;
|
$swapSymbol = $currency->swap_currency_symbol;
|
||||||
|
|
||||||
|
// App\Models\Client::country() returns instance of BelongsTo.
|
||||||
|
// App\Models\Company::country() returns record for the country, that's why we check for the instance.
|
||||||
|
|
||||||
|
if ($entity instanceof Company) {
|
||||||
|
$country = $entity->country();
|
||||||
|
} else {
|
||||||
|
$country = $entity->country;
|
||||||
|
}
|
||||||
|
|
||||||
/* Country settings override client settings */
|
/* Country settings override client settings */
|
||||||
if (isset($client->country->thousand_separator) && strlen($client->country->thousand_separator) >= 1) {
|
if (isset($country->thousand_separator) && strlen($country->thousand_separator) >= 1) {
|
||||||
$thousand = $client->country->thousand_separator;
|
$thousand = $country->thousand_separator;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($client->country->decimal_separator) && strlen($client->country->decimal_separator) >= 1) {
|
if (isset($country->decimal_separator) && strlen($country->decimal_separator) >= 1) {
|
||||||
$decimal = $client->country->decimal_separator;
|
$decimal = $country->decimal_separator;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($client->country->swap_currency_symbol) && strlen($client->country->swap_currency_symbol) >= 1) {
|
if (isset($country->swap_currency_symbol) && strlen($country->swap_currency_symbol) >= 1) {
|
||||||
$swapSymbol = $client->country->swap_currency_symbol;
|
$swapSymbol = $country->swap_currency_symbol;
|
||||||
}
|
}
|
||||||
|
|
||||||
$value = number_format($value, $precision, $decimal, $thousand);
|
$value = number_format($value, $precision, $decimal, $thousand);
|
||||||
$symbol = $currency->symbol;
|
$symbol = $currency->symbol;
|
||||||
|
|
||||||
if ($client->getSetting('show_currency_code') === true && $currency->code == 'CHF') {
|
if ($entity->getSetting('show_currency_code') === true && $currency->code == 'CHF') {
|
||||||
return "{$code} {$value}";
|
return "{$code} {$value}";
|
||||||
} elseif ($client->getSetting('show_currency_code') === true) {
|
} elseif ($entity->getSetting('show_currency_code') === true) {
|
||||||
return "{$value} {$code}";
|
return "{$value} {$code}";
|
||||||
} elseif ($swapSymbol) {
|
} elseif ($swapSymbol) {
|
||||||
return "{$value} ".trim($symbol);
|
return "{$value} ".trim($symbol);
|
||||||
} elseif ($client->getSetting('show_currency_code') === false) {
|
} elseif ($entity->getSetting('show_currency_code') === false) {
|
||||||
return "{$symbol}{$value}";
|
return "{$symbol}{$value}";
|
||||||
} else {
|
} else {
|
||||||
return self::formatValue($value, $currency);
|
return self::formatValue($value, $currency);
|
||||||
|
@ -65,6 +65,7 @@
|
|||||||
"predis/predis": "^1.1",
|
"predis/predis": "^1.1",
|
||||||
"sentry/sentry-laravel": "^2",
|
"sentry/sentry-laravel": "^2",
|
||||||
"stripe/stripe-php": "^7.50",
|
"stripe/stripe-php": "^7.50",
|
||||||
|
"symfony/http-client": "^5.2",
|
||||||
"turbo124/beacon": "^1.0",
|
"turbo124/beacon": "^1.0",
|
||||||
"webpatser/laravel-countries": "dev-master#75992ad",
|
"webpatser/laravel-countries": "dev-master#75992ad",
|
||||||
"wildbit/swiftmailer-postmark": "^3.3"
|
"wildbit/swiftmailer-postmark": "^3.3"
|
||||||
|
2
public/css/app.css
vendored
2
public/css/app.css
vendored
File diff suppressed because one or more lines are too long
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"/js/app.js": "/js/app.js?id=696e8203d5e8e7cf5ff5",
|
"/js/app.js": "/js/app.js?id=696e8203d5e8e7cf5ff5",
|
||||||
"/css/app.css": "/css/app.css?id=745170b7d7a4dc7469f2",
|
"/css/app.css": "/css/app.css?id=e8d6d5e8cb60bc2f15b3",
|
||||||
"/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js?id=a09bb529b8e1826f13b4",
|
"/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js?id=a09bb529b8e1826f13b4",
|
||||||
"/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js?id=8ce8955ba775ea5f47d1",
|
"/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js?id=8ce8955ba775ea5f47d1",
|
||||||
"/js/clients/linkify-urls.js": "/js/clients/linkify-urls.js?id=0dc8c34010d09195d2f7",
|
"/js/clients/linkify-urls.js": "/js/clients/linkify-urls.js?id=0dc8c34010d09195d2f7",
|
||||||
|
17
resources/views/billing-portal/purchase.blade.php
Normal file
17
resources/views/billing-portal/purchase.blade.php
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
@extends('portal.ninja2020.layout.clean')
|
||||||
|
@section('meta_title', $billing_subscription->product->product_key)
|
||||||
|
|
||||||
|
@section('body')
|
||||||
|
@livewire('billing-portal-purchase', ['billing_subscription' => $billing_subscription, 'contact' => auth('contact')->user(), 'hash' => $hash])
|
||||||
|
@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
|
@ -8,7 +8,9 @@
|
|||||||
@include('portal.ninja2020.components.general.sidebar.mobile')
|
@include('portal.ninja2020.components.general.sidebar.mobile')
|
||||||
|
|
||||||
<!-- Static sidebar for desktop -->
|
<!-- Static sidebar for desktop -->
|
||||||
@include('portal.ninja2020.components.general.sidebar.desktop')
|
@unless(request()->query('sidebar') === 'hidden')
|
||||||
|
@include('portal.ninja2020.components.general.sidebar.desktop')
|
||||||
|
@endunless
|
||||||
|
|
||||||
<div class="flex flex-col w-0 flex-1 overflow-hidden">
|
<div class="flex flex-col w-0 flex-1 overflow-hidden">
|
||||||
@include('portal.ninja2020.components.general.sidebar.header')
|
@include('portal.ninja2020.components.general.sidebar.header')
|
||||||
@ -34,4 +36,4 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -0,0 +1,127 @@
|
|||||||
|
<div class="grid grid-cols-12">
|
||||||
|
<div class="col-span-12 lg:col-span-6 bg-gray-50 shadow-lg lg:h-screen flex flex-col items-center">
|
||||||
|
<div class="w-full p-10 lg:w-1/2 lg:mt-48 lg:p-0">
|
||||||
|
<img class="h-8" src="{{ $billing_subscription->company->present()->logo }}"
|
||||||
|
alt="{{ $billing_subscription->company->present()->name }}">
|
||||||
|
|
||||||
|
<h1 id="billing-page-company-logo" class="text-3xl font-bold tracking-wide mt-8">
|
||||||
|
{{ $billing_subscription->product->product_key }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p class="my-6">{{ $billing_subscription->product->notes }}</p>
|
||||||
|
|
||||||
|
<span class="text-sm uppercase font-bold">{{ ctrans('texts.total') }}:</span>
|
||||||
|
|
||||||
|
<h1 class="text-2xl font-bold tracking-wide">{{ App\Utils\Number::formatMoney($billing_subscription->product->price, $billing_subscription->company) }}</h1>
|
||||||
|
|
||||||
|
@if(auth('contact')->user())
|
||||||
|
<a href="{{ route('client.invoices.index') }}" class="block mt-16 inline-flex items-center space-x-2">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none"
|
||||||
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
class="feather feather-arrow-left">
|
||||||
|
<line x1="19" y1="12" x2="5" y2="12"></line>
|
||||||
|
<polyline points="12 19 5 12 12 5"></polyline>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<span>{{ ctrans('texts.client_portal') }}</span>
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-12 lg:col-span-6 bg-white lg:shadow-lg lg:h-screen">
|
||||||
|
<div class="grid grid-cols-12 flex flex-col p-10 lg:mt-48 lg:ml-16">
|
||||||
|
<div class="col-span-12 w-full lg:col-span-6">
|
||||||
|
<h2 class="text-2xl font-bold tracking-wide">{{ $heading_text }}</h2>
|
||||||
|
@if (session()->has('message'))
|
||||||
|
@component('portal.ninja2020.components.message')
|
||||||
|
{{ session('message') }}
|
||||||
|
@endcomponent
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($this->steps['fetched_payment_methods'])
|
||||||
|
<div class="flex items-center mt-4 text-sm">
|
||||||
|
<form action="{{ route('client.payments.process', ['hash' => $hash, 'sidebar' => 'hidden']) }}"
|
||||||
|
method="post"
|
||||||
|
id="payment-method-form">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
@if($invoice instanceof \App\Models\Invoice)
|
||||||
|
<input type="hidden" name="invoices[]" value="{{ $invoice->hashed_id }}">
|
||||||
|
<input type="hidden" name="payable_invoices[0][amount]"
|
||||||
|
value="{{ $invoice->partial > 0 ? \App\Utils\Number::formatValue($invoice->partial, $invoice->client->currency()) : \App\Utils\Number::formatValue($invoice->balance, $invoice->client->currency()) }}">
|
||||||
|
<input type="hidden" name="payable_invoices[0][invoice_id]"
|
||||||
|
value="{{ $invoice->hashed_id }}">
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<input type="hidden" name="action" value="payment">
|
||||||
|
<input type="hidden" name="company_gateway_id" value="{{ $company_gateway_id }}"/>
|
||||||
|
<input type="hidden" name="payment_method_id" value="{{ $payment_method_id }}"/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
@foreach($this->methods as $method)
|
||||||
|
<button
|
||||||
|
wire:click="handleMethodSelectingEvent('{{ $method['company_gateway_id'] }}', '{{ $method['gateway_type_id'] }}')"
|
||||||
|
class="px-3 py-2 border rounded mr-4 hover:border-blue-600">
|
||||||
|
{{ $method['label'] }}
|
||||||
|
</button>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<form wire:submit.prevent="authenticate" class="mt-8">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<label for="email_address">
|
||||||
|
<span class="input-label">{{ ctrans('texts.email_address') }}</span>
|
||||||
|
<input wire:model.defer="email" type="email" class="input w-full"/>
|
||||||
|
|
||||||
|
@error('email')
|
||||||
|
<p class="validation validation-fail block w-full" role="alert">
|
||||||
|
{{ $message }}
|
||||||
|
</p>
|
||||||
|
@enderror
|
||||||
|
</label>
|
||||||
|
|
||||||
|
@if($steps['existing_user'])
|
||||||
|
<label for="password" class="block mt-2">
|
||||||
|
<span class="input-label">{{ ctrans('texts.password') }}</span>
|
||||||
|
<input wire:model.defer="password" type="password" class="input w-full" autofocus/>
|
||||||
|
|
||||||
|
@error('password')
|
||||||
|
<p class="validation validation-fail block w-full" role="alert">
|
||||||
|
{{ $message }}
|
||||||
|
</p>
|
||||||
|
@enderror
|
||||||
|
</label>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<button type="submit"
|
||||||
|
class="button button-block bg-primary text-white mt-4">{{ ctrans('texts.next') }}</button>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<span class="px-2 text-gray-700 bg-white">Have a coupon code?</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form wire:submit.prevent="applyCouponCode" class="mt-4">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
<label class="w-full mr-2">
|
||||||
|
<input type="text" wire:model.defer="coupon" class="input w-full m-0" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button class="button bg-primary m-0 text-white">{{ ctrans('texts.apply') }}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -63,6 +63,8 @@
|
|||||||
{{-- Feel free to push anything to header using @push('header') --}}
|
{{-- Feel free to push anything to header using @push('header') --}}
|
||||||
@stack('head')
|
@stack('head')
|
||||||
|
|
||||||
|
@livewireStyles
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/cookieconsent@3/build/cookieconsent.min.css" />
|
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/cookieconsent@3/build/cookieconsent.min.css" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@ -77,6 +79,8 @@
|
|||||||
|
|
||||||
@yield('body')
|
@yield('body')
|
||||||
|
|
||||||
|
@livewireScripts
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/cookieconsent@3/build/cookieconsent.min.js" data-cfasync="false"></script>
|
<script src="https://cdn.jsdelivr.net/npm/cookieconsent@3/build/cookieconsent.min.js" data-cfasync="false"></script>
|
||||||
<script>
|
<script>
|
||||||
window.addEventListener("load", function(){
|
window.addEventListener("load", function(){
|
||||||
|
@ -31,10 +31,13 @@
|
|||||||
<div>
|
<div>
|
||||||
@yield('gateway_content')
|
@yield('gateway_content')
|
||||||
</div>
|
</div>
|
||||||
<span class="block mx-4 mb-4 text-xs inline-flex items-center">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-600"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
|
@if(Request::isSecure())
|
||||||
<span class="ml-1">Secure 256-bit encryption</span>
|
<span class="block mx-4 mb-4 text-xs inline-flex items-center">
|
||||||
</span>
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-600"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
|
||||||
|
<span class="ml-1">Secure 256-bit encryption</span>
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
@ -76,6 +76,8 @@ Route::group(['middleware' => ['auth:contact', 'locale', 'check_client_existence
|
|||||||
Route::get('logout', 'Auth\ContactLoginController@logout')->name('logout');
|
Route::get('logout', 'Auth\ContactLoginController@logout')->name('logout');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Route::get('client/subscription/{billing_subscription}/purchase', 'ClientPortal\BillingSubscriptionPurchaseController@index')->name('client.subscription.purchase');
|
||||||
|
|
||||||
Route::group(['middleware' => ['invite_db'], 'prefix' => 'client', 'as' => 'client.'], function () {
|
Route::group(['middleware' => ['invite_db'], 'prefix' => 'client', 'as' => 'client.'], function () {
|
||||||
/*Invitation catches*/
|
/*Invitation catches*/
|
||||||
Route::get('recurring_invoice/{invitation_key}', 'ClientPortal\InvitationController@recurringRouter');
|
Route::get('recurring_invoice/{invitation_key}', 'ClientPortal\InvitationController@recurringRouter');
|
||||||
|
Loading…
Reference in New Issue
Block a user