diff --git a/app/Console/Commands/CreateSingleAccount.php b/app/Console/Commands/CreateSingleAccount.php index 147ae4ad516a..2d6c983ae043 100644 --- a/app/Console/Commands/CreateSingleAccount.php +++ b/app/Console/Commands/CreateSingleAccount.php @@ -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); diff --git a/app/Http/Controllers/ClientPortal/InvoiceController.php b/app/Http/Controllers/ClientPortal/InvoiceController.php index 622b64d20e73..844a0d6bf6e9 100644 --- a/app/Http/Controllers/ClientPortal/InvoiceController.php +++ b/app/Http/Controllers/ClientPortal/InvoiceController.php @@ -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') { diff --git a/app/Http/Controllers/ClientPortal/PaymentController.php b/app/Http/Controllers/ClientPortal/PaymentController.php index 6e3e72bb3a95..9267853d0af6 100644 --- a/app/Http/Controllers/ClientPortal/PaymentController.php +++ b/app/Http/Controllers/ClientPortal/PaymentController.php @@ -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); diff --git a/app/Http/Controllers/ClientPortal/SubscriptionPlanSwitchController.php b/app/Http/Controllers/ClientPortal/SubscriptionPlanSwitchController.php index b5b4593a52fa..f8bcbb29e283 100644 --- a/app/Http/Controllers/ClientPortal/SubscriptionPlanSwitchController.php +++ b/app/Http/Controllers/ClientPortal/SubscriptionPlanSwitchController.php @@ -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 diff --git a/app/Http/Livewire/BillingPortalPurchase.php b/app/Http/Livewire/BillingPortalPurchase.php index 3433f07a58df..3051d0aa606e 100644 --- a/app/Http/Livewire/BillingPortalPurchase.php +++ b/app/Http/Livewire/BillingPortalPurchase.php @@ -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'); diff --git a/app/Http/Livewire/SubscriptionPlanSwitch.php b/app/Http/Livewire/SubscriptionPlanSwitch.php index aac881662462..47e4ce5a0935 100644 --- a/app/Http/Livewire/SubscriptionPlanSwitch.php +++ b/app/Http/Livewire/SubscriptionPlanSwitch.php @@ -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, diff --git a/app/Mail/Admin/AutoBillingFailureObject.php b/app/Mail/Admin/AutoBillingFailureObject.php index abbafc45768b..0c69cc073f98 100644 --- a/app/Mail/Admin/AutoBillingFailureObject.php +++ b/app/Mail/Admin/AutoBillingFailureObject.php @@ -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(); diff --git a/app/Services/ClientPortal/InstantPayment.php b/app/Services/ClientPortal/InstantPayment.php index 2df4ea54fbea..ff496ede5e06 100644 --- a/app/Services/ClientPortal/InstantPayment.php +++ b/app/Services/ClientPortal/InstantPayment.php @@ -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); diff --git a/app/Services/Payment/PaymentService.php b/app/Services/Payment/PaymentService.php index 13c8570a44bd..f521ff52a973 100644 --- a/app/Services/Payment/PaymentService.php +++ b/app/Services/Payment/PaymentService.php @@ -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(); diff --git a/app/Services/Subscription/SubscriptionService.php b/app/Services/Subscription/SubscriptionService.php index 31626de248c6..0453e5fd5040 100644 --- a/app/Services/Subscription/SubscriptionService.php +++ b/app/Services/Subscription/SubscriptionService.php @@ -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() - ->where('is_deleted', 0) - ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) - ->where('balance', '>', 0); + $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) { diff --git a/config/ninja.php b/config/ninja.php index 845ef8659658..b085afa20f0d 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -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), diff --git a/resources/views/portal/ninja2020/gateways/credit/index.blade.php b/resources/views/portal/ninja2020/gateways/credit/index.blade.php index d0176f9d136f..df489d903777 100644 --- a/resources/views/portal/ninja2020/gateways/credit/index.blade.php +++ b/resources/views/portal/ninja2020/gateways/credit/index.blade.php @@ -5,6 +5,7 @@
@csrf +
diff --git a/resources/views/portal/ninja2020/invoices/show.blade.php b/resources/views/portal/ninja2020/invoices/show.blade.php index 6fb066a2ee99..c52b7aba16cd 100644 --- a/resources/views/portal/ninja2020/invoices/show.blade.php +++ b/resources/views/portal/ninja2020/invoices/show.blade.php @@ -31,7 +31,7 @@ - + diff --git a/routes/client.php b/routes/client.php index 207c05015fdf..f322850ab1b0 100644 --- a/routes/client.php +++ b/routes/client.php @@ -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');