mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-09-20 16:31:33 +02:00
Subscriptions
This commit is contained in:
parent
77e66d6483
commit
2237939491
@ -227,12 +227,22 @@ class CreateSingleAccount extends Command
|
||||
'user_id' => $user->id,
|
||||
'company_id' => $company->id,
|
||||
'product_key' => 'enterprise_plan',
|
||||
'notes' => 'The Pro Plan',
|
||||
'notes' => 'The Enterprise Plan',
|
||||
'cost' => 10,
|
||||
'price' => 10,
|
||||
'quantity' => 1,
|
||||
]);
|
||||
|
||||
$p3 = Product::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'company_id' => $company->id,
|
||||
'product_key' => 'free_plan',
|
||||
'notes' => 'The Free Plan',
|
||||
'cost' => 0,
|
||||
'price' => 0,
|
||||
'quantity' => 1,
|
||||
]);
|
||||
|
||||
$webhook_config = [
|
||||
'post_purchase_url' => 'http://ninja.test:8000/api/admin/plan',
|
||||
'post_purchase_rest_method' => 'POST',
|
||||
@ -254,6 +264,14 @@ class CreateSingleAccount extends Command
|
||||
$sub->webhook_configuration = $webhook_config;
|
||||
$sub->allow_plan_changes = true;
|
||||
$sub->save();
|
||||
|
||||
$sub = SubscriptionFactory::create($company->id, $user->id);
|
||||
$sub->name = "Free Plan";
|
||||
$sub->group_id = $gs->id;
|
||||
$sub->recurring_product_ids = "{$p3->hashed_id}";
|
||||
$sub->webhook_configuration = $webhook_config;
|
||||
$sub->allow_plan_changes = true;
|
||||
$sub->save();
|
||||
}
|
||||
|
||||
private function createClient($company, $user)
|
||||
|
@ -17,6 +17,7 @@ use App\Http\Requests\ClientPortal\Subscriptions\ShowPlanSwitchRequest;
|
||||
use App\Models\RecurringInvoice;
|
||||
use App\Models\Subscription;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class SubscriptionPlanSwitchController extends Controller
|
||||
{
|
||||
|
@ -339,6 +339,7 @@ class BillingPortalPurchase extends Component
|
||||
'email' => $this->email ?? $this->contact->email,
|
||||
'client_id' => $this->contact->client->id,
|
||||
'invoice_id' => $this->invoice->id,
|
||||
'context' => 'purchase',
|
||||
now()->addMinutes(60)]
|
||||
);
|
||||
|
||||
|
@ -14,6 +14,7 @@ namespace App\Http\Livewire;
|
||||
|
||||
use App\Models\ClientContact;
|
||||
use App\Models\Subscription;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Component;
|
||||
|
||||
@ -74,7 +75,7 @@ class SubscriptionPlanSwitch extends Component
|
||||
{
|
||||
$this->total = $this->amount;
|
||||
|
||||
$this->methods = $this->contact->client->service()->getPaymentMethods(100);
|
||||
$this->methods = $this->contact->client->service()->getPaymentMethods($this->amount);
|
||||
|
||||
$this->hash = Str::uuid()->toString();
|
||||
}
|
||||
@ -83,12 +84,23 @@ class SubscriptionPlanSwitch extends Component
|
||||
{
|
||||
$this->state['show_loading_bar'] = true;
|
||||
|
||||
$this->state['invoice'] = $this->subscription->service()->createChangePlanInvoice([
|
||||
$this->state['invoice'] = $this->target->service()->createChangePlanInvoice([
|
||||
'recurring_invoice' => $this->recurring_invoice,
|
||||
'subscription' => $this->subscription,
|
||||
'target' => $this->target,
|
||||
'hash' => $this->hash,
|
||||
]);
|
||||
|
||||
Cache::put($this->hash, [
|
||||
'subscription_id' => $this->target->id,
|
||||
'target_id' => $this->target->id,
|
||||
'recurring_invoice' => $this->recurring_invoice->id,
|
||||
'client_id' => $this->recurring_invoice->client->id,
|
||||
'invoice_id' => $this->state['invoice']->id,
|
||||
'context' => 'change_plan',
|
||||
now()->addMinutes(60)]
|
||||
);
|
||||
|
||||
$this->state['payment_initialised'] = true;
|
||||
|
||||
$this->emit('beforePaymentEventsCompleted');
|
||||
|
@ -61,6 +61,10 @@ class SubscriptionService
|
||||
throw new \Exception("Illegal entrypoint into method, payload must contain billing context");
|
||||
}
|
||||
|
||||
if($payment_hash->data->billing_context->context == 'change_plan') {
|
||||
return $this->handlePlanChange($payment_hash);
|
||||
};
|
||||
|
||||
// if we have a recurring product - then generate a recurring invoice
|
||||
if(strlen($this->subscription->recurring_product_ids) >=1){
|
||||
|
||||
@ -84,6 +88,7 @@ class SubscriptionService
|
||||
'invoice' => $this->encodePrimaryKey($payment_hash->fee_invoice_id),
|
||||
'client' => $recurring_invoice->client->hashed_id,
|
||||
'subscription' => $this->subscription->hashed_id,
|
||||
'contact' => auth('contact')->user()->hashed_id,
|
||||
];
|
||||
|
||||
$response = $this->triggerWebhook($context);
|
||||
@ -217,15 +222,43 @@ class SubscriptionService
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* We refund unused days left.
|
||||
*
|
||||
* @param Invoice $invoice
|
||||
* @return float
|
||||
*/
|
||||
private function calculateProRataRefund($invoice) :float
|
||||
{
|
||||
//determine the start date
|
||||
|
||||
$start_date = Carbon::parse($invoice->date);
|
||||
|
||||
$current_date = now();
|
||||
|
||||
$days_to_refund = $start_date->diffInDays($current_date);
|
||||
|
||||
$days_in_frequency = $this->getDaysInFrequency();
|
||||
|
||||
$pro_rata_refund = round((($days_in_frequency - $days_to_refund)/$days_in_frequency) * $invoice->amount ,2);
|
||||
|
||||
return $pro_rata_refund;
|
||||
}
|
||||
|
||||
/**
|
||||
* We only charge for the used days
|
||||
*
|
||||
* @param Invoice $invoice
|
||||
* @return float
|
||||
*/
|
||||
private function calculateProRataCharge($invoice) :float
|
||||
{
|
||||
|
||||
$start_date = Carbon::parse($invoice->date);
|
||||
|
||||
$current_date = now();
|
||||
|
||||
$days_to_refund = $start_date->diffInDays($current_date);
|
||||
|
||||
$days_in_frequency = $this->getDaysInFrequency();
|
||||
|
||||
$pro_rata_refund = round(($days_to_refund/$days_in_frequency) * $invoice->amount ,2);
|
||||
@ -235,6 +268,7 @@ class SubscriptionService
|
||||
|
||||
public function createChangePlanInvoice($data)
|
||||
{
|
||||
$recurring_invoice = $data['recurring_invoice'];
|
||||
//Data array structure
|
||||
/**
|
||||
* [
|
||||
@ -244,40 +278,145 @@ class SubscriptionService
|
||||
* ]
|
||||
*/
|
||||
|
||||
$outstanding_invoice = $recurring_invoice->invoices()
|
||||
// $outstanding_invoice = $recurring_invoice->invoices()
|
||||
// ->where('is_deleted', 0)
|
||||
// ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
|
||||
// ->where('balance', '>', 0)
|
||||
// ->first();
|
||||
|
||||
$pro_rata_charge_amount = 0;
|
||||
$pro_rata_refund_amount = 0;
|
||||
|
||||
// // We calculate the pro rata charge for this invoice.
|
||||
// if($outstanding_invoice)
|
||||
// {
|
||||
// }
|
||||
|
||||
$last_invoice = $recurring_invoice->invoices()
|
||||
->where('is_deleted', 0)
|
||||
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
|
||||
->where('balance', '>', 0)
|
||||
->orderBy('id', 'desc')
|
||||
->first();
|
||||
|
||||
$pro_rata_refund = null;
|
||||
//$last_invoice may not be here!
|
||||
|
||||
// we calculate the pro rata refund for this invoice.
|
||||
if($outstanding_invoice)
|
||||
if(!$last_invoice) {
|
||||
$data = [
|
||||
'client_id' => $recurring_invoice->client_id,
|
||||
'coupon' => '',
|
||||
];
|
||||
|
||||
return $this->createInvoice($data)->service()->markSent()->fillDefaults()->save();
|
||||
|
||||
}
|
||||
else if($last_invoice->balance > 0)
|
||||
{
|
||||
// $pro_rata_refund = $this->calculateProRataRefund($out
|
||||
$pro_rata_charge_amount = $this->calculateProRataCharge($last_invoice);
|
||||
}
|
||||
else
|
||||
{
|
||||
$pro_rata_refund_amount = $this->calculateProRataRefund($last_invoice) * -1;
|
||||
}
|
||||
|
||||
//logic
|
||||
$total_payable = $pro_rata_refund_amount + $pro_rata_charge_amount + $this->subscription->price;
|
||||
|
||||
// Is the user paid up to date? ie are there any outstanding invoices for this subscription
|
||||
|
||||
// User in arrears.
|
||||
if($total_payable > 0)
|
||||
{
|
||||
return $this->proRataInvoice($pro_rata_refund_amount, $data['subscription'], $data['target']);
|
||||
}
|
||||
else
|
||||
{
|
||||
//create credit
|
||||
}
|
||||
|
||||
|
||||
// User paid up to date (in credit!)
|
||||
|
||||
//generate credit amount.
|
||||
//
|
||||
//generate new billable amount
|
||||
//
|
||||
|
||||
//if billable amount is LESS than 0 -> generate a credit and pass through.
|
||||
//
|
||||
//if billable amoun is GREATER than 0 -> gener
|
||||
return Invoice::where('status_id', Invoice::STATUS_SENT)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Response from payment service on return from a plan change
|
||||
*
|
||||
*/
|
||||
private function handlePlanChange($payment_hash)
|
||||
{
|
||||
|
||||
//payment has been made.
|
||||
//
|
||||
//new subscription starts today - delete old recurring invoice.
|
||||
|
||||
$old_subscription_recurring_invoice = RecurringInvoice::find($payment_hash->data->billing_context->recurring_invoice);
|
||||
$old_subscription_recurring_invoice->service()->stop()->save();
|
||||
|
||||
$recurring_invoice_repo = new RecurringInvoiceRepository();
|
||||
$recurring_invoice_repo->archive($old_subscription_recurring_invoice);
|
||||
|
||||
$recurring_invoice = $this->convertInvoiceToRecurring($payment_hash->payment->client_id);
|
||||
$recurring_invoice = $recurring_invoice_repo->save([], $recurring_invoice);
|
||||
$recurring_invoice->next_send_date = now();
|
||||
$recurring_invoice->next_send_date = $recurring_invoice->nextSendDate();
|
||||
|
||||
/* Start the recurring service */
|
||||
$recurring_invoice->service()
|
||||
->start()
|
||||
->save();
|
||||
|
||||
$context = [
|
||||
'context' => 'change_plan',
|
||||
'recurring_invoice' => $recurring_invoice->hashed_id,
|
||||
'invoice' => $this->encodePrimaryKey($payment_hash->fee_invoice_id),
|
||||
'client' => $recurring_invoice->client->hashed_id,
|
||||
'subscription' => $this->subscription->hashed_id,
|
||||
'contact' => auth('contact')->user()->hashed_id,
|
||||
];
|
||||
|
||||
$response = $this->triggerWebhook($context);
|
||||
|
||||
nlog($response);
|
||||
|
||||
if(array_key_exists('post_purchase_url', $this->subscription->webhook_configuration) && strlen($this->subscription->webhook_configuration['post_purchase_url']) >=1)
|
||||
return redirect($this->subscription->webhook_configuration['post_purchase_url']);
|
||||
|
||||
return redirect('/client/recurring_invoices/'.$recurring_invoice->hashed_id);
|
||||
|
||||
}
|
||||
|
||||
public function handlePlanChangeNoPayment()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 'client_id' => 2,
|
||||
'date' => '2021-04-13',
|
||||
'invitations' =>
|
||||
'user_input_promo_code' => NULL,
|
||||
'coupon' => '',
|
||||
'quantity' => 1,
|
||||
*/
|
||||
private function proRataInvoice($refund_amount, $subscription, $target)
|
||||
{
|
||||
$subscription_repo = new SubscriptionRepository();
|
||||
$invoice_repo = new InvoiceRepository();
|
||||
|
||||
$line_items = $subscription_repo->generateLineItems($target);
|
||||
|
||||
$item = new InvoiceItem;
|
||||
$item->quantity = 1;
|
||||
$item->product_key = ctrans('texts.refund');
|
||||
$item->notes = ctrans('texts.refund') . ":" .$subscription->name;
|
||||
$item->cost = $refund_amount;
|
||||
|
||||
$line_items[] = $item;
|
||||
|
||||
$data = [
|
||||
'client_id' => $subscription->client_id,
|
||||
'quantity' => 1,
|
||||
'date' => now()->format('Y-m-d'),
|
||||
];
|
||||
|
||||
return $invoice_repo->save($data, $invoice)->service()->markSent()->fillDefaults()->save();
|
||||
|
||||
}
|
||||
|
||||
public function createInvoice($data): ?\App\Models\Invoice
|
||||
{
|
||||
|
||||
@ -401,11 +540,6 @@ class SubscriptionService
|
||||
->get();
|
||||
}
|
||||
|
||||
public function completePlanChange(PaymentHash $paymentHash)
|
||||
{
|
||||
// .. handle redirect, after upgrade redirects, etc..
|
||||
}
|
||||
|
||||
public function handleCancellation()
|
||||
{
|
||||
dd('Cancelling using SubscriptionService');
|
||||
@ -413,19 +547,6 @@ class SubscriptionService
|
||||
// ..
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pro rata calculation between subscriptions.
|
||||
*
|
||||
* @param Subscription $current
|
||||
* @param Subscription $target
|
||||
*/
|
||||
public function getPriceBetweenSubscriptions(Subscription $current, Subscription $target): int
|
||||
{
|
||||
// Calculate the pro rata. Return negative value if credits needed.
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
private function getDaysInFrequency()
|
||||
{
|
||||
|
||||
@ -459,4 +580,6 @@ class SubscriptionService
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -42,7 +42,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Payment box -->
|
||||
@livewire('subscription-plan-switch', compact('subscription', 'target', 'contact'))
|
||||
@livewire('subscription-plan-switch', compact('recurring_invoice', 'subscription', 'target', 'contact', 'amount'))
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user