From afad1245c989db44c53926b09c88db294552bd08 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 12 Jan 2023 13:52:06 +1100 Subject: [PATCH 1/6] Clean up for Filters --- app/Filters/ClientFilters.php | 2 -- app/Filters/DesignFilters.php | 1 - app/Filters/ExpenseFilters.php | 2 -- app/Filters/TokenFilters.php | 1 - app/Filters/VendorFilters.php | 1 - app/Filters/WebhookFilters.php | 1 - .../ClientPortal/SubscriptionPlanSwitchController.php | 3 ++- 7 files changed, 2 insertions(+), 9 deletions(-) diff --git a/app/Filters/ClientFilters.php b/app/Filters/ClientFilters.php index cd19453991f1..65923049a754 100644 --- a/app/Filters/ClientFilters.php +++ b/app/Filters/ClientFilters.php @@ -193,9 +193,7 @@ class ClientFilters extends QueryFilters ->where('clients.company_id', '=', $company_id) ->where('client_contacts.is_primary', '=', true) ->where('client_contacts.deleted_at', '=', null) - //->whereRaw('(clients.name != "" or contacts.first_name != "" or contacts.last_name != "" or contacts.email != "")') // filter out buy now invoices ->select( - // DB::raw('COALESCE(clients.currency_id, companies.currency_id) currency_id'), DB::raw('COALESCE(clients.country_id, companies.country_id) country_id'), DB::raw("CONCAT(COALESCE(client_contacts.first_name, ''), ' ', COALESCE(client_contacts.last_name, '')) contact"), 'clients.id', diff --git a/app/Filters/DesignFilters.php b/app/Filters/DesignFilters.php index 37577562cb2a..5db04472ff30 100644 --- a/app/Filters/DesignFilters.php +++ b/app/Filters/DesignFilters.php @@ -105,7 +105,6 @@ class DesignFilters extends QueryFilters $query = DB::table('designs') ->join('companies', 'companies.id', '=', 'designs.company_id') ->where('designs.company_id', '=', $company_id) - //->whereRaw('(designs.name != "" or contacts.first_name != "" or contacts.last_name != "" or contacts.email != "")') // filter out buy now invoices ->select( 'designs.id', 'designs.name', diff --git a/app/Filters/ExpenseFilters.php b/app/Filters/ExpenseFilters.php index 697f4b64f148..8b17219efff4 100644 --- a/app/Filters/ExpenseFilters.php +++ b/app/Filters/ExpenseFilters.php @@ -205,9 +205,7 @@ class ExpenseFilters extends QueryFilters $query = DB::table('expenses') ->join('companies', 'companies.id', '=', 'expenses.company_id') ->where('expenses.company_id', '=', $company_id) - //->whereRaw('(expenses.name != "" or contacts.first_name != "" or contacts.last_name != "" or contacts.email != "")') // filter out buy now invoices ->select( - // DB::raw('COALESCE(expenses.currency_id, companies.currency_id) currency_id'), DB::raw('COALESCE(expenses.country_id, companies.country_id) country_id'), DB::raw("CONCAT(COALESCE(expense_contacts.first_name, ''), ' ', COALESCE(expense_contacts.last_name, '')) contact"), 'expenses.id', diff --git a/app/Filters/TokenFilters.php b/app/Filters/TokenFilters.php index 2f471a1304a7..a771aaab7f59 100644 --- a/app/Filters/TokenFilters.php +++ b/app/Filters/TokenFilters.php @@ -104,7 +104,6 @@ class TokenFilters extends QueryFilters $query = DB::table('company_tokens') ->join('companies', 'companies.id', '=', 'company_tokens.company_id') ->where('company_tokens.company_id', '=', $company_id) - //->whereRaw('(designs.name != "" or contacts.first_name != "" or contacts.last_name != "" or contacts.email != "")') // filter out buy now invoices ->select( 'company_tokens.id', 'company_tokens.name', diff --git a/app/Filters/VendorFilters.php b/app/Filters/VendorFilters.php index dd2c7e885824..b330d53a334d 100644 --- a/app/Filters/VendorFilters.php +++ b/app/Filters/VendorFilters.php @@ -118,7 +118,6 @@ class VendorFilters extends QueryFilters ->where('vendors.company_id', '=', $company_id) ->where('vendor_contacts.is_primary', '=', true) ->where('vendor_contacts.deleted_at', '=', null) - //->whereRaw('(vendors.name != "" or contacts.first_name != "" or contacts.last_name != "" or contacts.email != "")') // filter out buy now invoices ->select( // DB::raw('COALESCE(vendors.currency_id, companies.currency_id) currency_id'), DB::raw('COALESCE(vendors.country_id, companies.country_id) country_id'), diff --git a/app/Filters/WebhookFilters.php b/app/Filters/WebhookFilters.php index 5dec35538f32..b42f49909890 100644 --- a/app/Filters/WebhookFilters.php +++ b/app/Filters/WebhookFilters.php @@ -105,7 +105,6 @@ class WebhookFilters extends QueryFilters $query = DB::table('webhooks') ->join('companies', 'companies.id', '=', 'webhooks.company_id') ->where('webhooks.company_id', '=', $company_id) - //->whereRaw('(designs.name != "" or contacts.first_name != "" or contacts.last_name != "" or contacts.email != "")') // filter out buy now invoices ->select( 'webhooks.id', 'webhooks.target_url', diff --git a/app/Http/Controllers/ClientPortal/SubscriptionPlanSwitchController.php b/app/Http/Controllers/ClientPortal/SubscriptionPlanSwitchController.php index aa829ef457e0..34ad3ae061c9 100644 --- a/app/Http/Controllers/ClientPortal/SubscriptionPlanSwitchController.php +++ b/app/Http/Controllers/ClientPortal/SubscriptionPlanSwitchController.php @@ -31,6 +31,7 @@ class SubscriptionPlanSwitchController extends Controller */ public function index(ShowPlanSwitchRequest $request, RecurringInvoice $recurring_invoice, Subscription $target) { + $amount = $recurring_invoice->subscription ->service() ->calculateUpgradePriceV2($recurring_invoice, $target); @@ -44,7 +45,6 @@ class SubscriptionPlanSwitchController extends Controller render('subscriptions.denied'); } - $amount = max(0,$amount); return render('subscriptions.switch', [ @@ -53,5 +53,6 @@ class SubscriptionPlanSwitchController extends Controller 'target' => $target, 'amount' => $amount, ]); + } } From 80a5d8a37d00545cca9e11e22c8a9fddd97a75b2 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 12 Jan 2023 14:21:54 +1100 Subject: [PATCH 2/6] Fixes for pro rata invoicing --- .../Ninja/RenewalFailureNotification.php | 79 +++++++++++++++++++ .../Subscription/SubscriptionService.php | 5 +- 2 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 app/Notifications/Ninja/RenewalFailureNotification.php diff --git a/app/Notifications/Ninja/RenewalFailureNotification.php b/app/Notifications/Ninja/RenewalFailureNotification.php new file mode 100644 index 000000000000..f9cf87f18afd --- /dev/null +++ b/app/Notifications/Ninja/RenewalFailureNotification.php @@ -0,0 +1,79 @@ +notification_message}"; + + return (new SlackMessage) + ->success() + ->from(ctrans('texts.notification_bot')) + ->image('https://app.invoiceninja.com/favicon.png') + ->content($content); + } +} diff --git a/app/Services/Subscription/SubscriptionService.php b/app/Services/Subscription/SubscriptionService.php index ba5fe331a5ad..fd517f04d169 100644 --- a/app/Services/Subscription/SubscriptionService.php +++ b/app/Services/Subscription/SubscriptionService.php @@ -679,8 +679,9 @@ class SubscriptionService } else if($last_invoice->balance > 0) { - $pro_rata_charge_amount = $this->calculateProRataCharge($last_invoice, $old_subscription); - nlog("pro rata charge = {$pro_rata_charge_amount}"); + $last_invoice = null; + // $pro_rata_charge_amount = $this->calculateProRataCharge($last_invoice, $old_subscription); + // nlog("pro rata charge = {$pro_rata_charge_amount}"); } else { From 1d811c49b9d065b529d6c221b9c4fefa7fbc37da Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 12 Jan 2023 15:58:02 +1100 Subject: [PATCH 3/6] Return success to webhook URL when the gateway has been deleted/non-resolvable to prevent constant webhook retries --- .../ClientPortal/SubscriptionController.php | 17 +++ .../Controllers/PaymentWebhookController.php | 4 + app/Http/Livewire/SubscriptionsTable.php | 51 +++++++ .../Payments/PaymentWebhookRequest.php | 3 +- .../livewire/subscriptions-table.blade.php | 137 ++++++------------ .../ninja2020/subscriptions/index.blade.php | 2 +- .../ninja2020/subscriptions/show.blade.php | 120 +++++++++++++++ routes/client.php | 5 +- 8 files changed, 239 insertions(+), 100 deletions(-) create mode 100644 app/Http/Livewire/SubscriptionsTable.php create mode 100644 resources/views/portal/ninja2020/subscriptions/show.blade.php diff --git a/app/Http/Controllers/ClientPortal/SubscriptionController.php b/app/Http/Controllers/ClientPortal/SubscriptionController.php index 21fa9116c23e..dd7634cfc264 100644 --- a/app/Http/Controllers/ClientPortal/SubscriptionController.php +++ b/app/Http/Controllers/ClientPortal/SubscriptionController.php @@ -13,6 +13,7 @@ namespace App\Http\Controllers\ClientPortal; use App\Http\Controllers\Controller; +use App\Http\Requests\ClientPortal\RecurringInvoices\ShowRecurringInvoiceRequest; use App\Models\RecurringInvoice; use App\Utils\Ninja; use Illuminate\Http\Request; @@ -38,4 +39,20 @@ class SubscriptionController extends Controller return render('subscriptions.index'); } + + /** + * Display the recurring invoice. + * + * @param ShowRecurringInvoiceRequest $request + * @param RecurringInvoice $recurring_invoice + * @return Factory|View + */ + public function show(ShowRecurringInvoiceRequest $request, RecurringInvoice $recurring_invoice) + { + return $this->render('subscriptions.show', [ + 'invoice' => $recurring_invoice->load('invoices','subscription'), + 'subscription' => $recurring_invoice->subscription + ]); + } + } diff --git a/app/Http/Controllers/PaymentWebhookController.php b/app/Http/Controllers/PaymentWebhookController.php index 9f03b3697e09..7cd9152e949c 100644 --- a/app/Http/Controllers/PaymentWebhookController.php +++ b/app/Http/Controllers/PaymentWebhookController.php @@ -18,6 +18,10 @@ class PaymentWebhookController extends Controller { public function __invoke(PaymentWebhookRequest $request) { + //return early if we cannot resolve the company gateway + if(!$request->getCompanyGateway()) + return response()->json([], 200); + return $request ->getCompanyGateway() ->driver() diff --git a/app/Http/Livewire/SubscriptionsTable.php b/app/Http/Livewire/SubscriptionsTable.php new file mode 100644 index 000000000000..3f5071ed1028 --- /dev/null +++ b/app/Http/Livewire/SubscriptionsTable.php @@ -0,0 +1,51 @@ +company->db); + } + + public function render() + { + $query = RecurringInvoice::query() + ->where('client_id', auth()->guard('contact')->user()->client->id) + ->where('company_id', $this->company->id) + ->whereNotNull('subscription_id') + ->where('is_deleted', false) + ->where('status_id', RecurringInvoice::STATUS_ACTIVE) + ->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc') + ->withTrashed() + ->paginate($this->per_page); + + return render('components.livewire.subscriptions-table', [ + 'recurring_invoices' => $query, + ]); + } +} diff --git a/app/Http/Requests/Payments/PaymentWebhookRequest.php b/app/Http/Requests/Payments/PaymentWebhookRequest.php index b3c528c776d9..b55c587ab54a 100644 --- a/app/Http/Requests/Payments/PaymentWebhookRequest.php +++ b/app/Http/Requests/Payments/PaymentWebhookRequest.php @@ -47,7 +47,8 @@ class PaymentWebhookRequest extends Request { MultiDB::findAndSetDbByCompanyKey($this->company_key); - return CompanyGateway::withTrashed()->findOrFail($this->decodePrimaryKey($this->company_gateway_id)); + return CompanyGateway::withTrashed()->find($this->decodePrimaryKey($this->company_gateway_id)); + } /** diff --git a/resources/views/portal/ninja2020/components/livewire/subscriptions-table.blade.php b/resources/views/portal/ninja2020/components/livewire/subscriptions-table.blade.php index cf8af07fcd43..2c14db058ddb 100644 --- a/resources/views/portal/ninja2020/components/livewire/subscriptions-table.blade.php +++ b/resources/views/portal/ninja2020/components/livewire/subscriptions-table.blade.php @@ -1,10 +1,5 @@
-

