Merge pull request #6993 from turbo124/v5-develop

WePay fixes
This commit is contained in:
David Bomba 2021-11-24 21:07:20 +11:00 committed by GitHub
commit cb07c1df9c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 256 additions and 19 deletions

View File

@ -18,14 +18,20 @@ use App\Libraries\MultiDB;
use App\Models\Account; use App\Models\Account;
use App\Models\ClientContact; use App\Models\ClientContact;
use App\Models\Company; use App\Models\Company;
use App\Models\Invoice;
use App\Models\RecurringInvoice;
use App\Models\Subscription;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use Illuminate\Contracts\Routing\ResponseFactory; use Illuminate\Contracts\Routing\ResponseFactory;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
class NinjaPlanController extends Controller class NinjaPlanController extends Controller
{ {
use MakesHash;
public function index(string $contact_key, string $account_or_company_key) public function index(string $contact_key, string $account_or_company_key)
{ {
@ -57,4 +63,82 @@ class NinjaPlanController extends Controller
return redirect()->route('client.catchall'); return redirect()->route('client.catchall');
} }
public function plan()
{
//harvest the current plan
$data = [];
if(MultiDB::findAndSetDbByAccountKey(Auth::guard('contact')->user()->client->custom_value2))
{
$account = Account::where('key', Auth::guard('contact')->user()->client->custom_value2)->first();
if($account)
{
if(Carbon::parse($account->plan_expires)->lt(now())){
//expired get the most recent invoice for payment
$late_invoice = Invoice::on('db-ninja-01')
->where('company_id', Auth::guard('contact')->user()->company->id)
->where('client_id', Auth::guard('contact')->user()->client->id)
->where('status_id', Invoice::STATUS_SENT)
->whereNotNull('subscription_id')
->orderBy('id', 'DESC')
->first();
//account status means user cannot perform upgrades until they pay their account.
$data['late_invoice'] = $late_invoice;
}
$recurring_invoice = RecurringInvoice::on('db-ninja-01')
->where('client_id', auth('contact')->user()->client->id)
->where('company_id', Auth::guard('contact')->user()->company->id)
->whereNotNull('subscription_id')
->where('status_id', RecurringInvoice::STATUS_ACTIVE)
->orderBy('id', 'desc')
->first();
$monthly_plans = Subscription::on('db-ninja-01')
->where('company_id', Auth::guard('contact')->user()->company->id)
->where('group_id', 6)
->orderBy('promo_price', 'ASC')
->get();
$yearly_plans = Subscription::on('db-ninja-01')
->where('company_id', Auth::guard('contact')->user()->company->id)
->where('group_id', 31)
->orderBy('promo_price', 'ASC')
->get();
$monthly_plans = $monthly_plans->merge($yearly_plans);
$current_subscription_id = $recurring_invoice ? $this->encodePrimaryKey($recurring_invoice->subscription_id) : false;
//remove existing subscription
if($current_subscription_id){
$monthly_plans = $monthly_plans->filter(function ($plan) use($current_subscription_id){
return (string)$plan->hashed_id != (string)$current_subscription_id;
});
}
$data['account'] = $account;
$data['client'] = Auth::guard('contact')->user()->client;
$data['plans'] = $monthly_plans;
$data['current_subscription_id'] = $current_subscription_id;
$data['current_recurring_id'] = $recurring_invoice ? $recurring_invoice->hashed_id : false;
return $this->render('plan.index', $data);
}
}
else
return redirect()->route('client.catchall');
}
} }

View File

