mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-11-18 00:53:10 +01:00
commit
a2c49f59a9
@ -1 +1 @@
|
|||||||
5.1.44
|
5.1.45
|
@ -32,6 +32,7 @@ use App\Models\Expense;
|
|||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
use App\Models\Quote;
|
use App\Models\Quote;
|
||||||
|
use App\Models\RecurringInvoice;
|
||||||
use App\Models\Task;
|
use App\Models\Task;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\Vendor;
|
use App\Models\Vendor;
|
||||||
@ -227,12 +228,22 @@ class CreateSingleAccount extends Command
|
|||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
'company_id' => $company->id,
|
'company_id' => $company->id,
|
||||||
'product_key' => 'enterprise_plan',
|
'product_key' => 'enterprise_plan',
|
||||||
'notes' => 'The Pro Plan',
|
'notes' => 'The Enterprise Plan',
|
||||||
'cost' => 10,
|
'cost' => 10,
|
||||||
'price' => 10,
|
'price' => 10,
|
||||||
'quantity' => 1,
|
'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 = [
|
$webhook_config = [
|
||||||
'post_purchase_url' => 'http://ninja.test:8000/api/admin/plan',
|
'post_purchase_url' => 'http://ninja.test:8000/api/admin/plan',
|
||||||
'post_purchase_rest_method' => 'POST',
|
'post_purchase_rest_method' => 'POST',
|
||||||
@ -245,6 +256,7 @@ class CreateSingleAccount extends Command
|
|||||||
$sub->recurring_product_ids = "{$p1->hashed_id}";
|
$sub->recurring_product_ids = "{$p1->hashed_id}";
|
||||||
$sub->webhook_configuration = $webhook_config;
|
$sub->webhook_configuration = $webhook_config;
|
||||||
$sub->allow_plan_changes = true;
|
$sub->allow_plan_changes = true;
|
||||||
|
$sub->frequency_id = RecurringInvoice::FREQUENCY_MONTHLY;
|
||||||
$sub->save();
|
$sub->save();
|
||||||
|
|
||||||
$sub = SubscriptionFactory::create($company->id, $user->id);
|
$sub = SubscriptionFactory::create($company->id, $user->id);
|
||||||
@ -253,6 +265,16 @@ class CreateSingleAccount extends Command
|
|||||||
$sub->recurring_product_ids = "{$p2->hashed_id}";
|
$sub->recurring_product_ids = "{$p2->hashed_id}";
|
||||||
$sub->webhook_configuration = $webhook_config;
|
$sub->webhook_configuration = $webhook_config;
|
||||||
$sub->allow_plan_changes = true;
|
$sub->allow_plan_changes = true;
|
||||||
|
$sub->frequency_id = RecurringInvoice::FREQUENCY_MONTHLY;
|
||||||
|
$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->frequency_id = RecurringInvoice::FREQUENCY_MONTHLY;
|
||||||
$sub->save();
|
$sub->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ use App\Http\Requests\ClientPortal\Subscriptions\ShowPlanSwitchRequest;
|
|||||||
use App\Models\RecurringInvoice;
|
use App\Models\RecurringInvoice;
|
||||||
use App\Models\Subscription;
|
use App\Models\Subscription;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
class SubscriptionPlanSwitchController extends Controller
|
class SubscriptionPlanSwitchController extends Controller
|
||||||
{
|
{
|
||||||
|
@ -112,6 +112,7 @@ class BillingPortalPurchase extends Component
|
|||||||
'show_loading_bar' => false,
|
'show_loading_bar' => false,
|
||||||
'not_eligible' => null,
|
'not_eligible' => null,
|
||||||
'not_eligible_message' => null,
|
'not_eligible_message' => null,
|
||||||
|
'payment_required' => true,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -270,6 +271,9 @@ class BillingPortalPurchase extends Component
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if((int)$this->subscription->price == 0)
|
||||||
|
$this->steps['payment_required'] = false;
|
||||||
|
else
|
||||||
$this->steps['fetched_payment_methods'] = true;
|
$this->steps['fetched_payment_methods'] = true;
|
||||||
|
|
||||||
$this->methods = $contact->client->service()->getPaymentMethods($this->price);
|
$this->methods = $contact->client->service()->getPaymentMethods($this->price);
|
||||||
@ -339,6 +343,7 @@ class BillingPortalPurchase extends Component
|
|||||||
'email' => $this->email ?? $this->contact->email,
|
'email' => $this->email ?? $this->contact->email,
|
||||||
'client_id' => $this->contact->client->id,
|
'client_id' => $this->contact->client->id,
|
||||||
'invoice_id' => $this->invoice->id,
|
'invoice_id' => $this->invoice->id,
|
||||||
|
'context' => 'purchase',
|
||||||
now()->addMinutes(60)]
|
now()->addMinutes(60)]
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -356,6 +361,30 @@ class BillingPortalPurchase extends Component
|
|||||||
'email' => $this->email ?? $this->contact->email,
|
'email' => $this->email ?? $this->contact->email,
|
||||||
'quantity' => $this->quantity,
|
'quantity' => $this->quantity,
|
||||||
'contact_id' => $this->contact->id,
|
'contact_id' => $this->contact->id,
|
||||||
|
'client_id' => $this->contact->client->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handlePaymentNotRequired()
|
||||||
|
{
|
||||||
|
|
||||||
|
$is_eligible = $this->subscription->service()->isEligible($this->contact);
|
||||||
|
|
||||||
|
if ($is_eligible['status_code'] != 200) {
|
||||||
|
$this->steps['not_eligible'] = true;
|
||||||
|
$this->steps['not_eligible_message'] = $is_eligible['exception']['message'];
|
||||||
|
$this->steps['show_loading_bar'] = false;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return $this->subscription->service()->handleNoPaymentRequired([
|
||||||
|
'email' => $this->email ?? $this->contact->email,
|
||||||
|
'quantity' => $this->quantity,
|
||||||
|
'contact_id' => $this->contact->id,
|
||||||
|
'client_id' => $this->contact->client->id,
|
||||||
|
'coupon' => '',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ namespace App\Http\Livewire;
|
|||||||
|
|
||||||
use App\Models\ClientContact;
|
use App\Models\ClientContact;
|
||||||
use App\Models\Subscription;
|
use App\Models\Subscription;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
@ -74,7 +75,7 @@ class SubscriptionPlanSwitch extends Component
|
|||||||
{
|
{
|
||||||
$this->total = $this->amount;
|
$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();
|
$this->hash = Str::uuid()->toString();
|
||||||
}
|
}
|
||||||
@ -83,12 +84,23 @@ class SubscriptionPlanSwitch extends Component
|
|||||||
{
|
{
|
||||||
$this->state['show_loading_bar'] = true;
|
$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,
|
'recurring_invoice' => $this->recurring_invoice,
|
||||||
'subscription' => $this->subscription,
|
'subscription' => $this->subscription,
|
||||||
'target' => $this->target,
|
'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->state['payment_initialised'] = true;
|
||||||
|
|
||||||
$this->emit('beforePaymentEventsCompleted');
|
$this->emit('beforePaymentEventsCompleted');
|
||||||
|
@ -35,10 +35,11 @@ class StoreSubscriptionRequest extends Request
|
|||||||
public function rules()
|
public function rules()
|
||||||
{
|
{
|
||||||
$rules = [
|
$rules = [
|
||||||
'product_id' => ['sometimes'],
|
'product_ids' => ['sometimes'],
|
||||||
|
'recurring_product_ids' => ['sometimes'],
|
||||||
'assigned_user_id' => ['sometimes'],
|
'assigned_user_id' => ['sometimes'],
|
||||||
'is_recurring' => ['sometimes'],
|
'is_recurring' => ['sometimes'],
|
||||||
'frequency_id' => ['sometimes'],
|
'frequency_id' => ['required_with:recurring_product_ids'],
|
||||||
'auto_bill' => ['sometimes'],
|
'auto_bill' => ['sometimes'],
|
||||||
'promo_code' => ['sometimes'],
|
'promo_code' => ['sometimes'],
|
||||||
'promo_discount' => ['sometimes'],
|
'promo_discount' => ['sometimes'],
|
||||||
|
@ -13,6 +13,7 @@ namespace App\Http\Requests\Subscription;
|
|||||||
|
|
||||||
use App\Http\Requests\Request;
|
use App\Http\Requests\Request;
|
||||||
use App\Utils\Traits\ChecksEntityStatus;
|
use App\Utils\Traits\ChecksEntityStatus;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
class UpdateSubscriptionRequest extends Request
|
class UpdateSubscriptionRequest extends Request
|
||||||
{
|
{
|
||||||
@ -36,11 +37,31 @@ class UpdateSubscriptionRequest extends Request
|
|||||||
public function rules()
|
public function rules()
|
||||||
{
|
{
|
||||||
$rules = [
|
$rules = [
|
||||||
//
|
'product_ids' => ['sometimes'],
|
||||||
|
'recurring_product_ids' => ['sometimes'],
|
||||||
|
'assigned_user_id' => ['sometimes'],
|
||||||
|
'is_recurring' => ['sometimes'],
|
||||||
|
'frequency_id' => ['required_with:recurring_product_ids'],
|
||||||
|
'auto_bill' => ['sometimes'],
|
||||||
|
'promo_code' => ['sometimes'],
|
||||||
|
'promo_discount' => ['sometimes'],
|
||||||
|
'is_amount_discount' => ['sometimes'],
|
||||||
|
'allow_cancellation' => ['sometimes'],
|
||||||
|
'per_set_enabled' => ['sometimes'],
|
||||||
|
'min_seats_limit' => ['sometimes'],
|
||||||
|
'max_seats_limit' => ['sometimes'],
|
||||||
|
'trial_enabled' => ['sometimes'],
|
||||||
|
'trial_duration' => ['sometimes'],
|
||||||
|
'allow_query_overrides' => ['sometimes'],
|
||||||
|
'allow_plan_changes' => ['sometimes'],
|
||||||
|
'refund_period' => ['sometimes'],
|
||||||
|
'webhook_configuration' => ['array'],
|
||||||
|
'name' => ['required', Rule::unique('subscriptions')->where('company_id', auth()->user()->company()->id)->ignore($this->subscription->id)]
|
||||||
];
|
];
|
||||||
|
|
||||||
return $this->globalRules($rules);
|
return $this->globalRules($rules);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function prepareForValidation()
|
protected function prepareForValidation()
|
||||||
|
@ -105,10 +105,10 @@ class NinjaMailerJob implements ShouldQueue
|
|||||||
|
|
||||||
switch ($class) {
|
switch ($class) {
|
||||||
case Invoice::class:
|
case Invoice::class:
|
||||||
event(new InvoiceWasEmailedAndFailed($this->nmo->invitation, $this->nmo->company, $message, $this->nmo->reminder_template, Ninja::eventVars(auth()->user()->id)));
|
event(new InvoiceWasEmailedAndFailed($this->nmo->invitation, $this->nmo->company, $message, $this->nmo->reminder_template, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
|
||||||
break;
|
break;
|
||||||
case Payment::class:
|
case Payment::class:
|
||||||
event(new PaymentWasEmailedAndFailed($this->nmo->entity, $this->nmo->company, $message, Ninja::eventVars(auth()->user()->id)));
|
event(new PaymentWasEmailedAndFailed($this->nmo->entity, $this->nmo->company, $message, Ninja::eventVars(auth()->user ? auth()->user()->id : null)));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
# code...
|
# code...
|
||||||
|
@ -29,6 +29,7 @@ use App\Models\SystemLog;
|
|||||||
use App\Repositories\InvoiceRepository;
|
use App\Repositories\InvoiceRepository;
|
||||||
use App\Repositories\RecurringInvoiceRepository;
|
use App\Repositories\RecurringInvoiceRepository;
|
||||||
use App\Repositories\SubscriptionRepository;
|
use App\Repositories\SubscriptionRepository;
|
||||||
|
use App\Services\Subscription\ZeroCostProduct;
|
||||||
use App\Utils\Ninja;
|
use App\Utils\Ninja;
|
||||||
use App\Utils\Traits\CleanLineItems;
|
use App\Utils\Traits\CleanLineItems;
|
||||||
use App\Utils\Traits\MakesHash;
|
use App\Utils\Traits\MakesHash;
|
||||||
@ -61,6 +62,10 @@ class SubscriptionService
|
|||||||
throw new \Exception("Illegal entrypoint into method, payload must contain billing context");
|
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 we have a recurring product - then generate a recurring invoice
|
||||||
if(strlen($this->subscription->recurring_product_ids) >=1){
|
if(strlen($this->subscription->recurring_product_ids) >=1){
|
||||||
|
|
||||||
@ -84,16 +89,15 @@ class SubscriptionService
|
|||||||
'invoice' => $this->encodePrimaryKey($payment_hash->fee_invoice_id),
|
'invoice' => $this->encodePrimaryKey($payment_hash->fee_invoice_id),
|
||||||
'client' => $recurring_invoice->client->hashed_id,
|
'client' => $recurring_invoice->client->hashed_id,
|
||||||
'subscription' => $this->subscription->hashed_id,
|
'subscription' => $this->subscription->hashed_id,
|
||||||
|
'contact' => auth('contact')->user()->hashed_id,
|
||||||
];
|
];
|
||||||
|
|
||||||
$response = $this->triggerWebhook($context);
|
$response = $this->triggerWebhook($context);
|
||||||
|
|
||||||
nlog($response);
|
// nlog($response);
|
||||||
|
|
||||||
if(array_key_exists('post_purchase_url', $this->subscription->webhook_configuration) && strlen($this->subscription->webhook_configuration['post_purchase_url']) >=1)
|
$this->handleRedirect('/client/recurring_invoices/'.$recurring_invoice->hashed_id);
|
||||||
return redirect($this->subscription->webhook_configuration['post_purchase_url']);
|
|
||||||
|
|
||||||
return redirect('/client/recurring_invoices/'.$recurring_invoice->hashed_id);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -109,10 +113,7 @@ class SubscriptionService
|
|||||||
//execute any webhooks
|
//execute any webhooks
|
||||||
$this->triggerWebhook($context);
|
$this->triggerWebhook($context);
|
||||||
|
|
||||||
if(array_key_exists('post_purchase_url', $this->subscription->webhook_configuration) && strlen($this->subscription->webhook_configuration['post_purchase_url']) >=1)
|
$this->handleRedirect('/client/invoices/'.$this->encodePrimaryKey($payment_hash->fee_invoice_id));
|
||||||
return redirect($this->subscription->webhook_configuration['post_purchase_url']);
|
|
||||||
|
|
||||||
return redirect('/client/invoices/'.$this->encodePrimaryKey($payment_hash->fee_invoice_id));
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -217,15 +218,43 @@ class SubscriptionService
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We refund unused days left.
|
||||||
|
*
|
||||||
|
* @param Invoice $invoice
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
private function calculateProRataRefund($invoice) :float
|
private function calculateProRataRefund($invoice) :float
|
||||||
{
|
{
|
||||||
//determine the start date
|
|
||||||
|
|
||||||
$start_date = Carbon::parse($invoice->date);
|
$start_date = Carbon::parse($invoice->date);
|
||||||
|
|
||||||
$current_date = now();
|
$current_date = now();
|
||||||
|
|
||||||
$days_to_refund = $start_date->diffInDays($current_date);
|
$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();
|
$days_in_frequency = $this->getDaysInFrequency();
|
||||||
|
|
||||||
$pro_rata_refund = round(($days_to_refund/$days_in_frequency) * $invoice->amount ,2);
|
$pro_rata_refund = round(($days_to_refund/$days_in_frequency) * $invoice->amount ,2);
|
||||||
@ -235,6 +264,7 @@ class SubscriptionService
|
|||||||
|
|
||||||
public function createChangePlanInvoice($data)
|
public function createChangePlanInvoice($data)
|
||||||
{
|
{
|
||||||
|
$recurring_invoice = $data['recurring_invoice'];
|
||||||
//Data array structure
|
//Data array structure
|
||||||
/**
|
/**
|
||||||
* [
|
* [
|
||||||
@ -244,40 +274,146 @@ 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)
|
->where('is_deleted', 0)
|
||||||
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
|
->orderBy('id', 'desc')
|
||||||
->where('balance', '>', 0)
|
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
$pro_rata_refund = null;
|
//$last_invoice may not be here!
|
||||||
|
|
||||||
// we calculate the pro rata refund for this invoice.
|
if(!$last_invoice) {
|
||||||
if($outstanding_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
|
if($total_payable > 0)
|
||||||
|
{
|
||||||
// User in arrears.
|
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();
|
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
|
public function createInvoice($data): ?\App\Models\Invoice
|
||||||
{
|
{
|
||||||
|
|
||||||
@ -299,7 +435,7 @@ class SubscriptionService
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private function convertInvoiceToRecurring($client_id) :RecurringInvoice
|
public function convertInvoiceToRecurring($client_id) :RecurringInvoice
|
||||||
{
|
{
|
||||||
|
|
||||||
$subscription_repo = new SubscriptionRepository();
|
$subscription_repo = new SubscriptionRepository();
|
||||||
@ -341,7 +477,8 @@ class SubscriptionService
|
|||||||
else {
|
else {
|
||||||
|
|
||||||
$status = $response->getStatusCode();
|
$status = $response->getStatusCode();
|
||||||
$response_body = $response->getBody();
|
|
||||||
|
//$response_body = $response->getReasonPhrase();
|
||||||
$body = array_merge($body, ['status' => $status, 'response_body' => $response_body]);
|
$body = array_merge($body, ['status' => $status, 'response_body' => $response_body]);
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -401,11 +538,6 @@ class SubscriptionService
|
|||||||
->get();
|
->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function completePlanChange(PaymentHash $paymentHash)
|
|
||||||
{
|
|
||||||
// .. handle redirect, after upgrade redirects, etc..
|
|
||||||
}
|
|
||||||
|
|
||||||
public function handleCancellation()
|
public function handleCancellation()
|
||||||
{
|
{
|
||||||
dd('Cancelling using SubscriptionService');
|
dd('Cancelling using SubscriptionService');
|
||||||
@ -413,19 +545,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()
|
private function getDaysInFrequency()
|
||||||
{
|
{
|
||||||
|
|
||||||
@ -459,4 +578,36 @@ class SubscriptionService
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 'email' => $this->email ?? $this->contact->email,
|
||||||
|
* 'quantity' => $this->quantity,
|
||||||
|
* 'contact_id' => $this->contact->id,
|
||||||
|
*/
|
||||||
|
public function handleNoPaymentRequired(array $data)
|
||||||
|
{
|
||||||
|
|
||||||
|
$context = (new ZeroCostProduct($this->subscription, $data))->run();
|
||||||
|
|
||||||
|
// Forward payload to webhook
|
||||||
|
if(array_key_exists('context', $context))
|
||||||
|
$response = $this->triggerWebhook($context);
|
||||||
|
|
||||||
|
// Hit the redirect
|
||||||
|
return $this->handleRedirect($context['redirect_url']);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles redirecting the user
|
||||||
|
*/
|
||||||
|
private function handleRedirect($default_redirect)
|
||||||
|
{
|
||||||
|
|
||||||
|
if(array_key_exists('return_url', $this->subscription->webhook_configuration) && strlen($this->subscription->webhook_configuration['return_url']) >=1)
|
||||||
|
return redirect($this->subscription->webhook_configuration['return_url']);
|
||||||
|
|
||||||
|
return redirect($default_redirect);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
84
app/Services/Subscription/ZeroCostProduct.php
Normal file
84
app/Services/Subscription/ZeroCostProduct.php
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://opensource.org/licenses/AAL
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Services\Subscription;
|
||||||
|
|
||||||
|
use App\Models\Subscription;
|
||||||
|
use App\Repositories\RecurringInvoiceRepository;
|
||||||
|
use App\Services\AbstractService;
|
||||||
|
|
||||||
|
class ZeroCostProduct extends AbstractService
|
||||||
|
{
|
||||||
|
private $subscription;
|
||||||
|
|
||||||
|
private $data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
$data = [
|
||||||
|
'email' => $this->email ?? $this->contact->email,
|
||||||
|
'quantity' => $this->quantity,
|
||||||
|
'contact_id' => $this->contact->id,
|
||||||
|
'client_id' => $this->contact->client->id,
|
||||||
|
];
|
||||||
|
*/
|
||||||
|
public function __construct(Subscription $subscription, array $data)
|
||||||
|
{
|
||||||
|
$this->subscription = $subscription;
|
||||||
|
|
||||||
|
$this->data = $data;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function run()
|
||||||
|
{
|
||||||
|
//create a zero dollar invoice.
|
||||||
|
|
||||||
|
$invoice = $this->subscription->service()->createInvoice($this->data);
|
||||||
|
|
||||||
|
$invoice->service()
|
||||||
|
->markPaid()
|
||||||
|
->save();
|
||||||
|
|
||||||
|
$redirect_url = "/client/invoices/{$invoice->hashed_id}";
|
||||||
|
|
||||||
|
//create a recurring zero dollar invoice attached to this subscription.
|
||||||
|
|
||||||
|
if(strlen($this->subscription->recurring_product_ids) >=1){
|
||||||
|
|
||||||
|
$recurring_invoice = $this->subscription->service()->convertInvoiceToRecurring($this->data['client_id']);
|
||||||
|
$recurring_invoice_repo = new RecurringInvoiceRepository();
|
||||||
|
|
||||||
|
$recurring_invoice->next_send_date = now();
|
||||||
|
$recurring_invoice = $recurring_invoice_repo->save([], $recurring_invoice);
|
||||||
|
$recurring_invoice->next_send_date = $recurring_invoice->nextSendDate();
|
||||||
|
|
||||||
|
/* Start the recurring service */
|
||||||
|
$recurring_invoice->service()
|
||||||
|
->start()
|
||||||
|
->save();
|
||||||
|
|
||||||
|
$context = [
|
||||||
|
'context' => 'recurring_purchase',
|
||||||
|
'recurring_invoice' => $recurring_invoice->hashed_id,
|
||||||
|
'invoice' => $invoice->hashed_id,
|
||||||
|
'client' => $recurring_invoice->client->hashed_id,
|
||||||
|
'subscription' => $this->subscription->hashed_id,
|
||||||
|
'contact' => auth('contact')->user()->hashed_id,
|
||||||
|
'redirect_url' => "/client/recurring_invoices/{$recurring_invoice->hashed_id}",
|
||||||
|
];
|
||||||
|
|
||||||
|
return $context;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['redirect_url' => $redirect_url];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -375,7 +375,7 @@ class HtmlEngine
|
|||||||
$data['$entity_footer'] = ['value' => $this->entity->footer, 'label' => ''];
|
$data['$entity_footer'] = ['value' => $this->entity->footer, 'label' => ''];
|
||||||
|
|
||||||
$data['$page_size'] = ['value' => $this->settings->page_size, 'label' => ''];
|
$data['$page_size'] = ['value' => $this->settings->page_size, 'label' => ''];
|
||||||
$data['$page_layout'] = ['value' => $this->settings->page_layout, 'label' => ''];
|
$data['$page_layout'] = ['value' => property_exists($this->settings, 'page_layout') ? $this->settings->page_layout : 'Portrait', 'label' => ''];
|
||||||
|
|
||||||
$arrKeysLength = array_map('strlen', array_keys($data));
|
$arrKeysLength = array_map('strlen', array_keys($data));
|
||||||
array_multisort($arrKeysLength, SORT_DESC, $data);
|
array_multisort($arrKeysLength, SORT_DESC, $data);
|
||||||
|
@ -40,11 +40,13 @@ trait SubscriptionHooker
|
|||||||
RequestOptions::JSON => ['body' => $body], RequestOptions::ALLOW_REDIRECTS => false
|
RequestOptions::JSON => ['body' => $body], RequestOptions::ALLOW_REDIRECTS => false
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $response;
|
return array_merge($body, ['exception' => json_decode($response->getBody(),true), 'status_code' => $response->getStatusCode()]);
|
||||||
}
|
}
|
||||||
catch(\Exception $e)
|
catch(\Exception $e)
|
||||||
{
|
{
|
||||||
$body = array_merge($body, ['exception' => $e->getMessage()]);
|
//;
|
||||||
|
// dd($e);
|
||||||
|
$body = array_merge($body, ['exception' => ['message' => $e->getMessage(), 'status_code' => 500]]);
|
||||||
return $body;
|
return $body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,8 +14,8 @@ return [
|
|||||||
'require_https' => env('REQUIRE_HTTPS', true),
|
'require_https' => env('REQUIRE_HTTPS', true),
|
||||||
'app_url' => rtrim(env('APP_URL', ''), '/'),
|
'app_url' => rtrim(env('APP_URL', ''), '/'),
|
||||||
'app_domain' => env('APP_DOMAIN', ''),
|
'app_domain' => env('APP_DOMAIN', ''),
|
||||||
'app_version' => '5.1.44',
|
'app_version' => '5.1.45',
|
||||||
'app_tag' => '5.1.44-release',
|
'app_tag' => '5.1.45-release',
|
||||||
'minimum_client_version' => '5.0.16',
|
'minimum_client_version' => '5.0.16',
|
||||||
'terms_version' => '1.0.1',
|
'terms_version' => '1.0.1',
|
||||||
'api_secret' => env('API_SECRET', false),
|
'api_secret' => env('API_SECRET', false),
|
||||||
|
@ -4210,7 +4210,7 @@ $LANG = array(
|
|||||||
'activity_83' => ':user deleted subscription :subscription',
|
'activity_83' => ':user deleted subscription :subscription',
|
||||||
'activity_84' => ':user restored subscription :subscription',
|
'activity_84' => ':user restored subscription :subscription',
|
||||||
'amount_greater_than_balance_v5' => 'The amount is greater than the invoice balance. You cannot overpay an invoice.',
|
'amount_greater_than_balance_v5' => 'The amount is greater than the invoice balance. You cannot overpay an invoice.',
|
||||||
|
'click_to_continue' => 'Click to continue',
|
||||||
);
|
);
|
||||||
|
|
||||||
return $LANG;
|
return $LANG;
|
||||||
|
@ -135,6 +135,13 @@
|
|||||||
</svg>
|
</svg>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
@elseif(!$steps['payment_required'])
|
||||||
|
<form wire:submit.prevent="handlePaymentNotRequired" class="mt-8">
|
||||||
|
@csrf
|
||||||
|
<button class="px-3 py-2 border rounded mr-4 hover:border-blue-600">
|
||||||
|
{{ ctrans('texts.click_to_continue') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
@elseif($steps['show_start_trial'])
|
@elseif($steps['show_start_trial'])
|
||||||
<form wire:submit.prevent="handleTrial" class="mt-8">
|
<form wire:submit.prevent="handleTrial" class="mt-8">
|
||||||
@csrf
|
@csrf
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Payment box -->
|
<!-- Payment box -->
|
||||||
@livewire('subscription-plan-switch', compact('subscription', 'target', 'contact'))
|
@livewire('subscription-plan-switch', compact('recurring_invoice', 'subscription', 'target', 'contact', 'amount'))
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
namespace Tests\Feature;
|
namespace Tests\Feature;
|
||||||
|
|
||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
|
use App\Models\RecurringInvoice;
|
||||||
use App\Models\Subscription;
|
use App\Models\Subscription;
|
||||||
use App\Utils\Traits\MakesHash;
|
use App\Utils\Traits\MakesHash;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
@ -91,6 +92,7 @@ class SubscriptionApiTest extends TestCase
|
|||||||
$product = Product::factory()->create([
|
$product = Product::factory()->create([
|
||||||
'company_id' => $this->company->id,
|
'company_id' => $this->company->id,
|
||||||
'user_id' => $this->user->id,
|
'user_id' => $this->user->id,
|
||||||
|
'frequency_id' => RecurringInvoice::FREQUENCY_MONTHLY,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response1 = $this
|
$response1 = $this
|
||||||
|
Loading…
Reference in New Issue
Block a user