- One-time payments -

- -
+
- - - - - -
-
-
-
- - - + + - @forelse($invoices as $invoice) + @forelse($recurring_invoices as $recurring_invoice) + + + @empty @@ -138,13 +83,13 @@
- - {{ ctrans('texts.invoice') }} - +

+ {{ ctrans('texts.frequency') }} +

+
+

+ {{ ctrans('texts.invoice') }} +

- - {{ ctrans('texts.total') }} - +

+ {{ ctrans('texts.amount') }} +

+
+

+ {{ ctrans('texts.date') }} +

- - {{ ctrans('texts.date') }} -
- subscription->name }} + + {{ \App\Models\RecurringInvoice::frequencyForKey($recurring_invoice->frequency_id) }} + + - {{ $invoice->number }} + {{ $recurring_invoice->number }} - {{ App\Utils\Number::formatMoney($invoice->amount, $invoice->client) }} + {{ App\Utils\Number::formatMoney($recurring_invoice->amount, $recurring_invoice->client) }} - {{ $invoice->translateDate($invoice->date, $invoice->client->date_format(), $invoice->client->locale()) }} + {{ $recurring_invoice->translateDate($recurring_invoice->date, $recurring_invoice->client->date_format(), $recurring_invoice->client->locale()) }} + + + {{ ctrans('texts.view') }} +
+
- @if($invoices->total() > 0) + @if($recurring_invoices->total() > 0) @endif - {{ $invoices->links('portal/ninja2020/vendor/pagination') }} + {{ $recurring_invoices->links('portal/ninja2020/vendor/pagination') }}
-
diff --git a/resources/views/portal/ninja2020/subscriptions/index.blade.php b/resources/views/portal/ninja2020/subscriptions/index.blade.php index fd25ac7a1e51..83da36c5e909 100644 --- a/resources/views/portal/ninja2020/subscriptions/index.blade.php +++ b/resources/views/portal/ninja2020/subscriptions/index.blade.php @@ -3,6 +3,6 @@ @section('body')
- @livewire('subscription-recurring-invoices-table', ['company' => $company]) + @livewire('subscriptions-table', ['company' => $company])
@endsection diff --git a/resources/views/portal/ninja2020/subscriptions/show.blade.php b/resources/views/portal/ninja2020/subscriptions/show.blade.php new file mode 100644 index 000000000000..15e8b41b4357 --- /dev/null +++ b/resources/views/portal/ninja2020/subscriptions/show.blade.php @@ -0,0 +1,120 @@ +@extends('portal.ninja2020.layout.app') +@section('meta_title', ctrans('texts.subscription')) + +@section('body') +
+
+
+