@ -60,8 +60,8 @@ class RequiredClientInfo extends Component
'contact_first_name' => 'first_name', 'contact_first_name' => 'first_name',
'contact_last_name' => 'last_name', 'contact_last_name' => 'last_name',
'contact_email' => 'email', // 'contact_email' => 'email',
'contact_phone' => 'phone', // 'contact_phone' => 'phone',
]; ];
public $show_form = false; public $show_form = false;
@ -141,7 +141,7 @@ class RequiredClientInfo extends Component
$_field = $this->mappings[$field['name']]; $_field = $this->mappings[$field['name']];
if (Str::startsWith($field['name'], 'client_')) { if (Str::startsWith($field['name'], 'client_')) {
if (empty($this->contact->client->{$_field}) || is_null($this->contact->client->{$_field}) || $this->contact->client->{$_field} = 840) { if (empty($this->contact->client->{$_field}) || is_null($this->contact->client->{$_field}) || $this->contact->client->{$_field} == 840) {
$this->show_form = true; $this->show_form = true;
} else { } else {
$this->fields[$index]['filled'] = true; $this->fields[$index]['filled'] = true;
@ -149,7 +149,7 @@ class RequiredClientInfo extends Component
} }
if (Str::startsWith($field['name'], 'contact_')) { if (Str::startsWith($field['name'], 'contact_')) {
if ((empty($this->contact->{$_field}) || is_null($this->contact->{$_field})) || $this->contact->client->{$_field} = 840) { if ((empty($this->contact->{$_field}) || is_null($this->contact->{$_field})) || $this->contact->client->{$_field} == 840) {
$this->show_form = true; $this->show_form = true;
} else { } else {
$this->fields[$index]['filled'] = true; $this->fields[$index]['filled'] = true;

View File

@ -38,6 +38,7 @@ class SubscriptionRecurringInvoicesTable extends Component
->where('client_id', auth('contact')->user()->client->id) ->where('client_id', auth('contact')->user()->client->id)
->where('company_id', $this->company->id) ->where('company_id', $this->company->id)
->whereNotNull('subscription_id') ->whereNotNull('subscription_id')
->where('is_deleted', false)
->where('status_id', RecurringInvoice::STATUS_ACTIVE) ->where('status_id', RecurringInvoice::STATUS_ACTIVE)
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc') ->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->withTrashed() ->withTrashed()

View File

@ -121,6 +121,10 @@ class PortalComposer
$data[] = ['title' => ctrans('texts.statement'), 'url' => 'client.statement', 'icon' => 'activity']; $data[] = ['title' => ctrans('texts.statement'), 'url' => 'client.statement', 'icon' => 'activity'];
if(Ninja::isHosted() && auth('contact')->user()->company->id == config('ninja.ninja_default_company_id'))
$data[] = ['title' => ctrans('texts.plan'), 'url' => 'client.plan', 'icon' => 'credit-card'];
return $data; return $data;
} }
} }

View File

@ -188,9 +188,9 @@ class CreateRawPdf implements ShouldQueue
nlog(print_r($e->getMessage(), 1)); nlog(print_r($e->getMessage(), 1));
} }
if (config('ninja.log_pdf_html')) { // if (config('ninja.log_pdf_html')) {
info($maker->getCompiledHTML()); info($maker->getCompiledHTML());
} // }
if ($pdf) if ($pdf)
return $pdf; return $pdf;

View File

@ -136,7 +136,10 @@ class CreditCard
return $this->processSuccessfulPayment($result); return $this->processSuccessfulPayment($result);
} }
return $this->processUnsuccessfulPayment($result); $error = 'Undefined gateway error';
return $this->processUnsuccessfulPayment($error);
} }
private function getPaymentToken(array $data, $customerId): ?string private function getPaymentToken(array $data, $customerId): ?string

View File

@ -294,7 +294,7 @@ class MolliePaymentDriver extends BaseDriver
} }
$this->init(); $this->init();
$codes = [ $codes = [
'open' => Payment::STATUS_PENDING, 'open' => Payment::STATUS_PENDING,
'canceled' => Payment::STATUS_CANCELLED, 'canceled' => Payment::STATUS_CANCELLED,
@ -312,6 +312,9 @@ class MolliePaymentDriver extends BaseDriver
$client = $record->client; $client = $record->client;
} }
else{ else{
nlog("mollie webhook");
nlog($payment);
$client = Client::withTrashed()->find($this->decodePrimaryKey($payment->metadata->client_id)); $client = Client::withTrashed()->find($this->decodePrimaryKey($payment->metadata->client_id));
} }

View File

