This commit is contained in:
Benjamin Beganović 2021-03-17 16:12:25 +01:00
parent 81f5808bf6
commit 127c6cb3cd
10 changed files with 170 additions and 50 deletions

View 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',
];
}

View File

@ -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 = [

View File

@ -7,6 +7,7 @@ use App\Models\ClientContact;
use App\Repositories\ClientContactRepository; use App\Repositories\ClientContactRepository;
use App\Repositories\ClientRepository; use App\Repositories\ClientRepository;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Livewire\Component; use Livewire\Component;
class BillingPortalPurchase extends Component class BillingPortalPurchase extends Component
@ -42,6 +43,8 @@ class BillingPortalPurchase extends Component
public $invoice; public $invoice;
public $coupon;
public function authenticate() public function authenticate()
{ {
$this->validate(); $this->validate();
@ -88,8 +91,6 @@ class BillingPortalPurchase extends Component
protected function getPaymentMethods(ClientContact $contact): self protected function getPaymentMethods(ClientContact $contact): self
{ {
// Cache::put($this->hash, ['email' => $this->email ?? $this->contact->email, 'url' => url()->current()]);
$this->steps['fetched_payment_methods'] = true; $this->steps['fetched_payment_methods'] = true;
$this->methods = $contact->client->service()->getPaymentMethods(1000); $this->methods = $contact->client->service()->getPaymentMethods(1000);
@ -120,7 +121,7 @@ class BillingPortalPurchase extends Component
'key' => '', 'key' => '',
'client_contact_id' => $this->contact->hashed_id, 'client_contact_id' => $this->contact->hashed_id,
]], ]],
'user_input_promo_code' => '', // Field to input the promo code, 'user_input_promo_code' => $this->coupon,
'quantity' => 1, // Option to increase quantity 'quantity' => 1, // Option to increase quantity
]; ];
@ -131,9 +132,21 @@ class BillingPortalPurchase extends Component
->markSent() ->markSent()
->save(); ->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'); $this->emit('beforePaymentEventsCompleted');
} }
public function applyCouponCode()
{
dd('Applying coupon code: ' . $this->coupon);
}
public function render() public function render()
{ {
if ($this->contact instanceof ClientContact) { if ($this->contact instanceof ClientContact) {

View File

@ -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();
} }
/** /**

View File

@ -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(

View File

@ -15,6 +15,7 @@ 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;
@ -107,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 ⭐
}
} }

View File

@ -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);

2
public/css/app.css vendored

File diff suppressed because one or more lines are too long

View File

@ -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=8d3e488939aa216c7a1c", "/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",

View File

@ -1,28 +1,31 @@
<div class="grid grid-cols-12"> <div class="grid grid-cols-12">
<!-- Left side with payment/product information. -->
<div class="col-span-12 lg:col-span-6 bg-gray-50 shadow-lg lg:h-screen flex flex-col items-center"> <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"> <div class="w-full p-10 lg:w-1/2 lg:mt-48 lg:p-0">
<h1 class="text-3xl font-bold tracking-wide">Summary</h1> <img class="h-8" src="{{ $billing_subscription->company->present()->logo }}"
<p class="text-gray-800 tracking-wide text-sm">A brief overview of the order</p> alt="{{ $billing_subscription->company->present()->name }}">
<p class="my-6">Lorem ipsum dolor sit amet, consectetur adipisicing elit. Culpa earum eos explicabo labore <h1 id="billing-page-company-logo" class="text-3xl font-bold tracking-wide mt-8">
laboriosam numquam officia pariatur recusandae repellat. Aliquam aliquid amet dignissimos facere iste, {{ $billing_subscription->product->product_key }}
provident sed voluptas! Consequuntur ea expedita magnam maiores nisi rem saepe suscipit. At autem, </h1>
expedita explicabo fugiat ipsam maiores modi, odit quae quia quos, voluptatum!</p>
<span class="text-sm uppercase font-bold">Total:</span> <p class="my-6">{{ $billing_subscription->product->notes }}</p>
<h1 class="text-2xl font-bold tracking-wide">$4,000</h1>
<a href="#" class="block mt-16 inline-flex items-center space-x-2"> <span class="text-sm uppercase font-bold">{{ ctrans('texts.total') }}:</span>
<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>Go back</span> <h1 class="text-2xl font-bold tracking-wide">{{ App\Utils\Number::formatMoney($billing_subscription->product->price, $billing_subscription->company) }}</h1>
</a>
@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> </div>
@ -46,7 +49,7 @@
@if($invoice instanceof \App\Models\Invoice) @if($invoice instanceof \App\Models\Invoice)
<input type="hidden" name="invoices[]" value="{{ $invoice->hashed_id }}"> <input type="hidden" name="invoices[]" value="{{ $invoice->hashed_id }}">
<input type="hidden" name="payable_invoices[0][amount]" <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()) }}"> 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]" <input type="hidden" name="payable_invoices[0][invoice_id]"
value="{{ $invoice->hashed_id }}"> value="{{ $invoice->hashed_id }}">
@endif @endif
@ -59,7 +62,7 @@
@foreach($this->methods as $method) @foreach($this->methods as $method)
<button <button
wire:click="handleMethodSelectingEvent('{{ $method['company_gateway_id'] }}', '{{ $method['gateway_type_id'] }}')" wire:click="handleMethodSelectingEvent('{{ $method['company_gateway_id'] }}', '{{ $method['gateway_type_id'] }}')"
class="p-4 border rounded mr-4 hover:border-blue-600"> class="px-3 py-2 border rounded mr-4 hover:border-blue-600">
{{ $method['label'] }} {{ $method['label'] }}
</button> </button>
@endforeach @endforeach
@ -69,7 +72,7 @@
@csrf @csrf
<label for="email_address"> <label for="email_address">
<span class="input-label">E-mail address</span> <span class="input-label">{{ ctrans('texts.email_address') }}</span>
<input wire:model.defer="email" type="email" class="input w-full"/> <input wire:model.defer="email" type="email" class="input w-full"/>
@error('email') @error('email')
@ -81,7 +84,7 @@
@if($steps['existing_user']) @if($steps['existing_user'])
<label for="password" class="block mt-2"> <label for="password" class="block mt-2">
<span class="input-label">Password</span> <span class="input-label">{{ ctrans('texts.password') }}</span>
<input wire:model.defer="password" type="password" class="input w-full" autofocus/> <input wire:model.defer="password" type="password" class="input w-full" autofocus/>
@error('password') @error('password')
@ -92,9 +95,32 @@
</label> </label>
@endif @endif
<button type="submit" class="button button-block bg-primary text-white mt-4">Next</button> <button type="submit"
class="button button-block bg-primary text-white mt-4">{{ ctrans('texts.next') }}</button>
</form> </form>
@endif @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>
</div> </div>