mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-05-24 02:14:21 -04:00
Refactor for subscriptions and changing between subscriptions
This commit is contained in:
parent
6d235bcf86
commit
28cbe52d9c
@ -307,7 +307,7 @@ class CreateSingleAccount extends Command
|
||||
$webhook_config = [
|
||||
'post_purchase_url' => 'http://ninja.test:8000/api/admin/plan',
|
||||
'post_purchase_rest_method' => 'POST',
|
||||
'post_purchase_headers' => [],
|
||||
'post_purchase_headers' => [config('ninja.ninja_hosted_header') => config('ninja.ninja_hosted_secret')],
|
||||
];
|
||||
|
||||
$sub = SubscriptionFactory::create($company->id, $user->id);
|
||||
|
@ -52,7 +52,7 @@ class InvoiceController extends Controller
|
||||
*
|
||||
* @return Factory|View
|
||||
*/
|
||||
public function show(ShowInvoiceRequest $request, Invoice $invoice)
|
||||
public function show(ShowInvoiceRequest $request, Invoice $invoice, ?string $hash)
|
||||
{
|
||||
set_time_limit(0);
|
||||
|
||||
@ -69,6 +69,7 @@ class InvoiceController extends Controller
|
||||
'invoice' => $invoice,
|
||||
'invitation' => $invitation ?: $invoice->invitations->first(),
|
||||
'key' => $invitation ? $invitation->key : false,
|
||||
'hash' => $hash,
|
||||
];
|
||||
|
||||
if ($request->query('mode') === 'fullscreen') {
|
||||
|
@ -148,8 +148,17 @@ class PaymentController extends Controller
|
||||
|
||||
$payment = $payment->service()->applyCredits($payment_hash)->save();
|
||||
|
||||
$invoices = Invoice::whereIn('id', $this->transformKeys(array_column($payment_hash->invoices(), 'invoice_id')));
|
||||
|
||||
event('eloquent.created: App\Models\Payment', $payment);
|
||||
|
||||
if($invoices->sum('balance') > 0){
|
||||
|
||||
$invoice = $invoices->first();
|
||||
|
||||
return redirect()->route('client.invoice.show', ['invoice' => $invoice->hashed_id, 'hash' => $request->input('hash')]);
|
||||
}
|
||||
|
||||
if (property_exists($payment_hash->data, 'billing_context')) {
|
||||
$billing_subscription = \App\Models\Subscription::find($payment_hash->data->billing_context->subscription_id);
|
||||
|
||||
|
@ -33,7 +33,9 @@ class SubscriptionPlanSwitchController extends Controller
|
||||
{
|
||||
$amount = $recurring_invoice->subscription
|
||||
->service()
|
||||
->calculateUpgradePrice($recurring_invoice, $target);
|
||||
->calculateUpgradePriceV2($recurring_invoice, $target);
|
||||
|
||||
nlog("upgrade amoutn = {$amount}");
|
||||
/**
|
||||
* Null value here is a proxy for
|
||||
* denying the user a change plan option
|
||||
|
@ -330,6 +330,8 @@ class BillingPortalPurchase extends Component
|
||||
else
|
||||
$this->steps['fetched_payment_methods'] = true;
|
||||
|
||||
nlog("payment methods price = {$this->price}");
|
||||
|
||||
$this->methods = $contact->client->service()->getPaymentMethods($this->price);
|
||||
|
||||
$this->heading_text = ctrans('texts.payment_methods');
|
||||
|
@ -142,7 +142,7 @@ class SubscriptionPlanSwitch extends Component
|
||||
{
|
||||
$this->hide_button = true;
|
||||
|
||||
$response = $this->target->service()->createChangePlanCredit([
|
||||
$response = $this->target->service()->createChangePlanCreditV2([
|
||||
'recurring_invoice' => $this->recurring_invoice,
|
||||
'subscription' => $this->subscription,
|
||||
'target' => $this->target,
|
||||
|
@ -64,7 +64,7 @@ class AutoBillingFailureObject
|
||||
/* Set customized translations _NOW_ */
|
||||
$t->replace(Ninja::transformTranslations($this->company->settings));
|
||||
|
||||
$this->$invoices = Invoice::whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->get();
|
||||
$this->invoices = Invoice::whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->get();
|
||||
|
||||
$mail_obj = new stdClass;
|
||||
$mail_obj->amount = $this->getAmount();
|
||||
|
@ -48,7 +48,7 @@ class InstantPayment
|
||||
|
||||
public function run()
|
||||
{
|
||||
|
||||
nlog($this->request->all());
|
||||
$is_credit_payment = false;
|
||||
|
||||
$tokens = [];
|
||||
@ -221,6 +221,9 @@ class InstantPayment
|
||||
if ($this->request->query('hash')) {
|
||||
$hash_data['billing_context'] = Cache::get($this->request->query('hash'));
|
||||
}
|
||||
elseif($this->request->hash){
|
||||
$hash_data['billing_context'] = Cache::get($this->request->hash);
|
||||
}
|
||||
|
||||
$payment_hash = new PaymentHash;
|
||||
$payment_hash->hash = Str::random(32);
|
||||
|
@ -140,6 +140,39 @@ class PaymentService
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function applyCreditsToInvoice($invoice)
|
||||
{
|
||||
|
||||
$amount = $invoice->amount;
|
||||
|
||||
$credits = $invoice->client
|
||||
->service()
|
||||
->getCredits();
|
||||
|
||||
foreach ($credits as $credit) {
|
||||
//starting invoice balance
|
||||
$invoice_balance = $invoice->balance;
|
||||
|
||||
//credit payment applied
|
||||
$credit->service()->applyPayment($invoice, $amount, $this->payment);
|
||||
|
||||
//amount paid from invoice calculated
|
||||
$remaining_balance = ($invoice_balance - $invoice->fresh()->balance);
|
||||
|
||||
//reduce the amount to be paid on the invoice from the NEXT credit
|
||||
$amount -= $remaining_balance;
|
||||
|
||||
//break if the invoice is no longer PAYABLE OR there is no more amount to be applied
|
||||
if (! $invoice->isPayable() || (int) $amount == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function save()
|
||||
{
|
||||
$this->payment->saveQuietly();
|
||||
|
@ -15,6 +15,7 @@ use App\DataMapper\InvoiceItem;
|
||||
use App\Factory\CreditFactory;
|
||||
use App\Factory\InvoiceFactory;
|
||||
use App\Factory\InvoiceToRecurringInvoiceFactory;
|
||||
use App\Factory\PaymentFactory;
|
||||
use App\Factory\RecurringInvoiceFactory;
|
||||
use App\Jobs\Mail\NinjaMailer;
|
||||
use App\Jobs\Mail\NinjaMailerJob;
|
||||
@ -28,6 +29,7 @@ use App\Models\ClientContact;
|
||||
use App\Models\Credit;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\PaymentHash;
|
||||
use App\Models\PaymentType;
|
||||
use App\Models\Product;
|
||||
use App\Models\RecurringInvoice;
|
||||
use App\Models\Subscription;
|
||||
@ -89,11 +91,17 @@ class SubscriptionService
|
||||
$recurring_invoice = $recurring_invoice_repo->save([], $recurring_invoice);
|
||||
$recurring_invoice->auto_bill = $this->subscription->auto_bill;
|
||||
|
||||
|
||||
/* Start the recurring service */
|
||||
$recurring_invoice->service()
|
||||
->start()
|
||||
->save();
|
||||
|
||||
//update the invoice and attach to the recurring invoice!!!!!
|
||||
$invoice = Invoice::find($payment_hash->fee_invoice_id);
|
||||
$invoice->recurring_id = $recurring_invoice->id;
|
||||
$invoice->save();
|
||||
|
||||
//execute any webhooks
|
||||
$context = [
|
||||
'context' => 'recurring_purchase',
|
||||
@ -217,23 +225,69 @@ class SubscriptionService
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function calculateUpgradePriceV2(RecurringInvoice $recurring_invoice, Subscription $target) :?float
|
||||
{
|
||||
|
||||
$outstanding_credit = 0;
|
||||
|
||||
$use_credit_setting = $recurring_invoice->client->getSetting('use_credits_payment');
|
||||
|
||||
$last_invoice = Invoice::query()
|
||||
->where('recurring_id', $recurring_invoice->id)
|
||||
->where('is_deleted', 0)
|
||||
->where('status_id', Invoice::STATUS_PAID)
|
||||
->first();
|
||||
|
||||
$refund = $this->calculateProRataRefundForSubscription($last_invoice);
|
||||
|
||||
if($use_credit_setting != 'off')
|
||||
{
|
||||
|
||||
$outstanding_credit = Credit::query()
|
||||
->where('client_id', $recurring_invoice->client_id)
|
||||
->whereIn('status_id', [Credit::STATUS_SENT,Credit::STATUS_PARTIAL])
|
||||
->where('is_deleted', 0)
|
||||
->where('balance', '>', 0)
|
||||
->sum('balance');
|
||||
|
||||
}
|
||||
|
||||
nlog("{$target->price} - {$refund} - {$outstanding_credit}");
|
||||
return $target->price - $refund - $outstanding_credit;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an upgrade price when moving between plans
|
||||
*
|
||||
* However we only allow people to move between plans
|
||||
* if their account is in good standing.
|
||||
*
|
||||
* @param RecurringInvoice $recurring_invoice
|
||||
* @param Subscription $target
|
||||
* @deprecated in favour of calculateUpgradePriceV2
|
||||
* @return float
|
||||
*/
|
||||
public function calculateUpgradePrice(RecurringInvoice $recurring_invoice, Subscription $target) :?float
|
||||
{
|
||||
//calculate based on daily prices
|
||||
|
||||
//calculate based on daily prices
|
||||
$current_amount = $recurring_invoice->amount;
|
||||
$currency_frequency = $recurring_invoice->frequency_id;
|
||||
|
||||
$outstanding = $recurring_invoice->invoices()
|
||||
$outstanding = Invoice::query()
|
||||
->where('recurring_id', $recurring_invoice->id)
|
||||
->where('is_deleted', 0)
|
||||
->where('is_proforma',0)
|
||||
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
|
||||
->where('balance', '>', 0);
|
||||
|
||||
$outstanding_amounts = $outstanding->sum('balance');
|
||||
|
||||
$outstanding_invoice = Invoice::where('subscription_id', $this->subscription->id)
|
||||
->where('client_id', $recurring_invoice->client_id)
|
||||
$outstanding_invoice = Invoice::where('client_id', $recurring_invoice->client_id)
|
||||
->where('is_deleted', 0)
|
||||
->where('is_proforma',0)
|
||||
->where('subscription_id', $this->subscription->id)
|
||||
->orderBy('id', 'desc')
|
||||
->first();
|
||||
|
||||
@ -242,6 +296,7 @@ class SubscriptionService
|
||||
|
||||
$outstanding_invoice = Credit::where('subscription_id', $this->subscription->id)
|
||||
->where('client_id', $recurring_invoice->client_id)
|
||||
->where('is_proforma',0)
|
||||
->where('is_deleted', 0)
|
||||
->orderBy('id', 'desc')
|
||||
->first();
|
||||
@ -289,7 +344,6 @@ class SubscriptionService
|
||||
|
||||
$days_in_frequency = $this->getDaysInFrequency();
|
||||
|
||||
//18-12-2022 - change $this->subscription->price => $invoice->amount if there was a discount on the invoice, we should not use the subscription price.
|
||||
$pro_rata_refund = round((($days_in_frequency - $days_of_subscription_used)/$days_in_frequency) * $invoice->amount ,2);
|
||||
|
||||
return $pro_rata_refund;
|
||||
@ -398,10 +452,81 @@ class SubscriptionService
|
||||
return $pro_rata_charge;
|
||||
}
|
||||
|
||||
/**
|
||||
* This entry point assumes the user does not have to make a
|
||||
* payment for the service.
|
||||
*
|
||||
* In this case, we generate a credit note for the old service
|
||||
* Generate a new invoice for the new service
|
||||
* Apply credits to the invoice
|
||||
*
|
||||
* @param array $data
|
||||
*/
|
||||
public function createChangePlanCreditV2($data)
|
||||
{
|
||||
/* Init vars */
|
||||
$recurring_invoice = $data['recurring_invoice'];
|
||||
$old_subscription = $data['subscription'];
|
||||
$target_subscription = $data['target'];
|
||||
|
||||
$pro_rata_charge_amount = 0;
|
||||
$pro_rata_refund_amount = 0;
|
||||
$is_credit = false;
|
||||
|
||||
/* Get last invoice */
|
||||
$last_invoice = Invoice::where('subscription_id', $recurring_invoice->subscription_id)
|
||||
->where('client_id', $recurring_invoice->client_id)
|
||||
->where('is_proforma',0)
|
||||
->where('is_deleted', 0)
|
||||
->where('status_id', Invoice::STATUS_PAID)
|
||||
->withTrashed()
|
||||
->orderBy('id', 'desc')
|
||||
->first();
|
||||
|
||||
// $pro_rata_refund_amount = $this->calculateProRataRefund($last_invoice, $old_subscription);
|
||||
|
||||
$credit = $this->createCredit($last_invoice, $target_subscription, false);
|
||||
|
||||
$new_recurring_invoice = $this->createNewRecurringInvoice($recurring_invoice);
|
||||
|
||||
$invoice = $this->changePlanInvoice($target_subscription, $recurring_invoice->client_id);
|
||||
$invoice->recurring_id = $new_recurring_invoice->id;
|
||||
$invoice->save();
|
||||
|
||||
$payment = PaymentFactory::create($invoice->company_id, $invoice->user_id, $invoice->client_id);
|
||||
$payment->type_id = PaymentType::CREDIT;
|
||||
$payment->client_id = $invoice->client_id;
|
||||
$payment->is_manual = true;
|
||||
$payment->save();
|
||||
|
||||
$payment->service()->applyCreditsToInvoice($invoice);
|
||||
|
||||
$context = [
|
||||
'context' => 'change_plan',
|
||||
'recurring_invoice' => $new_recurring_invoice->hashed_id,
|
||||
'credit' => $credit ? $credit->hashed_id : null,
|
||||
'client' => $new_recurring_invoice->client->hashed_id,
|
||||
'subscription' => $target_subscription->hashed_id,
|
||||
'contact' => auth()->guard('contact')->user()->hashed_id,
|
||||
'account_key' => $new_recurring_invoice->client->custom_value2,
|
||||
];
|
||||
|
||||
$response = $this->triggerWebhook($context);
|
||||
|
||||
if($credit){
|
||||
return '/client/invoices/'.$invoice->hashed_id;
|
||||
}
|
||||
else{
|
||||
return '/client/invoices';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* When downgrading, we may need to create
|
||||
* a credit
|
||||
*
|
||||
* @deprecated in favour of createChangePlanCreditV2
|
||||
* @param array $data
|
||||
*/
|
||||
public function createChangePlanCredit($data)
|
||||
@ -658,9 +783,10 @@ class SubscriptionService
|
||||
$credit->discount = $last_invoice->discount;
|
||||
$credit->is_amount_discount = $last_invoice->is_amount_discount;
|
||||
|
||||
$line_items = $subscription_repo->generateLineItems($target, false, true);
|
||||
// $line_items = $subscription_repo->generateLineItems($target, false, true);
|
||||
|
||||
$credit->line_items = array_merge($line_items, $this->calculateProRataRefundItems($last_invoice, $last_invoice_is_credit));
|
||||
// $credit->line_items = array_merge($line_items, $this->calculateProRataRefundItems($last_invoice, $last_invoice_is_credit));
|
||||
$credit->line_items = $this->calculateProRataRefundItems($last_invoice, true);
|
||||
|
||||
$data = [
|
||||
'client_id' => $last_invoice->client_id,
|
||||
@ -705,6 +831,39 @@ class SubscriptionService
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* When changing plans we need to generate a pro rata
|
||||
* invoice which takes into account any credits.
|
||||
*
|
||||
* @param Subscription $target
|
||||
* @return Invoice
|
||||
*/
|
||||
private function changePlanInvoice($target, $client_id)
|
||||
{
|
||||
$subscription_repo = new SubscriptionRepository();
|
||||
$invoice_repo = new InvoiceRepository();
|
||||
|
||||
$invoice = InvoiceFactory::create($this->subscription->company_id, $this->subscription->user_id);
|
||||
$invoice->date = now()->format('Y-m-d');
|
||||
$invoice->subscription_id = $target->id;
|
||||
|
||||
$invoice->line_items = $subscription_repo->generateLineItems($target);
|
||||
|
||||
$data = [
|
||||
'client_id' => $client_id,
|
||||
'quantity' => 1,
|
||||
'date' => now()->format('Y-m-d'),
|
||||
];
|
||||
|
||||
return $invoice_repo->save($data, $invoice)
|
||||
->service()
|
||||
->markSent()
|
||||
->fillDefaults()
|
||||
->save();
|
||||
|
||||
}
|
||||
|
||||
|
||||
public function createInvoiceV2($bundle, $client_id, $valid_coupon = false)
|
||||
{
|
||||
|
||||
|
@ -191,6 +191,7 @@ return [
|
||||
'ninja_default_company_id' => env('NINJA_COMPANY_ID', null),
|
||||
'ninja_default_company_gateway_id' => env('NINJA_COMPANY_GATEWAY_ID', null),
|
||||
'ninja_hosted_secret' => env('NINJA_HOSTED_SECRET', null),
|
||||
'ninja_hosted_header' =>env('NINJA_HEADER',''),
|
||||
'internal_queue_enabled' => env('INTERNAL_QUEUE_ENABLED', true),
|
||||
'ninja_apple_api_key' => env('APPLE_API_KEY', false),
|
||||
'ninja_apple_private_key' => env('APPLE_PRIVATE_KEY', false),
|
||||
|
@ -5,6 +5,7 @@
|
||||
<form action="{{route('client.payments.credit_response')}}" method="post" id="credit-payment">
|
||||
@csrf
|
||||
<input type="hidden" name="payment_hash" value="{{$payment_hash}}">
|
||||
<input type="hidden" name="hash" value="{{ request()->query('hash')}}">
|
||||
</form>
|
||||
|
||||
<div class="container mx-auto">
|
||||
|
@ -31,7 +31,7 @@
|
||||
<input type="hidden" name="company_gateway_id" id="company_gateway_id">
|
||||
<input type="hidden" name="payment_method_id" id="payment_method_id">
|
||||
<input type="hidden" name="signature">
|
||||
|
||||
<input type="hidden" name="hash" value="{{ $hash }}">
|
||||
<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 }}">
|
||||
|
||||
|
@ -54,7 +54,7 @@ Route::group(['middleware' => ['auth:contact', 'locale', 'domain_db','check_clie
|
||||
Route::post('invoices/payment', [App\Http\Controllers\ClientPortal\InvoiceController::class, 'bulk'])->name('invoices.bulk');
|
||||
Route::get('invoices/payment', [App\Http\Controllers\ClientPortal\InvoiceController::class, 'catch_bulk'])->name('invoices.catch_bulk');
|
||||
Route::post('invoices/download', [App\Http\Controllers\ClientPortal\InvoiceController::class, 'download'])->name('invoices.download');
|
||||
Route::get('invoices/{invoice}', [App\Http\Controllers\ClientPortal\InvoiceController::class, 'show'])->name('invoice.show');
|
||||
Route::get('invoices/{invoice}/{hash?}', [App\Http\Controllers\ClientPortal\InvoiceController::class, 'show'])->name('invoice.show');
|
||||
Route::get('invoices/{invoice_invitation}', [App\Http\Controllers\ClientPortal\InvoiceController::class, 'show'])->name('invoice.show_invitation');
|
||||
|
||||
Route::get('recurring_invoices', [App\Http\Controllers\ClientPortal\RecurringInvoiceController::class, 'index'])->name('recurring_invoices.index')->middleware('portal_enabled');
|
||||
|
Loading…
x
Reference in New Issue
Block a user