@ -231,12 +231,24 @@ class ACH
$this->stripe->payment_hash->data = array_merge((array)$this->stripe->payment_hash->data, $state); $this->stripe->payment_hash->data = array_merge((array)$this->stripe->payment_hash->data, $state);
$this->stripe->payment_hash->save(); $this->stripe->payment_hash->save();
$amount = array_sum(array_column($this->stripe->payment_hash->invoices(), 'amount')) + $this->stripe->payment_hash->fee_total;
$invoice = Invoice::whereIn('id', $this->transformKeys(array_column($this->stripe->payment_hash->invoices(), 'invoice_id')))
->withTrashed()
->first();
if ($invoice) {
$description = "Invoice {$invoice->number} for {$amount} for client {$this->stripe->client->present()->name()}";
} else {
$description = "Payment with no invoice for amount {$amount} for client {$this->stripe->client->present()->name()}";
}
try { try {
$state['charge'] = \Stripe\Charge::create([ $state['charge'] = \Stripe\Charge::create([
'amount' => $state['amount'], 'amount' => $state['amount'],
'currency' => $state['currency'], 'currency' => $state['currency'],
'customer' => $state['customer'], 'customer' => $state['customer'],
'source' => $state['source'], 'source' => $state['source'],
'description' => $description,
], $this->stripe->stripe_connect_auth); ], $this->stripe->stripe_connect_auth);
$state = array_merge($state, $request->all()); $state = array_merge($state, $request->all());

View File

@ -208,7 +208,7 @@ class WePayPaymentDriver extends BaseDriver
return 'Processed successfully'; return 'Processed successfully';
} elseif ($objectType == 'account') { } elseif ($objectType == 'account') {
if ($accountId !== $objectId) { if ($accountId != $objectId) {
throw new \Exception('Unknown account ' . $accountId . ' does not equal '.$objectId); throw new \Exception('Unknown account ' . $accountId . ' does not equal '.$objectId);
} }

View File

@ -18,6 +18,7 @@ use App\Factory\InvoiceToRecurringInvoiceFactory;
use App\Factory\RecurringInvoiceFactory; use App\Factory\RecurringInvoiceFactory;
use App\Jobs\Util\SubscriptionWebhookHandler; use App\Jobs\Util\SubscriptionWebhookHandler;
use App\Jobs\Util\SystemLogger; use App\Jobs\Util\SystemLogger;
use App\Libraries\MultiDB;
use App\Models\Client; use App\Models\Client;
use App\Models\ClientContact; use App\Models\ClientContact;
use App\Models\Credit; use App\Models\Credit;
@ -240,10 +241,10 @@ class SubscriptionService
elseif ($outstanding->count() > 1) { elseif ($outstanding->count() > 1) {
//user is changing plan mid frequency cycle //user is changing plan mid frequency cycle
//we cannot handle this if there are more than one invoice outstanding. //we cannot handle this if there are more than one invoice outstanding.
return null; return $target->price;
} }
return null; return $target->price;
} }
@ -439,7 +440,7 @@ class SubscriptionService
$credit = false; $credit = false;
/* Only generate a credit if the previous invoice was paid in full. */ /* Only generate a credit if the previous invoice was paid in full. */
if($last_invoice->balance == 0) if($last_invoice && $last_invoice->balance == 0)
$credit = $this->createCredit($last_invoice, $target_subscription, $is_credit); $credit = $this->createCredit($last_invoice, $target_subscription, $is_credit);
$new_recurring_invoice = $this->createNewRecurringInvoice($recurring_invoice); $new_recurring_invoice = $this->createNewRecurringInvoice($recurring_invoice);

View File

@ -4340,6 +4340,7 @@ $LANG = array(
'no_available_methods' => 'We can\'t find any credit cards on your device. <a href="https://invoiceninja.github.io/docs/payments#apple-pay-google-pay-microsoft-pay" target="_blank" class="underline">Read more about this.</a>', 'no_available_methods' => 'We can\'t find any credit cards on your device. <a href="https://invoiceninja.github.io/docs/payments#apple-pay-google-pay-microsoft-pay" target="_blank" class="underline">Read more about this.</a>',
'gocardless_mandate_not_ready' => 'Payment mandate is not ready. Please try again later.', 'gocardless_mandate_not_ready' => 'Payment mandate is not ready. Please try again later.',
'payment_type_instant_bank_pay' => 'Instant Bank Pay', 'payment_type_instant_bank_pay' => 'Instant Bank Pay',
); );
return $LANG; return $LANG;

View File

