1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-09-20 16:31:33 +02:00

Subscriptions

This commit is contained in:
David Bomba 2021-04-14 12:40:16 +10:00
parent 77e66d6483
commit 2237939491
6 changed files with 204 additions and 49 deletions

View File

@ -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)

View File

@ -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
{

View File

@ -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)]
);

View File

@ -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');

View File

@ -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
}
}
}

View File

@ -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