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/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/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,
]);
+
}
}
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/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/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/PaymentDrivers/Stripe/ACH.php b/app/PaymentDrivers/Stripe/ACH.php
index 50e5f32c4c50..a77838cca263 100644
--- a/app/PaymentDrivers/Stripe/ACH.php
+++ b/app/PaymentDrivers/Stripe/ACH.php
@@ -212,9 +212,9 @@ class ACH
->first();
if ($invoice) {
- $description = ctrans('texts.stripe_payment_text', ['invoicenumber' => $invoice->number, 'amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()]);
+ $description = ctrans('texts.stripe_payment_text', ['invoicenumber' => $invoice->number, 'amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()], $this->stripe->client->company->locale());
} else {
- $description = ctrans('texts.stripe_payment_text_without_invoice', ['amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()]);
+ $description = ctrans('texts.stripe_payment_text_without_invoice', ['amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()], $this->stripe->client->company->locale());
}
@@ -250,9 +250,9 @@ class ACH
->first();
if ($invoice) {
- $description = ctrans('texts.stripe_payment_text', ['invoicenumber' => $invoice->number, 'amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()]);
+ $description = ctrans('texts.stripe_payment_text', ['invoicenumber' => $invoice->number, 'amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()], $this->stripe->client->company->locale());
} else {
- $description = ctrans('texts.stripe_payment_text_without_invoice', ['amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()]);
+ $description = ctrans('texts.stripe_payment_text_without_invoice', ['amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()], $this->stripe->client->company->locale());
}
if (substr($cgt->token, 0, 2) === 'pm') {
@@ -494,9 +494,9 @@ class ACH
->first();
if ($invoice) {
- $description = ctrans('texts.stripe_payment_text', ['invoicenumber' => $invoice->number, 'amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()]);
+ $description = ctrans('texts.stripe_payment_text', ['invoicenumber' => $invoice->number, 'amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()], $this->stripe->client->company->locale());
} else {
- $description = ctrans('texts.stripe_payment_text_without_invoice', ['amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()]);
+ $description = ctrans('texts.stripe_payment_text_without_invoice', ['amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()], $this->stripe->client->company->locale());
}
if (substr($source->token, 0, 2) === 'pm') {
diff --git a/app/PaymentDrivers/Stripe/Charge.php b/app/PaymentDrivers/Stripe/Charge.php
index ee09ba95fb57..102e392f1f07 100644
--- a/app/PaymentDrivers/Stripe/Charge.php
+++ b/app/PaymentDrivers/Stripe/Charge.php
@@ -63,9 +63,9 @@ class Charge
$invoice = Invoice::whereIn('id', $this->transformKeys(array_column($payment_hash->invoices(), 'invoice_id')))->withTrashed()->first();
if ($invoice) {
- $description = ctrans('texts.stripe_payment_text', ['invoicenumber' => $invoice->number, 'amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()]);
+ $description = ctrans('texts.stripe_payment_text', ['invoicenumber' => $invoice->number, 'amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()], $this->stripe->client->company->locale());
} else {
- $description = ctrans('texts.stripe_payment_text_without_invoice', ['amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()]);
+ $description = ctrans('texts.stripe_payment_text_without_invoice', ['amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()], $this->stripe->client->company->locale());
}
$this->stripe->init();
diff --git a/app/PaymentDrivers/Stripe/CreditCard.php b/app/PaymentDrivers/Stripe/CreditCard.php
index fb2f4e6b901d..670ea7d9f805 100644
--- a/app/PaymentDrivers/Stripe/CreditCard.php
+++ b/app/PaymentDrivers/Stripe/CreditCard.php
@@ -61,9 +61,8 @@ class CreditCard
public function paymentView(array $data)
{
- // $description = $this->stripe->decodeUnicodeString(ctrans('texts.invoices') . ': ' . collect($data['invoices'])->pluck('invoice_number')) . " for client {$this->stripe->client->present()->name()}";
$invoice_numbers = collect($data['invoices'])->pluck('invoice_number')->implode(',');
- $description = ctrans('texts.stripe_payment_text', ['invoicenumber' => $invoice_numbers, 'amount' => Number::formatMoney($data['total']['amount_with_fee'], $this->stripe->client), 'client' => $this->stripe->client->present()->name()]);
+ $description = ctrans('texts.stripe_payment_text', ['invoicenumber' => $invoice_numbers, 'amount' => Number::formatMoney($data['total']['amount_with_fee'], $this->stripe->client), 'client' => $this->stripe->client->present()->name()], $this->stripe->client->company->locale());
$payment_intent_data = [
'amount' => $this->stripe->convertToStripeAmount($data['total']['amount_with_fee'], $this->stripe->client->currency()->precision, $this->stripe->client->currency()),
diff --git a/app/PaymentDrivers/Stripe/Klarna.php b/app/PaymentDrivers/Stripe/Klarna.php
index eb30191b4a44..77902124fdc0 100644
--- a/app/PaymentDrivers/Stripe/Klarna.php
+++ b/app/PaymentDrivers/Stripe/Klarna.php
@@ -53,9 +53,9 @@ class Klarna
$invoice_numbers = collect($data['invoices'])->pluck('invoice_number');
if ($invoice_numbers->count() > 0) {
- $description = ctrans('texts.stripe_payment_text', ['invoicenumber' => $invoice_numbers->implode(', '), 'amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()]);
+ $description = ctrans('texts.stripe_payment_text', ['invoicenumber' => $invoice_numbers->implode(', '), 'amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()], $this->stripe->client->company->locale());
} else {
- $description = ctrans('texts.stripe_payment_text_without_invoice', ['amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()]);
+ $description = ctrans('texts.stripe_payment_text_without_invoice', ['amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()], $this->stripe->client->company->locale());
}
$intent = \Stripe\PaymentIntent::create([
diff --git a/app/Services/Subscription/SubscriptionService.php b/app/Services/Subscription/SubscriptionService.php
index ba5fe331a5ad..3af24a43ea7a 100644
--- a/app/Services/Subscription/SubscriptionService.php
+++ b/app/Services/Subscription/SubscriptionService.php
@@ -393,6 +393,8 @@ class SubscriptionService
if(!$invoice)
return [];
+ $handle_discount = false;
+
/* depending on whether we are creating an invoice or a credit*/
$multiplier = $is_credit ? 1 : -1;
@@ -408,17 +410,27 @@ class SubscriptionService
$line_items = [];
+ //Handle when we are refunding a discounted invoice. Need to consider the
+ //total discount and also the line item discount.
+ if($invoice->discount > 0)
+ $handle_discount = true;
+
+
foreach($invoice->line_items as $item)
{
if($item->product_key != ctrans('texts.refund') && ($item->type_id == "1" || $item->type_id == "2"))
{
- $item->cost = ($item->cost*$ratio*$multiplier);
+ $discount_ratio = 1;
+
+ if($handle_discount)
+ $discount_ratio = $this->calculateDiscountRatio($invoice);
+
+ $item->cost = ($item->cost*$ratio*$multiplier*$discount_ratio);
$item->product_key = ctrans('texts.refund');
$item->notes = ctrans('texts.refund') . ": ". $item->notes;
-
$line_items[] = $item;
}
@@ -428,6 +440,23 @@ class SubscriptionService
}
+
+ /**
+ * We only charge for the used days
+ *
+ * @param Invoice $invoice
+ * @return float
+ */
+ public function calculateDiscountRatio($invoice) : float
+ {
+
+ if($invoice->is_amount_discount)
+ return $invoice->discount / ($invoice->amount + $invoice->discount);
+ else
+ return $invoice->discount / 100;
+
+ }
+
/**
* We only charge for the used days
*
@@ -679,8 +708,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
{
@@ -1178,11 +1208,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();
@@ -1198,7 +1232,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())
@@ -1207,8 +1241,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],
],
@@ -1216,6 +1251,7 @@ class SubscriptionService
];
$payment->refund($data);
+ $gateway_refund_attempted = true;
}
}
@@ -1231,7 +1267,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