+ {{ ctrans('texts.subscription') }} +

+

+ {{ ctrans('texts.details_of_recurring_invoice') }}. +

+
+
+
+
+
+ {{ ctrans('texts.start_date') }} +
+
+ {{ $invoice->translateDate($invoice->start_date, $invoice->client->date_format(), $invoice->client->locale()) }} +
+
+
+
+ {{ ctrans('texts.next_send_date') }} +
+
+ {{ $invoice->translateDate(\Carbon\Carbon::parse($invoice->next_send_date)->subSeconds($invoice->client->timezone_offset()), $invoice->client->date_format(), $invoice->client->locale()) }} +
+
+
+
+ {{ ctrans('texts.frequency') }} +
+
+ {{ \App\Models\RecurringInvoice::frequencyForKey($invoice->frequency_id) }} +
+
+
+
+ {{ ctrans('texts.cycles_remaining') }} +
+
+ {{ $invoice->remaining_cycles == '-1' ? ctrans('texts.endless') : $invoice->remaining_cycles }} + @if($invoice->remaining_cycles == '-1') ∞ @endif +
+
+
+
+ {{ ctrans('texts.amount') }} +
+
+ {{ \App\Utils\Number::formatMoney($invoice->amount, $invoice->client) }} +
+
+
+
+
+ + @include('portal.ninja2020.components.entity-documents', ['entity' => $invoice]) + + @if($invoice->auto_bill === 'optin' || $invoice->auto_bill === 'optout') +
+
+
+