@ -2,8 +2,8 @@
@import url($font_url); @import url($font_url);
:root { :root {
--primary-color: #298aab; --primary-color: $primary_color;
--secondary-color: #7081e0; --secondary-color: $secondary_color;
} }
body { body {
@ -149,13 +149,13 @@
#footer, #footer-spacer { #footer, #footer-spacer {
height: 220px; height: 220px;
padding: 1rem 3rem; padding: 1rem 1.5rem;
margin-top: 1rem; margin-top: 1rem;
} }
.footer-content { .footer-content {
display: flex; display: flex;
gap: 20px; gap: 10px;
width: 100%; width: 100%;
/* grid-template-columns: 1fr 1fr 1fr; */ /* grid-template-columns: 1fr 1fr 1fr; */
color: #fff4e9; color: #fff4e9;
@ -165,8 +165,8 @@
.footer-company-details-address-wrapper { .footer-company-details-address-wrapper {
display: flex; display: flex;
gap: 25px; gap: 5px;
margin-right: 150px; margin-right: 60px;
} }
#company-address, #company-address,
@ -348,7 +348,7 @@ $entity_images
<div id="footer"> <div id="footer">
<div class="footer-content"> <div class="footer-content">
<div> <div style="width: 70%;">
<p data-ref="total_table-footer">$entity_footer</p> <p data-ref="total_table-footer">$entity_footer</p>
<script> <script>

View File

@ -0,0 +1,126 @@
@extends('portal.ninja2020.layout.app')
@section('meta_title', ctrans('texts.account_management'))
@section('body')
<!-- This example requires Tailwind CSS v2.0+ -->
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
<div class="px-4 py-5 sm:px-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.plan_status') }}
</h3>
</div>
<div class="border-t border-gray-200">
<dl>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">
{{ ctrans('texts.plan') }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
{{ $account->plan ? ucfirst($account->plan) : 'Free' }}
</dd>
</div>
@if($account->plan)
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">
{{ ctrans('texts.expires') }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
{{ $client->formatDate($account->plan_expires, $client->date_format()) }}
</dd>
</div>
@if($account->plan == 'enterprise')
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">
{{ ctrans('texts.users')}}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
{{ $account->num_users }}
</dd>
</div>
@endif
@endif
@if($late_invoice)
<div class="px-4 py-5 sm:px-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.invoice_status_id') }}
</h3>
<p class="mt-1 max-w-2xl text-sm text-gray-500">
{{ ctrans('texts.past_due') }}
</p>
</div>
<dl>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">
{{ ctrans('texts.invoice') }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
{{ $late_invoice->number }} - {{ \App\Utils\Number::formatMoney($late_invoice->balance, $client) }} <a class="button-link text-primary" href="/client/invoices/{{$late_invoice->hashed_id}}">{{ ctrans('texts.pay_now')}}</a>
</dd>
</div>
</dl>
@else
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">
{{ ctrans('texts.plan_change') }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
<div>
<select id="newPlan" class="pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md">
<option value="">Select Plan</option>
@foreach($plans as $plan)
<option value="{{ $plan->hashed_id}}">{{ $plan->name }} {{ \App\Utils\Number::formatMoney($plan->promo_price, $client) }}</option>
@endforeach
</select>
@if($current_recurring_id)
<button id="handlePlanChange" class="bg-transparent hover:bg-blue-500 text-blue-700 font-semibold hover:text-white py-2 px-4 border border-blue-500 hover:border-transparent rounded">
{{ ctrans('texts.plan_change') }}
</button>
@else
<button id="handleNewPlan" class="bg-transparent hover:bg-blue-500 text-blue-700 font-semibold hover:text-white py-2 px-4 border border-blue-500 hover:border-transparent rounded">
{{ ctrans('texts.plan_upgrade') }}
</button>
@endif
</dd>
</div>
@endif
</dl>
</div>
</div>
@endsection
@push('footer')
<script type="text/javascript">
@if($current_recurring_id)
document.getElementById('handlePlanChange').addEventListener('click', function() {
if(document.getElementById("newPlan").value.length > 1)
location.href = 'https://invoiceninja.invoicing.co/client/subscriptions/{{ $current_recurring_id }}/plan_switch/' + document.getElementById("newPlan").value + '';
});
@else
document.getElementById('handleNewPlan').addEventListener('click', function() {
if(document.getElementById("newPlan").value.length > 1)
location.href = 'https://invoiceninja.invoicing.co/client/subscriptions/' + document.getElementById("newPlan").value + '/purchase';
});
@endif
</script>
@endpush

View File

@ -31,6 +31,8 @@ Route::get('client/ninja/{contact_key}/{company_key}', 'ClientPortal\NinjaPlanCo
Route::group(['middleware' => ['auth:contact', 'locale', 'check_client_existence','domain_db'], 'prefix' => 'client', 'as' => 'client.'], function () { Route::group(['middleware' => ['auth:contact', 'locale', 'check_client_existence','domain_db'], 'prefix' => 'client', 'as' => 'client.'], function () {
Route::get('dashboard', 'ClientPortal\DashboardController@index')->name('dashboard'); // name = (dashboard. index / create / show / update / destroy / edit Route::get('dashboard', 'ClientPortal\DashboardController@index')->name('dashboard'); // name = (dashboard. index / create / show / update / destroy / edit
Route::get('plan', 'ClientPortal\NinjaPlanController@plan')->name('plan'); // name = (dashboard. index / create / show / update / destroy / edit
Route::get('invoices', 'ClientPortal\InvoiceController@index')->name('invoices.index')->middleware('portal_enabled'); Route::get('invoices', 'ClientPortal\InvoiceController@index')->name('invoices.index')->middleware('portal_enabled');
Route::post('invoices/payment', 'ClientPortal\InvoiceController@bulk')->name('invoices.bulk'); Route::post('invoices/payment', 'ClientPortal\InvoiceController@bulk')->name('invoices.bulk');
Route::get('invoices/{invoice}', 'ClientPortal\InvoiceController@show')->name('invoice.show'); Route::get('invoices/{invoice}', 'ClientPortal\InvoiceController@show')->name('invoice.show');