diff --git a/app/Console/Commands/CreateSingleAccount.php b/app/Console/Commands/CreateSingleAccount.php index 2a4dab445893..ab850b11aa1d 100644 --- a/app/Console/Commands/CreateSingleAccount.php +++ b/app/Console/Commands/CreateSingleAccount.php @@ -229,8 +229,8 @@ class CreateSingleAccount extends Command 'company_id' => $company->id, 'product_key' => 'enterprise_plan', 'notes' => 'The Enterprise Plan', - 'cost' => 10, - 'price' => 10, + 'cost' => 14, + 'price' => 14, 'quantity' => 1, ]); diff --git a/app/Http/Controllers/ClientPortal/SubscriptionPlanSwitchController.php b/app/Http/Controllers/ClientPortal/SubscriptionPlanSwitchController.php index 7adf172baa1a..a6cb764d0516 100644 --- a/app/Http/Controllers/ClientPortal/SubscriptionPlanSwitchController.php +++ b/app/Http/Controllers/ClientPortal/SubscriptionPlanSwitchController.php @@ -35,7 +35,6 @@ class SubscriptionPlanSwitchController extends Controller $amount = $recurring_invoice->subscription ->service() ->calculateUpgradePrice($recurring_invoice, $target); - /** * * Null value here is a proxy for diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index 480a0816ee43..c1463151d9aa 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -592,7 +592,7 @@ class PaymentController extends BaseController $this->payment_repo->restore($payment); if (! $bulk) { - return $this->listResponse($payment); + return $this->itemResponse($payment); } break; @@ -600,7 +600,7 @@ class PaymentController extends BaseController $this->payment_repo->archive($payment); if (! $bulk) { - return $this->listResponse($payment); + return $this->itemResponse($payment); } // code... break; @@ -608,14 +608,26 @@ class PaymentController extends BaseController $this->payment_repo->delete($payment); if (! $bulk) { - return $this->listResponse($payment); + return $this->itemResponse($payment); } // code... break; case 'email': //dispatch email to queue - break; + $this->payment->service()->sendEmail(); + if (! $bulk) { + return $this->itemResponse($payment); + } + break; + case 'email_receipt': + $this->payment->service()->sendEmail(); + + if (! $bulk) { + return $this->itemResponse($payment); + } + break; + default: // code... break; @@ -671,6 +683,8 @@ class PaymentController extends BaseController { $payment = $request->payment(); +// nlog($request->all()); + $payment = $payment->refund($request->all()); return $this->itemResponse($payment); diff --git a/app/Http/Controllers/SelfUpdateController.php b/app/Http/Controllers/SelfUpdateController.php index bbe4926ca739..cd77546c5d7d 100644 --- a/app/Http/Controllers/SelfUpdateController.php +++ b/app/Http/Controllers/SelfUpdateController.php @@ -85,7 +85,7 @@ class SelfUpdateController extends BaseController Artisan::call('clear-compiled'); Artisan::call('cache:clear'); - Artisan::call('debugbar:clear'); + // Artisan::call('debugbar:clear'); Artisan::call('route:clear'); Artisan::call('view:clear'); Artisan::call('config:clear'); diff --git a/app/Http/Controllers/TaskController.php b/app/Http/Controllers/TaskController.php index 54383b06ede0..28efb452caf1 100644 --- a/app/Http/Controllers/TaskController.php +++ b/app/Http/Controllers/TaskController.php @@ -279,8 +279,8 @@ class TaskController extends BaseController $task = $this->task_repo->save($request->all(), $task); - // if($task->status_order != $old_task->status_order) - // $this->task_repo->sortStatuses($old_task, $task); + if($task->status_order != $old_task->status_order) + $this->task_repo->sortStatuses($old_task, $task); event(new TaskWasUpdated($task, $task->company, Ninja::eventVars(auth()->user()->id))); diff --git a/app/Http/Livewire/BillingPortalPurchase.php b/app/Http/Livewire/BillingPortalPurchase.php index c864cc666f3d..a1614a3cc65d 100644 --- a/app/Http/Livewire/BillingPortalPurchase.php +++ b/app/Http/Livewire/BillingPortalPurchase.php @@ -330,9 +330,9 @@ class BillingPortalPurchase extends Component $is_eligible = $this->subscription->service()->isEligible($this->contact); - if (is_array($is_eligible)) { + if ($is_eligible['exception']['message'] != 'Success') { $this->steps['not_eligible'] = true; - $this->steps['not_eligible_message'] = $is_eligible['exception']; + $this->steps['not_eligible_message'] = $is_eligible['exception']['message']; $this->steps['show_loading_bar'] = false; return; diff --git a/app/Http/Livewire/RecurringInvoiceCancellation.php b/app/Http/Livewire/RecurringInvoiceCancellation.php index 4e03e3b09745..d3904c874330 100644 --- a/app/Http/Livewire/RecurringInvoiceCancellation.php +++ b/app/Http/Livewire/RecurringInvoiceCancellation.php @@ -25,7 +25,7 @@ class RecurringInvoiceCancellation extends Component public function processCancellation() { if ($this->invoice->subscription) { - return $this->invoice->subscription->service()->handleCancellation(); + return $this->invoice->subscription->service()->handleCancellation($this->invoice); } return redirect()->route('client.recurring_invoices.request_cancellation', ['recurring_invoice' => $this->invoice->hashed_id]); diff --git a/app/Http/Livewire/SubscriptionPlanSwitch.php b/app/Http/Livewire/SubscriptionPlanSwitch.php index 078096ab7f61..9ad9f35dc672 100644 --- a/app/Http/Livewire/SubscriptionPlanSwitch.php +++ b/app/Http/Livewire/SubscriptionPlanSwitch.php @@ -82,14 +82,15 @@ class SubscriptionPlanSwitch extends Component public function handleBeforePaymentEvents(): void { + $this->state['show_loading_bar'] = true; - $this->state['invoice'] = $this->target->service()->createChangePlanInvoice([ - 'recurring_invoice' => $this->recurring_invoice, - 'subscription' => $this->subscription, - 'target' => $this->target, - 'hash' => $this->hash, - ]); + $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, @@ -121,6 +122,18 @@ class SubscriptionPlanSwitch extends Component $this->handleBeforePaymentEvents(); } + public function handlePaymentNotRequired() + { + + return $this->target->service()->createChangePlanCredit([ + 'recurring_invoice' => $this->recurring_invoice, + 'subscription' => $this->subscription, + 'target' => $this->target, + 'hash' => $this->hash, + ]); + + } + public function render() { return render('components.livewire.subscription-plan-switch'); diff --git a/app/Http/Requests/Invoice/StoreInvoiceRequest.php b/app/Http/Requests/Invoice/StoreInvoiceRequest.php index 2da8e72603a6..9090badcb699 100644 --- a/app/Http/Requests/Invoice/StoreInvoiceRequest.php +++ b/app/Http/Requests/Invoice/StoreInvoiceRequest.php @@ -51,7 +51,7 @@ class StoreInvoiceRequest extends Request $rules['invitations.*.client_contact_id'] = 'distinct'; - $rules['number'] = ['nullable',Rule::unique('invoices')->where('company_id', auth()->user()->company()->id)]; + $rules['number'] = ['nullable', Rule::unique('invoices')->where('company_id', auth()->user()->company()->id)]; $rules['project_id'] = ['bail', 'sometimes', new ValidProjectForClient($this->all())]; diff --git a/app/Http/Requests/Subscription/UpdateSubscriptionRequest.php b/app/Http/Requests/Subscription/UpdateSubscriptionRequest.php index fdf72b188633..99d33048a5f0 100644 --- a/app/Http/Requests/Subscription/UpdateSubscriptionRequest.php +++ b/app/Http/Requests/Subscription/UpdateSubscriptionRequest.php @@ -56,7 +56,7 @@ class UpdateSubscriptionRequest extends Request '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)] + 'name' => ['sometimes', Rule::unique('subscriptions')->where('company_id', auth()->user()->company()->id)->ignore($this->subscription->id)] ]; return $this->globalRules($rules); diff --git a/app/Jobs/Company/CreateCompanyTaskStatuses.php b/app/Jobs/Company/CreateCompanyTaskStatuses.php index 3eec3f7203bd..99ec5658855d 100644 --- a/app/Jobs/Company/CreateCompanyTaskStatuses.php +++ b/app/Jobs/Company/CreateCompanyTaskStatuses.php @@ -45,10 +45,10 @@ class CreateCompanyTaskStatuses public function handle() { $task_statuses = [ - ['name' => ctrans('texts.backlog'), 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now()], - ['name' => ctrans('texts.ready_to_do'), 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now()], - ['name' => ctrans('texts.in_progress'), 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now()], - ['name' => ctrans('texts.done'), 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now()], + ['name' => ctrans('texts.backlog'), 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now(), 'status_order' => 1], + ['name' => ctrans('texts.ready_to_do'), 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now(), 'status_order' => 2], + ['name' => ctrans('texts.in_progress'), 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now(), 'status_order' => 3], + ['name' => ctrans('texts.done'), 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now(), 'status_order' => 4], ]; diff --git a/app/Models/Webhook.php b/app/Models/Webhook.php index 1372bf03e92c..c6adaa44139b 100644 --- a/app/Models/Webhook.php +++ b/app/Models/Webhook.php @@ -45,12 +45,29 @@ class Webhook extends BaseModel public static $valid_events = [ self::EVENT_CREATE_CLIENT, - self::EVENT_CREATE_PAYMENT, - self::EVENT_CREATE_QUOTE, self::EVENT_CREATE_INVOICE, + self::EVENT_CREATE_QUOTE, + self::EVENT_CREATE_PAYMENT, self::EVENT_CREATE_VENDOR, + self::EVENT_UPDATE_QUOTE, + self::EVENT_DELETE_QUOTE, + self::EVENT_UPDATE_INVOICE, + self::EVENT_DELETE_INVOICE, + self::EVENT_UPDATE_CLIENT, + self::EVENT_DELETE_CLIENT, + self::EVENT_DELETE_PAYMENT, + self::EVENT_UPDATE_VENDOR, + self::EVENT_DELETE_VENDOR, self::EVENT_CREATE_EXPENSE, + self::EVENT_UPDATE_EXPENSE, + self::EVENT_DELETE_EXPENSE, self::EVENT_CREATE_TASK, + self::EVENT_UPDATE_TASK, + self::EVENT_DELETE_TASK, + self::EVENT_APPROVE_QUOTE, + self::EVENT_LATE_INVOICE, + self::EVENT_EXPIRED_QUOTE, + self::EVENT_REMIND_INVOICE, ]; protected $fillable = [ diff --git a/app/Repositories/BaseRepository.php b/app/Repositories/BaseRepository.php index ddf2645d9051..9942698ef7f6 100644 --- a/app/Repositories/BaseRepository.php +++ b/app/Repositories/BaseRepository.php @@ -291,6 +291,10 @@ class BaseRepository /* Apply entity number */ $model = $model->service()->applyNumber()->save(); + /* Handle attempts where the deposit is greater than the amount/balance of the invoice */ + if((int)$model->balance != 0 && $model->partial > $model->amount) + $model->partial = min($model->amount, $model->balance); + /* Update product details if necessary */ if ($model->company->update_products) UpdateOrCreateProduct::dispatch($model->line_items, $model, $model->company); diff --git a/app/Repositories/PaymentRepository.php b/app/Repositories/PaymentRepository.php index ae9f7733b37c..43235573e257 100644 --- a/app/Repositories/PaymentRepository.php +++ b/app/Repositories/PaymentRepository.php @@ -155,7 +155,11 @@ class PaymentRepository extends BaseRepository { } if ( ! $is_existing_payment && ! $this->import_mode ) { - event( new PaymentWasCreated( $payment, $payment->company, Ninja::eventVars(auth()->user()->id) ) ); + + if ($payment->client->getSetting('client_manual_payment_notification')) + $payment->service()->sendEmail(); + + event( new PaymentWasCreated( $payment, $payment->company, Ninja::eventVars(auth()->user()->id) ) ); } nlog("payment amount = {$payment->amount}"); diff --git a/app/Services/Subscription/SubscriptionService.php b/app/Services/Subscription/SubscriptionService.php index 7c02d9aa6347..a75e9d0f0c75 100644 --- a/app/Services/Subscription/SubscriptionService.php +++ b/app/Services/Subscription/SubscriptionService.php @@ -12,6 +12,7 @@ namespace App\Services\Subscription; use App\DataMapper\InvoiceItem; +use App\Factory\CreditFactory; use App\Factory\InvoiceFactory; use App\Factory\InvoiceToRecurringInvoiceFactory; use App\Factory\RecurringInvoiceFactory; @@ -26,6 +27,7 @@ use App\Models\Product; use App\Models\RecurringInvoice; use App\Models\Subscription; use App\Models\SystemLog; +use App\Repositories\CreditRepository; use App\Repositories\InvoiceRepository; use App\Repositories\RecurringInvoiceRepository; use App\Repositories\SubscriptionRepository; @@ -130,7 +132,7 @@ class SubscriptionService ]; $response = $this->triggerWebhook($context); - nlog($response); + // nlog($response); return $response; } @@ -177,11 +179,7 @@ class SubscriptionService //execute any webhooks $response = $this->triggerWebhook($context); - 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('/client/recurring_invoices/'.$recurring_invoice->hashed_id); + return $this->handleRedirect('/client/recurring_invoices/'.$recurring_invoice->hashed_id); } public function calculateUpgradePrice(RecurringInvoice $recurring_invoice, Subscription $target) :?float @@ -198,15 +196,19 @@ class SubscriptionService $outstanding_amounts = $outstanding->sum('balance'); // $outstanding_invoices = $outstanding->get(); - $outstanding_invoices = $outstanding; + $outstanding_invoice = Invoice::where('subscription_id', $this->subscription->id) + ->where('client_id', $recurring_invoice->client_id) + ->where('is_deleted', 0) + ->orderBy('id', 'desc') + ->first(); if ($outstanding->count() == 0){ //nothing outstanding - return $target->price; + return $target->price - $this->calculateProRataRefund($outstanding_invoice); } elseif ($outstanding->count() == 1){ //user has multiple amounts outstanding - return $target->price - $this->calculateProRataRefund($outstanding->first()); + return $target->price - $this->calculateProRataRefund($outstanding_invoice); } elseif ($outstanding->count() > 1) { //user is changing plan mid frequency cycle @@ -231,15 +233,61 @@ class SubscriptionService $current_date = now(); - $days_to_refund = $start_date->diffInDays($current_date); + $days_of_subscription_used = $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); - + $pro_rata_refund = round((($days_in_frequency - $days_of_subscription_used)/$days_in_frequency) * $invoice->amount ,2); + return $pro_rata_refund; + } + /** + * Returns refundable set of line items + * transformed for direct injection into + * the invoice + * @param Invoice $invoice + * @return array + */ + private function calculateProRataRefundItems($invoice, $is_credit = false) :array + { + /* depending on whether we are creating an invoice or a credit*/ + $multiplier = $is_credit ? 1 : -1; + + $start_date = Carbon::parse($invoice->date); + + $current_date = now(); + + $days_of_subscription_used = $start_date->diffInDays($current_date); + + $days_in_frequency = $this->getDaysInFrequency(); + + $ratio = ($days_in_frequency - $days_of_subscription_used)/$days_in_frequency; + + $line_items = []; + + foreach($invoice->line_items as $item) + { + + if($item->product_key != ctrans('texts.refund')) + { + + $item->cost = ($item->cost*$ratio*$multiplier); + $item->product_key = ctrans('texts.refund'); + $item->notes = ctrans('texts.refund') . ": ". $item->notes; + + + $line_items[] = $item; + + } + } + + return $line_items; + + } + + /** * We only charge for the used days * @@ -253,79 +301,105 @@ class SubscriptionService $current_date = now(); - $days_to_refund = $start_date->diffInDays($current_date); + $days_to_charge = $start_date->diffInDays($current_date); $days_in_frequency = $this->getDaysInFrequency(); - $pro_rata_refund = round(($days_to_refund/$days_in_frequency) * $invoice->amount ,2); + nlog("days to charge = {$days_to_charge} fays in frequency = {$days_in_frequency}"); + + $pro_rata_charge = round(($days_to_charge/$days_in_frequency) * $invoice->amount ,2); - return $pro_rata_refund; + nlog("pro rata charge = {$pro_rata_charge}"); + + return $pro_rata_charge; + } + + public function createChangePlanCredit($data) + { + $recurring_invoice = $data['recurring_invoice']; + $old_subscription = $data['subscription']; + $target_subscription = $data['target']; + + $pro_rata_charge_amount = 0; + $pro_rata_refund_amount = 0; + + $last_invoice = Invoice::where('subscription_id', $recurring_invoice->subscription_id) + ->where('client_id', $recurring_invoice->client_id) + ->where('is_deleted', 0) + ->withTrashed() + ->orderBy('id', 'desc') + ->first(); + + if($last_invoice->balance > 0) + { + $pro_rata_charge_amount = $this->calculateProRataCharge($last_invoice, $old_subscription); + nlog("pro rata charge = {$pro_rata_charge_amount}"); + } + else + { + $pro_rata_refund_amount = $this->calculateProRataRefund($last_invoice, $old_subscription) * -1; + nlog("pro rata refund = {$pro_rata_refund_amount}"); + } + + $total_payable = $pro_rata_refund_amount + $pro_rata_charge_amount + $this->subscription->price; + + nlog("total payable = {$total_payable}"); + + $credit = $this->createCredit($pro_rata_refund_amount, $last_invoice, $target_subscription, $old_subscription); + + $new_recurring_invoice = $this->createNewRecurringInvoice($recurring_invoice); + + $context = [ + 'context' => 'change_plan', + 'recurring_invoice' => $new_recurring_invoice->hashed_id, + 'credit' => $credit->hashed_id, + 'client' => $new_recurring_invoice->client->hashed_id, + 'subscription' => $target_subscription->hashed_id, + 'contact' => auth('contact')->user()->hashed_id, + ]; + + $response = $this->triggerWebhook($context); + + nlog($response); + + return $this->handleRedirect('/client/credits/'.$credit->hashed_id); + } public function createChangePlanInvoice($data) { $recurring_invoice = $data['recurring_invoice']; - //Data array structure - /** - * [ - * 'recurring_invoice' => RecurringInvoice::class, - * 'subscription' => Subscription::class, - * 'target' => Subscription::class - * ] - */ - - // $outstanding_invoice = $recurring_invoice->invoices() - // ->where('is_deleted', 0) - // ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) - // ->where('balance', '>', 0) - // ->first(); + $old_subscription = $data['subscription']; + $target_subscription = $data['target']; $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() + $last_invoice = Invoice::where('subscription_id', $recurring_invoice->subscription_id) + ->where('client_id', $recurring_invoice->client_id) ->where('is_deleted', 0) + ->withTrashed() ->orderBy('id', 'desc') ->first(); - - //$last_invoice may not be here! - 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) + if($last_invoice->balance > 0) { - $pro_rata_charge_amount = $this->calculateProRataCharge($last_invoice); + $pro_rata_charge_amount = $this->calculateProRataCharge($last_invoice, $old_subscription); + nlog("pro rata charge = {$pro_rata_charge_amount}"); } else { - $pro_rata_refund_amount = $this->calculateProRataRefund($last_invoice) * -1; + $pro_rata_refund_amount = $this->calculateProRataRefund($last_invoice, $old_subscription) * -1; + nlog("pro rata refund = {$pro_rata_refund_amount}"); } $total_payable = $pro_rata_refund_amount + $pro_rata_charge_amount + $this->subscription->price; - if($total_payable > 0) - { - return $this->proRataInvoice($pro_rata_refund_amount, $data['subscription'], $data['target']); - } - else - { - //create credit - } + nlog("total payable = {$total_payable}"); - return Invoice::where('status_id', Invoice::STATUS_SENT)->first(); + return $this->proRataInvoice($pro_rata_refund_amount, $last_invoice, $target_subscription, $old_subscription); + } /** @@ -334,26 +408,10 @@ class SubscriptionService */ 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(); + $old_recurring_invoice = RecurringInvoice::find($payment_hash->data->billing_context->recurring_invoice); - $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(); + $recurring_invoice = $this->createNewRecurringInvoice($old_recurring_invoice); $context = [ 'context' => 'change_plan', @@ -368,18 +426,82 @@ class SubscriptionService 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); + return $this->handleRedirect('/client/recurring_invoices/'.$recurring_invoice->hashed_id); } - public function handlePlanChangeNoPayment() + private function createNewRecurringInvoice($old_recurring_invoice) :RecurringInvoice { + $old_recurring_invoice->service()->stop()->save(); + + $recurring_invoice_repo = new RecurringInvoiceRepository(); + $recurring_invoice_repo->archive($$old_recurring_invoice); + + $recurring_invoice = $this->convertInvoiceToRecurring($old_recurring_invoice->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(); + + return $recurring_invoice; + } + public function handlePlanChangeNoPayment($data) + { + /* + 'recurring_invoice' => $this->recurring_invoice, + 'subscription' => $this->subscription, + 'target' => $this->target, + 'hash' => $this->hash, + */ + + $recurring_invoice = $this->createNewRecurringInvoice($data['recurring_invoice']); + + $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); + + return $this->handleRedirect('/client/recurring_invoices/'.$recurring_invoice->hashed_id); + } + + private function createCredit($refund_amount, $last_invoice, $target, $old_subscription) + { + + $subscription_repo = new SubscriptionRepository(); + $credit_repo = new CreditRepository(); + + $credit = CreditFactory::create($this->subscription->company_id, $this->subscription->user_id); + $credit->date = now()->format('Y-m-d'); + $credit->subscription_id = $this->subscription->id; + + $line_items = $subscription_repo->generateLineItems($target); + + $credit->line_items = array_merge($line_items, $this->calculateProRataRefundItems($last_invoice, true)); + + $data = [ + 'client_id' => $last_invoice->client_id, + 'quantity' => 1, + 'date' => now()->format('Y-m-d'), + ]; + + return $credit_repo->save($data, $credit)->service()->markSent()->fillDefaults()->save(); + + } /** * 'client_id' => 2, 'date' => '2021-04-13', @@ -388,23 +510,21 @@ class SubscriptionService 'coupon' => '', 'quantity' => 1, */ - private function proRataInvoice($refund_amount, $subscription, $target) + private function proRataInvoice($refund_amount, $last_invoice, $target, $old_subscription) { $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 = $this->subscription->id; + $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; + $invoice->line_items = array_merge($line_items, $this->calculateProRataRefundItems($last_invoice)); - $line_items[] = $item; - $data = [ - 'client_id' => $subscription->client_id, + 'client_id' => $last_invoice->client_id, 'quantity' => 1, 'date' => now()->format('Y-m-d'), ]; @@ -538,40 +658,91 @@ class SubscriptionService ->get(); } - public function handleCancellation() + public function handleCancellation(RecurringInvoice $recurring_invoice) { - dd('Cancelling using SubscriptionService'); + //only allow cancellation of services that are paid up to date. + + // $last_invoice = + + //only refund if they are in the refund window. + $outstanding_invoice = Invoice::where('subscription_id', $this->subscription->id) + ->where('client_id', $recurring_invoice->client_id) + ->where('is_deleted', 0) + ->orderBy('id', 'desc') + ->first(); + + $invoice_start_date = Carbon::parse($outstanding_invoice->date); + $refund_end_date = $invoice_start_date->addSeconds($this->subscription->refund_period); + + /* Stop the recurring invoice and archive */ + $recurring_invoice->service()->stop()->save(); + $recurring_invoice_repo = new RecurringInvoiceRepository(); + $recurring_invoice_repo->archive($recurring_invoice); + + if($refund_end_date->greaterThan(now()) && (int)$outstanding_invoice->balance == 0) + { + //we are in the refund window. + // + //$outstanding_invoice + if($outstanding_invoice->payments()->exists()) + { + $payment = $outstanding_invoice->payments()->first(); + + $data = [ + 'id' => $payment->id, + 'gateway_refund' => true, + 'send_email' => true, + 'invoices' => [ + ['invoice_id' => $outstanding_invoice->id, 'amount' => $outstanding_invoice->amount], + ], + + ]; + + $payment->refund($data); + } + } + + $context = [ + 'context' => 'cancellation', + 'subscription' => $this->subscription->hashed_id, + 'recurring_invoice' => $recurring_invoice->hashed_id, + 'client' => $recurring_invoice->client->hashed_id, + 'contact' => auth('contact')->user()->hashed_id, + ]; + + $this->triggerWebhook($context); + + return $this->handleRedirect('client/subscriptions'); - // .. } private function getDaysInFrequency() { switch ($this->subscription->frequency_id) { - case self::FREQUENCY_DAILY: + case RecurringInvoice::FREQUENCY_DAILY: return 1; - case self::FREQUENCY_WEEKLY: + case RecurringInvoice::FREQUENCY_WEEKLY: return 7; - case self::FREQUENCY_TWO_WEEKS: + case RecurringInvoice::FREQUENCY_TWO_WEEKS: return 14; - case self::FREQUENCY_FOUR_WEEKS: + case RecurringInvoice::FREQUENCY_FOUR_WEEKS: return now()->diffInDays(now()->addWeeks(4)); - case self::FREQUENCY_MONTHLY: + case RecurringInvoice::FREQUENCY_MONTHLY: return now()->diffInDays(now()->addMonthNoOverflow()); - case self::FREQUENCY_TWO_MONTHS: + case RecurringInvoice::FREQUENCY_TWO_MONTHS: return now()->diffInDays(now()->addMonthNoOverflow(2)); - case self::FREQUENCY_THREE_MONTHS: + case RecurringInvoice::FREQUENCY_THREE_MONTHS: return now()->diffInDays(now()->addMonthNoOverflow(3)); - case self::FREQUENCY_FOUR_MONTHS: + case RecurringInvoice::FREQUENCY_FOUR_MONTHS: return now()->diffInDays(now()->addMonthNoOverflow(4)); - case self::FREQUENCY_SIX_MONTHS: + case RecurringInvoice::FREQUENCY_SIX_MONTHS: return now()->diffInDays(now()->addMonthNoOverflow(6)); - case self::FREQUENCY_ANNUALLY: + case RecurringInvoice::FREQUENCY_ANNUALLY: return now()->diffInDays(now()->addYear()); - case self::FREQUENCY_TWO_YEARS: + case RecurringInvoice::FREQUENCY_TWO_YEARS: return now()->diffInDays(now()->addYears(2)); - case self::FREQUENCY_THREE_YEARS: + case RecurringInvoice::FREQUENCY_THREE_YEARS: return now()->diffInDays(now()->addYears(3)); default: return 0; diff --git a/resources/views/portal/ninja2020/components/livewire/subscription-plan-switch.blade.php b/resources/views/portal/ninja2020/components/livewire/subscription-plan-switch.blade.php index ff43981e9128..47cf8f129928 100644 --- a/resources/views/portal/ninja2020/components/livewire/subscription-plan-switch.blade.php +++ b/resources/views/portal/ninja2020/components/livewire/subscription-plan-switch.blade.php @@ -1,6 +1,9 @@
+ + @if(isset($state['invoice'])) +
@@ -15,7 +18,6 @@
- @if($state['invoice'])
@csrf @@ -32,7 +34,6 @@
- @endif
@@ -61,5 +62,10 @@ @endif
+ @elseif($amount < 0) + + @endif
diff --git a/tests/Feature/SubscriptionApiTest.php b/tests/Feature/SubscriptionApiTest.php index 2ef470bbe649..cff411c33c4f 100644 --- a/tests/Feature/SubscriptionApiTest.php +++ b/tests/Feature/SubscriptionApiTest.php @@ -21,6 +21,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\WithFaker; use Illuminate\Support\Facades\Session; use Illuminate\Support\Str; +use Illuminate\Validation\ValidationException; use Tests\MockAccountData; use Tests\TestCase; @@ -40,6 +41,8 @@ class SubscriptionApiTest extends TestCase $this->makeTestData(); + $this->withoutExceptionHandling(); + Session::start(); $this->faker = \Faker\Factory::create(); @@ -92,33 +95,27 @@ class SubscriptionApiTest extends TestCase $product = Product::factory()->create([ 'company_id' => $this->company->id, 'user_id' => $this->user->id, - 'frequency_id' => RecurringInvoice::FREQUENCY_MONTHLY, ]); $response1 = $this ->withHeaders(['X-API-SECRET' => config('ninja.api_secret'),'X-API-TOKEN' => $this->token]) - ->post('/api/v1/subscriptions', ['product_ids' => $product->id, 'name' => Str::random(5)]) + ->post('/api/v1/subscriptions', ['product_ids' => $product->id, 'name' => Str::random(5)]) ->assertStatus(200) ->json(); - $response2 = $this + // try { + $response2 = $this ->withHeaders(['X-API-SECRET' => config('ninja.api_secret'),'X-API-TOKEN' => $this->token]) ->put('/api/v1/subscriptions/' . $response1['data']['id'], ['allow_cancellation' => true]) ->assertStatus(200) ->json(); + // }catch(ValidationException $e) { + // nlog($e->validator->getMessageBag()); + // } $this->assertNotEquals($response1['data']['allow_cancellation'], $response2['data']['allow_cancellation']); } - /* - TypeError : Argument 1 passed to App\Transformers\SubscriptionTransformer::transform() must be an instance of App\Models\Subscription, bool given, called in /var/www/html/vendor/league/fractal/src/Scope.php on line 407 - /var/www/html/app/Transformers/SubscriptionTransformer.php:35 - /var/www/html/vendor/league/fractal/src/Scope.php:407 - /var/www/html/vendor/league/fractal/src/Scope.php:349 - /var/www/html/vendor/league/fractal/src/Scope.php:235 - /var/www/html/app/Http/Controllers/BaseController.php:395 - /var/www/html/app/Http/Controllers/SubscriptionController.php:408 - */ public function testSubscriptionDeleted() {