{{ ctrans('texts.auto_bill') }}

+

{{ ctrans('texts.auto_bill_option')}}

+
+ +
+ @livewire('recurring-invoices.update-auto-billing', ['invoice' => $invoice]) +
+
+
+ @endif + + @if($invoice->subscription && $invoice->subscription?->allow_cancellation) + {{-- INV2-591 --}} + {{-- @if(false) --}} +
+
+
+
+

+ {{ ctrans('texts.cancellation') }} +

+
+

+ {{ ctrans('texts.about_cancellation') }} +

+
+
+
+
+ + @include('portal.ninja2020.recurring_invoices.includes.modals.cancellation') +
+
+
+
+
+ @endif + + @if($invoice->subscription && $invoice->subscription->allow_plan_changes) +
+

Switch Plans:

+

Upgrade or downgrade your current plan.

+ +
+ @foreach($invoice->subscription->service()->getPlans() as $subscription) + {{ $subscription->name }} + @endforeach +
+
+ @endif +
+@endsection diff --git a/routes/client.php b/routes/client.php index f322850ab1b0..c02a23cb26a3 100644 --- a/routes/client.php +++ b/routes/client.php @@ -98,8 +98,9 @@ Route::group(['middleware' => ['auth:contact', 'locale', 'domain_db','check_clie Route::resource('documents', App\Http\Controllers\ClientPortal\DocumentController::class)->only(['index', 'show']); Route::get('subscriptions/{recurring_invoice}/plan_switch/{target}', [App\Http\Controllers\ClientPortal\SubscriptionPlanSwitchController::class, 'index'])->name('subscription.plan_switch'); - - Route::resource('subscriptions', SubscriptionController::class)->middleware('portal_enabled')->only(['index']); + + Route::get('subscriptions/{recurring_invoice}', [SubscriptionController::class, 'show'])->middleware('portal_enabled')->name('subscriptions.show'); + Route::get('subscriptions', [SubscriptionController::class, 'index'])->middleware('portal_enabled')->name('subscriptions.index'); Route::resource('tasks', TaskController::class)->only(['index']); From 7e7cffa8c865432ebcc8371708208814be369de8 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 12 Jan 2023 17:29:28 +1100 Subject: [PATCH 4/6] Fixes for recurring invoice cancellations --- ...ClientContactRequestCancellationObject.php | 22 +++++++++---------- .../Subscription/SubscriptionService.php | 12 +++++++--- resources/views/index/index.blade.php | 17 ++++++++++++++ .../livewire/subscriptions-table.blade.php | 21 ++++++++---------- .../ninja2020/subscriptions/show.blade.php | 18 +++------------ 5 files changed, 48 insertions(+), 42 deletions(-) diff --git a/app/Mail/RecurringInvoice/ClientContactRequestCancellationObject.php b/app/Mail/RecurringInvoice/ClientContactRequestCancellationObject.php index a0c5bdca62ad..5201fc07f5a6 100644 --- a/app/Mail/RecurringInvoice/ClientContactRequestCancellationObject.php +++ b/app/Mail/RecurringInvoice/ClientContactRequestCancellationObject.php @@ -11,35 +11,33 @@ namespace App\Mail\RecurringInvoice; +use App\Models\ClientContact; +use App\Models\RecurringInvoice; use App\Utils\Ninja; use Illuminate\Support\Facades\App; class ClientContactRequestCancellationObject { - public $recurring_invoice; - public $client_contact; - - private $company; - - public function __construct($recurring_invoice, $client_contact) - { - $this->recurring_invoice = $recurring_invoice; - $this->client_contact = $client_contact; - $this->company = $recurring_invoice->company; - } + public function __construct(public RecurringInvoice $recurring_invoice, public ClientContact $client_contact, private bool $gateway_refund_attempted){} public function build() { + $this->company = $this->recurring_invoice->company; + App::forgetInstance('translator'); App::setLocale($this->company->getLocale()); $t = app('translator'); $t->replace(Ninja::transformTranslations($this->company->settings)); + $content = ctrans('texts.recurring_cancellation_request_body', ['contact' => $this->client_contact->present()->name(), 'client' => $this->client_contact->client->present()->name(), 'invoice' => $this->recurring_invoice->number]); + + if($this->gateway_refund_attempted) + $content .= "\n\n" . ctrans('texts.status') . " : " . ctrans('texts.payment_status_6'); $data = [ 'title' => ctrans('texts.recurring_cancellation_request', ['contact' => $this->client_contact->present()->name()]), - 'content' => ctrans('texts.recurring_cancellation_request_body', ['contact' => $this->client_contact->present()->name(), 'client' => $this->client_contact->client->present()->name(), 'invoice' => $this->recurring_invoice->number]), + 'content' => $content, 'url' => config('ninja.web_url'), 'button' => ctrans('texts.account_login'), 'signature' => $this->company->settings->email_signature, diff --git a/app/Services/Subscription/SubscriptionService.php b/app/Services/Subscription/SubscriptionService.php index fd517f04d169..b4902dc868e5 100644 --- a/app/Services/Subscription/SubscriptionService.php +++ b/app/Services/Subscription/SubscriptionService.php @@ -1179,11 +1179,15 @@ class SubscriptionService { $invoice_start_date = false; $refund_end_date = false; + $gateway_refund_attempted = false; //only refund if they are in the refund window. $outstanding_invoice = Invoice::where('subscription_id', $this->subscription->id) ->where('client_id', $recurring_invoice->client_id) + ->where('status_id', Invoice::STATUS_PAID) ->where('is_deleted', 0) + ->where('is_proforma',0) + ->where('balance',0) ->orderBy('id', 'desc') ->first(); @@ -1199,7 +1203,7 @@ class SubscriptionService $recurring_invoice_repo->archive($recurring_invoice); /* Refund only if we are in the window - and there is nothing outstanding on the invoice */ - if($refund_end_date && $refund_end_date->greaterThan(now()) && (int)$outstanding_invoice->balance == 0) + if($refund_end_date && $refund_end_date->greaterThan(now())) { if($outstanding_invoice->payments()->exists()) @@ -1208,8 +1212,9 @@ class SubscriptionService $data = [ 'id' => $payment->id, - 'gateway_refund' => true, + 'gateway_refund' => $outstanding_invoice->amount >= 1 ? true : false, 'send_email' => true, + 'email_receipt', 'invoices' => [ ['invoice_id' => $outstanding_invoice->id, 'amount' => $outstanding_invoice->amount], ], @@ -1217,6 +1222,7 @@ class SubscriptionService ]; $payment->refund($data); + $gateway_refund_attempted = true; } } @@ -1232,7 +1238,7 @@ class SubscriptionService $this->triggerWebhook($context); $nmo = new NinjaMailerObject; - $nmo->mailable = (new NinjaMailer((new ClientContactRequestCancellationObject($recurring_invoice, auth()->guard('contact')->user()))->build())); + $nmo->mailable = (new NinjaMailer((new ClientContactRequestCancellationObject($recurring_invoice, auth()->guard('contact')->user(), $gateway_refund_attempted))->build())); $nmo->company = $recurring_invoice->company; $nmo->settings = $recurring_invoice->company->settings; diff --git a/resources/views/index/index.blade.php b/resources/views/index/index.blade.php index bdce834ae5fa..2d95233ce2f3 100644 --- a/resources/views/index/index.blade.php +++ b/resources/views/index/index.blade.php @@ -10,11 +10,21 @@ @if(\App\Utils\Ninja::isHosted()) + + + + + + @endif