mirror of
				https://github.com/invoiceninja/invoiceninja.git
				synced 2025-10-31 08:57:34 -04:00 
			
		
		
		
	sub v2
This commit is contained in:
		
							parent
							
								
									5e7a184118
								
							
						
					
					
						commit
						f1b81e1587
					
				
							
								
								
									
										128
									
								
								app/Services/Subscription/ChangePlanInvoice.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								app/Services/Subscription/ChangePlanInvoice.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,128 @@ | |||||||
|  | <?php | ||||||
|  | /** | ||||||
|  |  * Invoice Ninja (https://invoiceninja.com). | ||||||
|  |  * | ||||||
|  |  * @link https://github.com/invoiceninja/invoiceninja source repository | ||||||
|  |  * | ||||||
|  |  * @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com) | ||||||
|  |  * | ||||||
|  |  * @license https://www.elastic.co/licensing/elastic-license | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | namespace App\Services\Subscription; | ||||||
|  | 
 | ||||||
|  | use App\Models\Credit; | ||||||
|  | use App\Models\Invoice; | ||||||
|  | use App\Models\Subscription; | ||||||
|  | use App\Factory\CreditFactory; | ||||||
|  | use App\DataMapper\InvoiceItem; | ||||||
|  | use App\Factory\InvoiceFactory; | ||||||
|  | use App\Models\RecurringInvoice; | ||||||
|  | use App\Services\AbstractService; | ||||||
|  | use App\Repositories\CreditRepository; | ||||||
|  | use App\Repositories\InvoiceRepository; | ||||||
|  | use App\Repositories\SubscriptionRepository; | ||||||
|  | 
 | ||||||
|  | class ChangePlanInvoice extends AbstractService | ||||||
|  | {     | ||||||
|  |     protected \App\Services\Subscription\SubscriptionStatus $status; | ||||||
|  | 
 | ||||||
|  |     public function __construct(protected RecurringInvoice $recurring_invoice, public Subscription $target, public string $hash) | ||||||
|  |     { | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function run(): Invoice | Credit | ||||||
|  |     { | ||||||
|  | 
 | ||||||
|  |         $this->status = $this->recurring_invoice | ||||||
|  |                     ->subscription | ||||||
|  |                     ->status($this->recurring_invoice); | ||||||
|  | 
 | ||||||
|  |         //refund
 | ||||||
|  |         $refund = $this->status->getProRataRefund(); | ||||||
|  | 
 | ||||||
|  |         //newcharges
 | ||||||
|  |         $new_charge = $this->target->price; | ||||||
|  | 
 | ||||||
|  |         $invoice = $this->generateInvoice($refund); | ||||||
|  | 
 | ||||||
|  |         if($refund >= $new_charge){ | ||||||
|  |             $invoice = $invoice->markPaid()->save(); | ||||||
|  | 
 | ||||||
|  |             //generate new recurring invoice at this point as we know the user has succeeded with their upgrade.
 | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         if($refund > $new_charge) | ||||||
|  |             return $this->generateCredit($refund - $new_charge); | ||||||
|  | 
 | ||||||
|  |         return $invoice; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function generateCredit(float $credit_balance): Credit | ||||||
|  |     { | ||||||
|  | 
 | ||||||
|  |         $credit_repo = new CreditRepository(); | ||||||
|  | 
 | ||||||
|  |         $credit = CreditFactory::create($this->target->company_id, $this->target->user_id); | ||||||
|  |         $credit->status_id = Credit::STATUS_SENT; | ||||||
|  |         $credit->date = now()->addSeconds($this->recurring_invoice->client->timezone_offset())->format('Y-m-d'); | ||||||
|  |         $credit->subscription_id = $this->target->id; | ||||||
|  | 
 | ||||||
|  |         $invoice_item = new InvoiceItem(); | ||||||
|  |         $invoice_item->type_id = '1'; | ||||||
|  |         $invoice_item->product_key = ctrans('texts.credit'); | ||||||
|  |         $invoice_item->notes = ctrans('texts.credit') . " # {$this->recurring_invoice->subscription->name} #"; | ||||||
|  |         $invoice_item->quantity = 1; | ||||||
|  |         $invoice_item->cost = $credit_balance; | ||||||
|  | 
 | ||||||
|  |         $invoice_items = []; | ||||||
|  |         $invoice_items[] = $invoice_item; | ||||||
|  | 
 | ||||||
|  |         $data = [ | ||||||
|  |             'client_id' => $this->recurring_invoice->client_id, | ||||||
|  |             'date' => now()->format('Y-m-d'), | ||||||
|  |         ]; | ||||||
|  | 
 | ||||||
|  |         return $credit_repo->save($data, $credit)->service()->markSent()->fillDefaults()->save(); | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     //Careful with Invoice Numbers.
 | ||||||
|  |     private function generateInvoice(float $refund): Invoice | ||||||
|  |     { | ||||||
|  | 
 | ||||||
|  |         $subscription_repo = new SubscriptionRepository(); | ||||||
|  |         $invoice_repo = new InvoiceRepository(); | ||||||
|  | 
 | ||||||
|  |         $invoice = InvoiceFactory::create($this->target->company_id, $this->target->user_id); | ||||||
|  |         $invoice->date = now()->format('Y-m-d'); | ||||||
|  |         $invoice->subscription_id = $this->target->id; | ||||||
|  | 
 | ||||||
|  |         $invoice_item = new InvoiceItem(); | ||||||
|  |         $invoice_item->type_id = '1'; | ||||||
|  |         $invoice_item->product_key = ctrans('texts.refund'); | ||||||
|  |         $invoice_item->notes = ctrans('texts.refund'). " #{$this->status->refundable_invoice->number}"; | ||||||
|  |         $invoice_item->quantity = 1; | ||||||
|  |         $invoice_item->cost = $refund; | ||||||
|  | 
 | ||||||
|  |         $invoice_items = []; | ||||||
|  |         $invoice_items[] = $subscription_repo->generateLineItems($this->target); | ||||||
|  |         $invoice_items[] = $invoice_item; | ||||||
|  |         $invoice->line_items = $invoice_items; | ||||||
|  |         $invoice->is_proforma = true; | ||||||
|  | 
 | ||||||
|  |         $data = [ | ||||||
|  |             'client_id' => $this->recurring_invoice->client_id, | ||||||
|  |             'date' => now()->addSeconds($this->recurring_invoice->client->timezone_offset())->format('Y-m-d'), | ||||||
|  |         ]; | ||||||
|  | 
 | ||||||
|  |         $invoice = $invoice_repo->save($data, $invoice) | ||||||
|  |                                 ->service() | ||||||
|  |                                 ->markSent() | ||||||
|  |                                 ->fillDefaults() | ||||||
|  |                                 ->save(); | ||||||
|  | 
 | ||||||
|  |         return $invoice; | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										70
									
								
								app/Services/Subscription/InvoiceToRecurring.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								app/Services/Subscription/InvoiceToRecurring.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,70 @@ | |||||||
|  | <?php | ||||||
|  | /** | ||||||
|  |  * Invoice Ninja (https://invoiceninja.com). | ||||||
|  |  * | ||||||
|  |  * @link https://github.com/invoiceninja/invoiceninja source repository | ||||||
|  |  * | ||||||
|  |  * @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com) | ||||||
|  |  * | ||||||
|  |  * @license https://www.elastic.co/licensing/elastic-license | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | namespace App\Services\Subscription; | ||||||
|  | 
 | ||||||
|  | use App\Models\Client; | ||||||
|  | use App\Libraries\MultiDB; | ||||||
|  | use App\Models\Subscription; | ||||||
|  | use App\Models\RecurringInvoice; | ||||||
|  | use App\Services\AbstractService; | ||||||
|  | use App\Factory\RecurringInvoiceFactory; | ||||||
|  | use App\Repositories\SubscriptionRepository; | ||||||
|  | 
 | ||||||
|  | class InvoiceToRecurring extends AbstractService | ||||||
|  | {     | ||||||
|  | 
 | ||||||
|  |     protected \App\Services\Subscription\SubscriptionStatus $status; | ||||||
|  | 
 | ||||||
|  |     public function __construct(protected int $client_id, public Subscription $subscription, public array $bundle = []) | ||||||
|  |     { | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     public function run(): RecurringInvoice | ||||||
|  |     { | ||||||
|  | 
 | ||||||
|  |         MultiDB::setDb($this->subscription->company->db); | ||||||
|  | 
 | ||||||
|  |         $client = Client::withTrashed()->find($this->client_id); | ||||||
|  | 
 | ||||||
|  |         $subscription_repo = new SubscriptionRepository(); | ||||||
|  | 
 | ||||||
|  |         $line_items = count($this->bundle) > 1 ? $subscription_repo->generateBundleLineItems($this->bundle, true, false) : $subscription_repo->generateLineItems($this->subscription, true, false); | ||||||
|  | 
 | ||||||
|  |         $recurring_invoice = RecurringInvoiceFactory::create($this->subscription->company_id, $this->subscription->user_id); | ||||||
|  |         $recurring_invoice->client_id = $this->client_id; | ||||||
|  |         $recurring_invoice->line_items = $line_items; | ||||||
|  |         $recurring_invoice->subscription_id = $this->subscription->id; | ||||||
|  |         $recurring_invoice->frequency_id = $this->subscription->frequency_id ?: RecurringInvoice::FREQUENCY_MONTHLY; | ||||||
|  |         $recurring_invoice->date = now(); | ||||||
|  |         $recurring_invoice->remaining_cycles = -1; | ||||||
|  |         $recurring_invoice->auto_bill = $client->getSetting('auto_bill'); | ||||||
|  |         $recurring_invoice->auto_bill_enabled =  $this->setAutoBillFlag($recurring_invoice->auto_bill); | ||||||
|  |         $recurring_invoice->due_date_days = 'terms'; | ||||||
|  |         $recurring_invoice->next_send_date = now()->format('Y-m-d'); | ||||||
|  |         $recurring_invoice->next_send_date_client = now()->format('Y-m-d'); | ||||||
|  |         $recurring_invoice->next_send_date =  $recurring_invoice->nextSendDate(); | ||||||
|  |         $recurring_invoice->next_send_date_client = $recurring_invoice->nextSendDateClient(); | ||||||
|  | 
 | ||||||
|  |         return $recurring_invoice; | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function setAutoBillFlag($auto_bill): bool | ||||||
|  |     { | ||||||
|  |         if ($auto_bill == 'always' || $auto_bill == 'optout') { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -11,13 +11,26 @@ | |||||||
| 
 | 
 | ||||||
| namespace App\Services\Subscription; | namespace App\Services\Subscription; | ||||||
| 
 | 
 | ||||||
|  | use App\Models\Client; | ||||||
|  | use App\Models\Credit; | ||||||
|  | use App\Models\Invoice; | ||||||
|  | use App\Models\SystemLog; | ||||||
| use App\Models\PaymentHash; | use App\Models\PaymentHash; | ||||||
| use App\Models\Subscription; | use App\Models\Subscription; | ||||||
| use App\Models\ClientContact; | use App\Models\ClientContact; | ||||||
|  | use GuzzleHttp\RequestOptions; | ||||||
|  | use App\Jobs\Util\SystemLogger; | ||||||
|  | use App\Utils\Traits\MakesHash; | ||||||
| use App\Models\RecurringInvoice; | use App\Models\RecurringInvoice; | ||||||
|  | use GuzzleHttp\Exception\ClientException; | ||||||
|  | use App\Services\Subscription\UpgradePrice; | ||||||
|  | use App\Services\Subscription\ZeroCostProduct; | ||||||
|  | use App\Repositories\RecurringInvoiceRepository; | ||||||
|  | use App\Services\Subscription\ChangePlanInvoice; | ||||||
| 
 | 
 | ||||||
| class PaymentLinkService | class PaymentLinkService | ||||||
| { | { | ||||||
|  |     use MakesHash; | ||||||
|      |      | ||||||
|     public const WHITE_LABEL = 4316; |     public const WHITE_LABEL = 4316; | ||||||
| 
 | 
 | ||||||
| @ -32,11 +45,85 @@ class PaymentLinkService | |||||||
|      * or recurring product |      * or recurring product | ||||||
|      *  |      *  | ||||||
|      * @param  PaymentHash $payment_hash |      * @param  PaymentHash $payment_hash | ||||||
|      * @return Illuminate\Routing\Redirector |      * @return  \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse|null | ||||||
|      */ |      */ | ||||||
|     public function completePurchase(PaymentHash $payment_hash): \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse |     public function completePurchase(PaymentHash $payment_hash): \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse|null | ||||||
|     { |     { | ||||||
|          |          | ||||||
|  |         if (!property_exists($payment_hash->data, '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 ($payment_hash->data->billing_context->context == 'whitelabel') {
 | ||||||
|  |         //     return $this->handleWhiteLabelPurchase($payment_hash);
 | ||||||
|  |         // }
 | ||||||
|  | 
 | ||||||
|  |         if (strlen($this->subscription->recurring_product_ids) >= 1) { | ||||||
|  | 
 | ||||||
|  |             $bundle = isset($payment_hash->data->billing_context->bundle) ? $payment_hash->data->billing_context->bundle : []; | ||||||
|  |             $recurring_invoice = (new InvoiceToRecurring($payment_hash->payment->client_id, $this->subscription, $bundle))->run(); | ||||||
|  | 
 | ||||||
|  |             $recurring_invoice_repo = new RecurringInvoiceRepository(); | ||||||
|  | 
 | ||||||
|  |             $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::withTrashed()->find($payment_hash->fee_invoice_id); | ||||||
|  |             $invoice->recurring_id = $recurring_invoice->id; | ||||||
|  |             $invoice->is_proforma = false; | ||||||
|  |             $invoice->save(); | ||||||
|  | 
 | ||||||
|  |             //execute any webhooks
 | ||||||
|  |             $context = [ | ||||||
|  |                 'context' => 'recurring_purchase', | ||||||
|  |                 '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()->guard('contact')->user() ? auth()->guard('contact')->user()->hashed_id : $recurring_invoice->client->contacts()->whereNotNull('email')->first()->hashed_id, | ||||||
|  |                 'account_key' => $recurring_invoice->client->custom_value2, | ||||||
|  |             ]; | ||||||
|  | 
 | ||||||
|  |             if (property_exists($payment_hash->data->billing_context, 'campaign')) { | ||||||
|  |                 $context['campaign'] = $payment_hash->data->billing_context->campaign; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             $response = $this->triggerWebhook($context); | ||||||
|  | 
 | ||||||
|  |             return $this->handleRedirect('/client/recurring_invoices/' . $recurring_invoice->hashed_id); | ||||||
|  |         } else { | ||||||
|  |             $invoice = Invoice::withTrashed()->find($payment_hash->fee_invoice_id); | ||||||
|  | 
 | ||||||
|  |             $context = [ | ||||||
|  |                 'context' => 'single_purchase', | ||||||
|  |                 'invoice' => $this->encodePrimaryKey($payment_hash->fee_invoice_id), | ||||||
|  |                 'client'  => $invoice->client->hashed_id, | ||||||
|  |                 'subscription' => $this->subscription->hashed_id, | ||||||
|  |                 'account_key' => $invoice->client->custom_value2, | ||||||
|  |             ]; | ||||||
|  | 
 | ||||||
|  |             //execute any webhooks
 | ||||||
|  |             $this->triggerWebhook($context); | ||||||
|  | 
 | ||||||
|  |             /* 06-04-2022 */ | ||||||
|  |             /* We may not be in a state where the user is present */ | ||||||
|  |             if (auth()->guard('contact')) { | ||||||
|  |                 return $this->handleRedirect('/client/invoices/' . $this->encodePrimaryKey($payment_hash->fee_invoice_id)); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return null; | ||||||
|  | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -48,6 +135,19 @@ class PaymentLinkService | |||||||
|     public function isEligible(ClientContact $contact): array |     public function isEligible(ClientContact $contact): array | ||||||
|     { |     { | ||||||
|                  |                  | ||||||
|  |         $context = [ | ||||||
|  |                     'context' => 'is_eligible', | ||||||
|  |                     'subscription' => $this->subscription->hashed_id, | ||||||
|  |                     'contact' => $contact->hashed_id, | ||||||
|  |                     'contact_email' => $contact->email, | ||||||
|  |                     'client' => $contact->client->hashed_id, | ||||||
|  |                     'account_key' => $contact->client->custom_value2, | ||||||
|  |                 ]; | ||||||
|  | 
 | ||||||
|  |         $response = $this->triggerWebhook($context); | ||||||
|  | 
 | ||||||
|  |         return $response; | ||||||
|  | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /* Starts the process to create a trial |     /* Starts the process to create a trial | ||||||
| @ -58,11 +158,68 @@ class PaymentLinkService | |||||||
|      * startTrial |      * startTrial | ||||||
|      * |      * | ||||||
|      * @param  array $data{contact_id: int, client_id: int, bundle: \Illuminate\Support\Collection, coupon?: string, } |      * @param  array $data{contact_id: int, client_id: int, bundle: \Illuminate\Support\Collection, coupon?: string, } | ||||||
|      * @return Illuminate\Routing\Redirector |      * @return \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse | ||||||
|      */ |      */ | ||||||
|     public function startTrial(array $data): \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse |     public function startTrial(array $data): \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse | ||||||
|     { |     { | ||||||
| 
 | 
 | ||||||
|  |         // Redirects from here work just fine. Livewire will respect it.
 | ||||||
|  |         $client_contact = ClientContact::find($this->decodePrimaryKey($data['contact_id'])); | ||||||
|  | 
 | ||||||
|  |         if(is_string($data['client_id'])) { | ||||||
|  |             $data['client_id'] = $this->decodePrimaryKey($data['client_id']); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (!$this->subscription->trial_enabled) { | ||||||
|  |             return new \Exception("Trials are disabled for this product"); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         //create recurring invoice with start date = trial_duration + 1 day
 | ||||||
|  |         $recurring_invoice_repo = new RecurringInvoiceRepository(); | ||||||
|  | 
 | ||||||
|  |         $bundle = []; | ||||||
|  | 
 | ||||||
|  |         if (isset($data['bundle'])) { | ||||||
|  | 
 | ||||||
|  |             $bundle = $data['bundle']->map(function ($bundle) { | ||||||
|  |                 return (object) $bundle; | ||||||
|  |             })->toArray(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $recurring_invoice = (new InvoiceToRecurring($client_contact->client_id, $this->subscription, $bundle))->run(); | ||||||
|  |              | ||||||
|  |         $recurring_invoice->next_send_date = now()->addSeconds($this->subscription->trial_duration); | ||||||
|  |         $recurring_invoice->next_send_date_client = now()->addSeconds($this->subscription->trial_duration); | ||||||
|  |         $recurring_invoice->backup = 'is_trial'; | ||||||
|  | 
 | ||||||
|  |         if (array_key_exists('coupon', $data) && ($data['coupon'] == $this->subscription->promo_code) && $this->subscription->promo_discount > 0) { | ||||||
|  |             $recurring_invoice->discount = $this->subscription->promo_discount; | ||||||
|  |             $recurring_invoice->is_amount_discount = $this->subscription->is_amount_discount; | ||||||
|  |         } elseif (strlen($this->subscription->promo_code ?? '') == 0 && $this->subscription->promo_discount > 0) { | ||||||
|  |             $recurring_invoice->discount = $this->subscription->promo_discount; | ||||||
|  |             $recurring_invoice->is_amount_discount = $this->subscription->is_amount_discount; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $recurring_invoice = $recurring_invoice_repo->save($data, $recurring_invoice); | ||||||
|  | 
 | ||||||
|  |         /* Start the recurring service */ | ||||||
|  |         $recurring_invoice->service() | ||||||
|  |                         ->start() | ||||||
|  |                         ->save(); | ||||||
|  | 
 | ||||||
|  |         $context = [ | ||||||
|  |             'context' => 'trial', | ||||||
|  |             'recurring_invoice' => $recurring_invoice->hashed_id, | ||||||
|  |             'client' => $recurring_invoice->client->hashed_id, | ||||||
|  |             'subscription' => $this->subscription->hashed_id, | ||||||
|  |             'account_key' => $recurring_invoice->client->custom_value2, | ||||||
|  |         ]; | ||||||
|  | 
 | ||||||
|  |         //execute any webhooks
 | ||||||
|  |         $response = $this->triggerWebhook($context); | ||||||
|  | 
 | ||||||
|  |         return $this->handleRedirect('/client/recurring_invoices/' . $recurring_invoice->hashed_id); | ||||||
|  | 
 | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     /** |     /** | ||||||
| @ -76,6 +233,238 @@ class PaymentLinkService | |||||||
|      */ |      */ | ||||||
|     public function calculateUpgradePriceV2(RecurringInvoice $recurring_invoice, Subscription $target): ?float |     public function calculateUpgradePriceV2(RecurringInvoice $recurring_invoice, Subscription $target): ?float | ||||||
|     { |     { | ||||||
|        return (new UpgradePrice($recurring_invoice, $target))->run(); |        return (new UpgradePrice($recurring_invoice, $target))->run()->upgrade_price; | ||||||
|  |     } | ||||||
|  |          | ||||||
|  |     /** | ||||||
|  |      * When changing plans, we need to generate a pro rata invoice | ||||||
|  |      * | ||||||
|  |      * @param  array $data{recurring_invoice: RecurringInvoice, subscription: Subscription, target: Subscription, hash: string} | ||||||
|  |      * @return Invoice | Credit | ||||||
|  |      */ | ||||||
|  |     public function createChangePlanInvoice($data): Invoice | Credit | ||||||
|  |     { | ||||||
|  |             $recurring_invoice = $data['recurring_invoice']; | ||||||
|  |             $old_subscription = $data['subscription']; | ||||||
|  |             $target_subscription = $data['target']; | ||||||
|  |             $hash = $data['hash']; | ||||||
|  |      | ||||||
|  |         return (new ChangePlanInvoice($recurring_invoice, $target_subscription, $hash))->run(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |     * 'email' => $this->email ?? $this->contact->email, | ||||||
|  |     * 'quantity' => $this->quantity, | ||||||
|  |     * 'contact_id' => $this->contact->id, | ||||||
|  |     * | ||||||
|  |     * @param array $data | ||||||
|  |     * @return \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse | ||||||
|  |     */ | ||||||
|  |     public function handleNoPaymentRequired(array $data): \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse | ||||||
|  |     { | ||||||
|  |         $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']); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @param Invoice $invoice | ||||||
|  |      * @return true | ||||||
|  |      * @throws BindingResolutionException | ||||||
|  |      */ | ||||||
|  |     public function planPaid(Invoice $invoice) | ||||||
|  |     { | ||||||
|  |         $recurring_invoice_hashed_id = $invoice->recurring_invoice()->exists() ? $invoice->recurring_invoice->hashed_id : null; | ||||||
|  | 
 | ||||||
|  |         $context = [ | ||||||
|  |             'context' => 'plan_paid', | ||||||
|  |             'subscription' => $this->subscription->hashed_id, | ||||||
|  |             'recurring_invoice' => $recurring_invoice_hashed_id, | ||||||
|  |             'client' => $invoice->client->hashed_id, | ||||||
|  |             'contact' => $invoice->client->primary_contact()->first() ? $invoice->client->primary_contact()->first()->hashed_id : $invoice->client->contacts->first()->hashed_id, | ||||||
|  |             'invoice' => $invoice->hashed_id, | ||||||
|  |             'account_key' => $invoice->client->custom_value2, | ||||||
|  |         ]; | ||||||
|  | 
 | ||||||
|  |         $response = $this->triggerWebhook($context); | ||||||
|  | 
 | ||||||
|  |         nlog($response); | ||||||
|  | 
 | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Response from payment service on | ||||||
|  |      * return from a plan change | ||||||
|  |      * | ||||||
|  |      * @param  PaymentHash $payment_hash | ||||||
|  |      */ | ||||||
|  |     private function handlePlanChange(PaymentHash $payment_hash): \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse | ||||||
|  |     { | ||||||
|  |         nlog("handle plan change"); | ||||||
|  | 
 | ||||||
|  |         $old_recurring_invoice = RecurringInvoice::query()->find($this->decodePrimaryKey($payment_hash->data->billing_context->recurring_invoice)); | ||||||
|  | 
 | ||||||
|  |         if (!$old_recurring_invoice) { | ||||||
|  |             return $this->handleRedirect('/client/recurring_invoices/'); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $old_recurring_invoice->service()->stop()->save(); | ||||||
|  | 
 | ||||||
|  |         $recurring_invoice = (new InvoiceToRecurring($old_recurring_invoice->client_id, $this->subscription, []))->run(); | ||||||
|  |          | ||||||
|  |         $recurring_invoice->service() | ||||||
|  |                         ->start() | ||||||
|  |                         ->save(); | ||||||
|  | 
 | ||||||
|  |         //update the invoice and attach to the recurring invoice!!!!!
 | ||||||
|  |         $invoice = Invoice::query()->find($payment_hash->fee_invoice_id); | ||||||
|  |         $invoice->recurring_id = $recurring_invoice->id; | ||||||
|  |         $invoice->is_proforma = false; | ||||||
|  |         $invoice->save(); | ||||||
|  | 
 | ||||||
|  |         // 29-06-2023 handle webhooks for payment intent - user may not be present.
 | ||||||
|  |         $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()->guard('contact')->user()?->hashed_id ?? $recurring_invoice->client->contacts()->first()->hashed_id, | ||||||
|  |             'account_key' => $recurring_invoice->client->custom_value2, | ||||||
|  |         ]; | ||||||
|  | 
 | ||||||
|  |         $response = $this->triggerWebhook($context); | ||||||
|  | 
 | ||||||
|  |         nlog($response); | ||||||
|  | 
 | ||||||
|  |         return $this->handleRedirect('/client/recurring_invoices/'.$recurring_invoice->hashed_id); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Handles redirecting the user | ||||||
|  |      */ | ||||||
|  |     private function handleRedirect($default_redirect): \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse  | ||||||
|  |     { | ||||||
|  |         if (array_key_exists('return_url', $this->subscription->webhook_configuration) && strlen($this->subscription->webhook_configuration['return_url']) >= 1) { | ||||||
|  |             return method_exists(redirect(), "send") ? redirect($this->subscription->webhook_configuration['return_url'])->send() : redirect($this->subscription->webhook_configuration['return_url']); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return method_exists(redirect(), "send") ? redirect($default_redirect)->send() : redirect($default_redirect); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Hit a 3rd party API if defined in the subscription | ||||||
|  |      * | ||||||
|  |      * @param  array $context | ||||||
|  |      * @return array | ||||||
|  |      */ | ||||||
|  |     public function triggerWebhook($context): array | ||||||
|  |     { | ||||||
|  |         if (empty($this->subscription->webhook_configuration['post_purchase_url']) || is_null($this->subscription->webhook_configuration['post_purchase_url']) || strlen($this->subscription->webhook_configuration['post_purchase_url']) < 1) { | ||||||
|  |             return ["message" => "Success", "status_code" => 200]; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $response = false; | ||||||
|  | 
 | ||||||
|  |         $body = array_merge($context, [ | ||||||
|  |             'db' => $this->subscription->company->db, | ||||||
|  |         ]); | ||||||
|  | 
 | ||||||
|  |         $response = $this->sendLoad($this->subscription, $body); | ||||||
|  | 
 | ||||||
|  |         /* Append the response to the system logger body */ | ||||||
|  |         if (is_array($response)) { | ||||||
|  |             $body = $response; | ||||||
|  |         } else { | ||||||
|  |             $body = $response->getStatusCode(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $client = Client::query()->where('id', $this->decodePrimaryKey($body['client']))->withTrashed()->first(); | ||||||
|  | 
 | ||||||
|  |         SystemLogger::dispatch( | ||||||
|  |             $body, | ||||||
|  |             SystemLog::CATEGORY_WEBHOOK, | ||||||
|  |             SystemLog::EVENT_WEBHOOK_RESPONSE, | ||||||
|  |             SystemLog::TYPE_WEBHOOK_RESPONSE, | ||||||
|  |             $client, | ||||||
|  |             $client->company, | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         nlog("ready to fire back"); | ||||||
|  | 
 | ||||||
|  |         if (is_array($body)) { | ||||||
|  |             return $response; | ||||||
|  |         } else { | ||||||
|  |             return ['message' => 'There was a problem encountered with the webhook', 'status_code' => 500]; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function sendLoad($subscription, $body) | ||||||
|  |     { | ||||||
|  |         $headers = [ | ||||||
|  |             'Content-Type' => 'application/json', | ||||||
|  |             'X-Requested-With' => 'XMLHttpRequest', | ||||||
|  |         ]; | ||||||
|  | 
 | ||||||
|  |         if (!isset($subscription->webhook_configuration['post_purchase_url']) && !isset($subscription->webhook_configuration['post_purchase_rest_method'])) { | ||||||
|  |             return []; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (count($subscription->webhook_configuration['post_purchase_headers']) >= 1) { | ||||||
|  |             $headers = array_merge($headers, $subscription->webhook_configuration['post_purchase_headers']); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $client = new \GuzzleHttp\Client( | ||||||
|  |             [ | ||||||
|  |                 'headers' => $headers, | ||||||
|  |             ] | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         $post_purchase_rest_method = (string) $subscription->webhook_configuration['post_purchase_rest_method']; | ||||||
|  |         $post_purchase_url = (string) $subscription->webhook_configuration['post_purchase_url']; | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             $response = $client->{$post_purchase_rest_method}($post_purchase_url, [ | ||||||
|  |                 RequestOptions::JSON => ['body' => $body], RequestOptions::ALLOW_REDIRECTS => false, | ||||||
|  |             ]); | ||||||
|  | 
 | ||||||
|  |             return array_merge($body, json_decode($response->getBody(), true)); | ||||||
|  |         } catch (ClientException $e) { | ||||||
|  |             $message = $e->getMessage(); | ||||||
|  | 
 | ||||||
|  |             $error = json_decode($e->getResponse()->getBody()->getContents()); | ||||||
|  | 
 | ||||||
|  |             if (is_null($error)) { | ||||||
|  |                 nlog("empty response"); | ||||||
|  |                 nlog($e->getMessage()); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if ($error && property_exists($error, 'message')) { | ||||||
|  |                 $message = $error->message; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return array_merge($body, ['message' => $message, 'status_code' => 500]); | ||||||
|  |         } catch (\Exception $e) { | ||||||
|  |             return array_merge($body, ['message' => $e->getMessage(), 'status_code' => 500]); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @ -1,126 +0,0 @@ | |||||||
| <?php |  | ||||||
| /** |  | ||||||
|  * Invoice Ninja (https://invoiceninja.com). |  | ||||||
|  * |  | ||||||
|  * @link https://github.com/invoiceninja/invoiceninja source repository |  | ||||||
|  * |  | ||||||
|  * @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com) |  | ||||||
|  * |  | ||||||
|  * @license https://www.elastic.co/licensing/elastic-license |  | ||||||
|  */ |  | ||||||
| 
 |  | ||||||
| namespace App\Services\Subscription; |  | ||||||
| 
 |  | ||||||
| use App\Models\Invoice; |  | ||||||
| use App\Models\Subscription; |  | ||||||
| use Illuminate\Support\Carbon; |  | ||||||
| use App\Models\RecurringInvoice; |  | ||||||
| use App\Services\AbstractService; |  | ||||||
| 
 |  | ||||||
| class ProRata extends AbstractService |  | ||||||
| {     |  | ||||||
|     /** @var bool $is_trial */ |  | ||||||
|     private bool $is_trial = false; |  | ||||||
|          |  | ||||||
|     /** @var \Illuminate\Database\Eloquent\Collection<Invoice> | null $unpaid_invoices */ |  | ||||||
|     private $unpaid_invoices = null; |  | ||||||
|      |  | ||||||
|     /** @var bool $refundable */ |  | ||||||
|     private bool $refundable = false; |  | ||||||
|      |  | ||||||
|     /** @var int $pro_rata_duration */ |  | ||||||
|     private int $pro_rata_duration = 0; |  | ||||||
|          |  | ||||||
|     /** @var int $subscription_interval_duration */ |  | ||||||
|     private int $subscription_interval_duration = 0; |  | ||||||
|      |  | ||||||
|     /** @var int $pro_rata_ratio */ |  | ||||||
|     private int $pro_rata_ratio = 1; |  | ||||||
| 
 |  | ||||||
|     public function __construct(public Subscription $subscription, protected RecurringInvoice $recurring_invoice) |  | ||||||
|     { |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public function run() |  | ||||||
|     { |  | ||||||
|         $this->setCalculations(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private function setCalculations(): self |  | ||||||
|     { |  | ||||||
|         $this->isInTrialPeriod() |  | ||||||
|              ->checkUnpaidInvoices() |  | ||||||
|              ->checkRefundPeriod() |  | ||||||
|              ->checkProRataDuration() |  | ||||||
|              ->calculateSubscriptionIntervalDuration() |  | ||||||
|              ->calculateProRataRatio(); |  | ||||||
| 
 |  | ||||||
|         return $this; |  | ||||||
|     } |  | ||||||
|                  |  | ||||||
|     private function calculateProRataRatio(): self |  | ||||||
|     { |  | ||||||
|         if($this->pro_rata_duration < $this->subscription_interval_duration) |  | ||||||
|             $this->setProRataRatio($this->pro_rata_duration/$this->subscription_interval_duration); |  | ||||||
| 
 |  | ||||||
|         return $this; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     private function calculateSubscriptionIntervalDuration(): self |  | ||||||
|     { |  | ||||||
|         |  | ||||||
|         $primary_invoice = $this->recurring_invoice |  | ||||||
|                                 ->invoices() |  | ||||||
|                                 ->where('is_deleted', 0) |  | ||||||
|                                 ->where('is_proforma', 0) |  | ||||||
|                                 ->orderBy('id', 'desc') |  | ||||||
|                                 ->first(); |  | ||||||
| 
 |  | ||||||
|         if(!$primary_invoice) |  | ||||||
|             return $this->setSubscriptionIntervalDuration(0); |  | ||||||
| 
 |  | ||||||
|         $start = Carbon::parse($primary_invoice->date); |  | ||||||
|         $end = Carbon::parse($this->recurring_invoice->next_send_date_client); |  | ||||||
| 
 |  | ||||||
|         $this->setSubscriptionIntervalDuration($start->diffInSeconds($end)); |  | ||||||
| 
 |  | ||||||
|         return $this; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|      |  | ||||||
|     private function setProRataRatio(int $ratio): self |  | ||||||
|     { |  | ||||||
|         $this->pro_rata_ratio = $ratio; |  | ||||||
| 
 |  | ||||||
|         return $this; |  | ||||||
|     } |  | ||||||
|     /** |  | ||||||
|      * setSubscriptionIntervalDuration |  | ||||||
|      * |  | ||||||
|      * @param  int $seconds |  | ||||||
|      * @return self |  | ||||||
|      */ |  | ||||||
|     private function setSubscriptionIntervalDuration(int $seconds): self |  | ||||||
|     { |  | ||||||
|         $this->subscription_interval_duration = $seconds; |  | ||||||
| 
 |  | ||||||
|         return $this; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * setProRataDuration |  | ||||||
|      * |  | ||||||
|      * @param  int $seconds |  | ||||||
|      * @return self |  | ||||||
|      */ |  | ||||||
|     private function setProRataDuration(int $seconds): self |  | ||||||
|     { |  | ||||||
|         $this->pro_rata_duration = $seconds; |  | ||||||
| 
 |  | ||||||
|         return $this; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|          |  | ||||||
| 
 |  | ||||||
| } |  | ||||||
| @ -763,7 +763,7 @@ class SubscriptionService | |||||||
|     /** |     /** | ||||||
|      * When changing plans, we need to generate a pro rata invoice |      * When changing plans, we need to generate a pro rata invoice | ||||||
|      * |      * | ||||||
|      * @param  array $data |      * @param  array $data{recurring_invoice: RecurringInvoice, subscription: Subscription, target: Subscription} | ||||||
|      * @return Invoice |      * @return Invoice | ||||||
|      */ |      */ | ||||||
|     public function createChangePlanInvoice($data) |     public function createChangePlanInvoice($data) | ||||||
| @ -1087,12 +1087,12 @@ class SubscriptionService | |||||||
|         $recurring_invoice->line_items = $subscription_repo->generateBundleLineItems($bundle, true, false); |         $recurring_invoice->line_items = $subscription_repo->generateBundleLineItems($bundle, true, false); | ||||||
|         $recurring_invoice->subscription_id = $this->subscription->id; |         $recurring_invoice->subscription_id = $this->subscription->id; | ||||||
|         $recurring_invoice->frequency_id = $this->subscription->frequency_id ?: RecurringInvoice::FREQUENCY_MONTHLY; |         $recurring_invoice->frequency_id = $this->subscription->frequency_id ?: RecurringInvoice::FREQUENCY_MONTHLY; | ||||||
|         $recurring_invoice->date = now(); |         $recurring_invoice->date = now()->addSeconds($client->timezone_offset()); | ||||||
|         $recurring_invoice->remaining_cycles = -1; |         $recurring_invoice->remaining_cycles = -1; | ||||||
|         $recurring_invoice->auto_bill = $client->getSetting('auto_bill'); |         $recurring_invoice->auto_bill = $client->getSetting('auto_bill'); | ||||||
|         $recurring_invoice->auto_bill_enabled =  $this->setAutoBillFlag($recurring_invoice->auto_bill); |         $recurring_invoice->auto_bill_enabled =  $this->setAutoBillFlag($recurring_invoice->auto_bill); | ||||||
|         $recurring_invoice->due_date_days = 'terms'; |         $recurring_invoice->due_date_days = 'terms'; | ||||||
|         $recurring_invoice->next_send_date = now()->format('Y-m-d'); |         $recurring_invoice->next_send_date = now()->addSeconds($client->timezone_offset())->format('Y-m-d'); | ||||||
|         $recurring_invoice->next_send_date_client = now()->format('Y-m-d'); |         $recurring_invoice->next_send_date_client = now()->format('Y-m-d'); | ||||||
|         $recurring_invoice->next_send_date =  $recurring_invoice->nextSendDate(); |         $recurring_invoice->next_send_date =  $recurring_invoice->nextSendDate(); | ||||||
|         $recurring_invoice->next_send_date_client = $recurring_invoice->nextSendDateClient(); |         $recurring_invoice->next_send_date_client = $recurring_invoice->nextSendDateClient(); | ||||||
|  | |||||||
| @ -30,6 +30,9 @@ class SubscriptionStatus extends AbstractService | |||||||
|     /** @var bool $is_in_good_standing */ |     /** @var bool $is_in_good_standing */ | ||||||
|     public bool $is_in_good_standing = false; |     public bool $is_in_good_standing = false; | ||||||
|      |      | ||||||
|  |     /** @var Invoice $refundable_invoice */ | ||||||
|  |     public Invoice $refundable_invoice; | ||||||
|  |      | ||||||
|     public function run(): self |     public function run(): self | ||||||
|     { |     { | ||||||
|         $this->checkTrial() |         $this->checkTrial() | ||||||
| @ -39,9 +42,42 @@ class SubscriptionStatus extends AbstractService | |||||||
|         return $this; |         return $this; | ||||||
|     } |     } | ||||||
|          |          | ||||||
|  |     /** | ||||||
|  |      * GetProRataRefund | ||||||
|  |      * | ||||||
|  |      * @return float | ||||||
|  |      */ | ||||||
|  |     public function getProRataRefund(): float | ||||||
|  |     { | ||||||
|  | 
 | ||||||
|  |         $subscription_interval_end_date = Carbon::parse($this->recurring_invoice->next_send_date_client); | ||||||
|  |         $subscription_interval_start_date = $subscription_interval_end_date->copy()->subDays($this->recurring_invoice->subscription->service()->getDaysInFrequency())->subDay(); | ||||||
|  | 
 | ||||||
|  |         $primary_invoice =  Invoice::query() | ||||||
|  |                                     ->where('company_id', $this->recurring_invoice->company_id) | ||||||
|  |                                     ->where('client_id', $this->recurring_invoice->client_id) | ||||||
|  |                                     ->where('recurring_id', $this->recurring_invoice->id) | ||||||
|  |                                     ->whereIn('status_id', [Invoice::STATUS_PAID]) | ||||||
|  |                                     ->whereBetween('date', [$subscription_interval_start_date, $subscription_interval_end_date]) | ||||||
|  |                                     ->where('is_deleted', 0) | ||||||
|  |                                     ->where('is_proforma', 0) | ||||||
|  |                                     ->orderBy('id', 'desc') | ||||||
|  |                                     ->first(); | ||||||
|  | 
 | ||||||
|  |         $this->refundable_invoice = $primary_invoice; | ||||||
|  |          | ||||||
|  |         return $primary_invoice ? max(0, round(($primary_invoice->paid_to_date * $this->getProRataRatio()),2)) : 0; | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * GetProRataRatio | ||||||
|  |      * | ||||||
|  |      * The ratio of days used / days in interval | ||||||
|  |      * @return float | ||||||
|  |      */ | ||||||
|     public function getProRataRatio():float |     public function getProRataRatio():float | ||||||
|     { |     { | ||||||
|         //calculate how much used.
 |  | ||||||
| 
 | 
 | ||||||
|         $subscription_interval_end_date = Carbon::parse($this->recurring_invoice->next_send_date_client); |         $subscription_interval_end_date = Carbon::parse($this->recurring_invoice->next_send_date_client); | ||||||
|         $subscription_interval_start_date = $subscription_interval_end_date->copy()->subDays($this->recurring_invoice->subscription->service()->getDaysInFrequency())->subDay(); |         $subscription_interval_start_date = $subscription_interval_end_date->copy()->subDays($this->recurring_invoice->subscription->service()->getDaysInFrequency())->subDay(); | ||||||
| @ -64,12 +100,14 @@ class SubscriptionStatus extends AbstractService | |||||||
| 
 | 
 | ||||||
|         $days_of_subscription_used = $subscription_start_date->copy()->diffInDays(now()); |         $days_of_subscription_used = $subscription_start_date->copy()->diffInDays(now()); | ||||||
| 
 | 
 | ||||||
|         return $days_of_subscription_used / $this->recurring_invoice->subscription->service()->getDaysInFrequency(); |         return 1 - ($days_of_subscription_used / $this->recurring_invoice->subscription->service()->getDaysInFrequency()); | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * checkInGoodStanding |      * CheckInGoodStanding | ||||||
|  |      *  | ||||||
|  |      * Are there any outstanding invoices? | ||||||
|      *  |      *  | ||||||
|      * @return self |      * @return self | ||||||
|      */ |      */ | ||||||
| @ -91,7 +129,11 @@ class SubscriptionStatus extends AbstractService | |||||||
|     } |     } | ||||||
|      |      | ||||||
|     /** |     /** | ||||||
|      * checkTrial |      * CheckTrial | ||||||
|  |      * | ||||||
|  |      * Check if this subscription is in its trial window. | ||||||
|  |      *  | ||||||
|  |      * Trials do not have an invoice yet - only a pending recurring invoice. | ||||||
|      *  |      *  | ||||||
|      * @return self |      * @return self | ||||||
|      */ |      */ | ||||||
| @ -101,14 +143,16 @@ class SubscriptionStatus extends AbstractService | |||||||
|         if(!$this->subscription->trial_enabled) |         if(!$this->subscription->trial_enabled) | ||||||
|             return $this->setIsTrial(false); |             return $this->setIsTrial(false); | ||||||
| 
 | 
 | ||||||
|         $primary_invoice = $this->recurring_invoice |         $primary_invoice = Invoice::query() | ||||||
|                             ->invoices() |                             ->where('company_id', $this->recurring_invoice->company_id) | ||||||
|  |                             ->where('client_id', $this->recurring_invoice->client_id) | ||||||
|  |                             ->where('recurring_id', $this->recurring_invoice->id) | ||||||
|                             ->where('is_deleted', 0) |                             ->where('is_deleted', 0) | ||||||
|                             ->where('is_proforma', 0) |                             ->where('is_proforma', 0) | ||||||
|                             ->orderBy('id', 'asc') |                             ->orderBy('id', 'asc') | ||||||
|                             ->first(); |                             ->doesntExist(); | ||||||
| 
 | 
 | ||||||
|         if($primary_invoice && Carbon::parse($primary_invoice->date)->addSeconds($this->subscription->trial_duration)->lte(now()->startOfDay()->addSeconds($primary_invoice->client->timezone_offset()))) { |         if($primary_invoice && Carbon::parse($this->recurring_invoice->next_send_date_client)->gte(now()->startOfDay()->addSeconds($this->recurring_invoice->client->timezone_offset()))) { | ||||||
|                 return $this->setIsTrial(true); |                 return $this->setIsTrial(true); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -21,26 +21,33 @@ class UpgradePrice extends AbstractService | |||||||
| {     | {     | ||||||
|     protected \App\Services\Subscription\SubscriptionStatus $status; |     protected \App\Services\Subscription\SubscriptionStatus $status; | ||||||
| 
 | 
 | ||||||
|  |     public float $upgrade_price = 0; | ||||||
|  | 
 | ||||||
|  |     public float $refund = 0; | ||||||
|  | 
 | ||||||
|  |     public float $outstanding_credit = 0; | ||||||
|  | 
 | ||||||
|     public function __construct(protected RecurringInvoice $recurring_invoice, public Subscription $subscription) |     public function __construct(protected RecurringInvoice $recurring_invoice, public Subscription $subscription) | ||||||
|     { |     { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function run(): float |     public function run(): self | ||||||
|     { |     { | ||||||
| 
 | 
 | ||||||
|         $this->status = $this->recurring_invoice |         $this->status = $this->recurring_invoice | ||||||
|                        ->subscription |                        ->subscription | ||||||
|                        ->status($this->recurring_invoice); |                        ->status($this->recurring_invoice); | ||||||
| 
 | 
 | ||||||
|         if($this->status->is_trial || !$this->status->is_in_good_standing) |  | ||||||
|             return $this->subscription->price;  |  | ||||||
| 
 |  | ||||||
|         if($this->status->is_in_good_standing) |         if($this->status->is_in_good_standing) | ||||||
|             return $this->calculateUpgrade(); |             $this->calculateUpgrade(); | ||||||
|  |         else | ||||||
|  |             $this->upgrade_price = $this->subscription->price; | ||||||
|  | 
 | ||||||
|  |         return $this; | ||||||
|          |          | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private function calculateUpgrade(): float |     private function calculateUpgrade(): self | ||||||
|     { |     { | ||||||
|         $ratio = $this->status->getProRataRatio(); |         $ratio = $this->status->getProRataRatio(); | ||||||
| 
 | 
 | ||||||
| @ -51,13 +58,14 @@ class UpgradePrice extends AbstractService | |||||||
|                              ->orderBy('id', 'desc') |                              ->orderBy('id', 'desc') | ||||||
|                              ->first(); |                              ->first(); | ||||||
|          |          | ||||||
|         $refund = $this->getRefundableAmount($last_invoice, $ratio); |         $this->refund = $this->getRefundableAmount($last_invoice, $ratio); | ||||||
|         $outstanding_credit = $this->getCredits(); |         $this->outstanding_credit = $this->getCredits(); | ||||||
|          |          | ||||||
|         nlog("{$this->subscription->price} - {$refund} - {$outstanding_credit}"); |         nlog("{$this->subscription->price} - {$this->refund} - {$this->outstanding_credit}"); | ||||||
| 
 | 
 | ||||||
|         return $this->subscription->price - $refund - $outstanding_credit; |         $this->upgrade_price = $this->subscription->price - $this->refund - $this->outstanding_credit; | ||||||
| 
 | 
 | ||||||
|  |         return $this; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private function getRefundableAmount(?Invoice $invoice, float $ratio): float |     private function getRefundableAmount(?Invoice $invoice, float $ratio): float | ||||||
|  | |||||||
| @ -104,7 +104,7 @@ class PaymentLinkTest extends TestCase | |||||||
| 
 | 
 | ||||||
|         $days = $recurring_invoice->subscription->service()->getDaysInFrequency(); |         $days = $recurring_invoice->subscription->service()->getDaysInFrequency(); | ||||||
| 
 | 
 | ||||||
|         $ratio = (14 / $days); |         $ratio = 1 - (14 / $days); | ||||||
| 
 | 
 | ||||||
|         $this->assertEquals($ratio, $status->getProRataRatio()); |         $this->assertEquals($ratio, $status->getProRataRatio()); | ||||||
| 
 | 
 | ||||||
| @ -114,31 +114,6 @@ class PaymentLinkTest extends TestCase | |||||||
| 
 | 
 | ||||||
|         $this->assertEquals(($target->price - $refund), $price); |         $this->assertEquals(($target->price - $refund), $price); | ||||||
| 
 | 
 | ||||||
|          |  | ||||||
|         // $this->assertEquals($target->price-$refund, $upgrade_price);
 |  | ||||||
| 
 |  | ||||||
|         // $sub_calculator = new SubscriptionCalculator($target->fresh(), $invoice->fresh());
 |  | ||||||
| 
 |  | ||||||
|         // $this->assertFalse($sub_calculator->isPaidUp());
 |  | ||||||
| 
 |  | ||||||
|         // $invoice = $invoice->service()->markPaid()->save();
 |  | ||||||
| 
 |  | ||||||
|         // $this->assertTrue($sub_calculator->isPaidUp());
 |  | ||||||
| 
 |  | ||||||
|         // $this->assertEquals(10, $invoice->amount);
 |  | ||||||
|         // $this->assertEquals(0, $invoice->balance);
 |  | ||||||
| 
 |  | ||||||
|         // $pro_rata = new ProRata();
 |  | ||||||
| 
 |  | ||||||
|         // $refund = $pro_rata->refund($invoice->amount, Carbon::parse('2021-01-01'), Carbon::parse('2021-01-06'), $subscription->frequency_id);
 |  | ||||||
| 
 |  | ||||||
|         // // $this->assertEquals(1.61, $refund);
 |  | ||||||
| 
 |  | ||||||
|         // $pro_rata = new ProRata();
 |  | ||||||
| 
 |  | ||||||
|         // $upgrade = $pro_rata->charge($target->price, Carbon::parse('2021-01-01'), Carbon::parse('2021-01-06'), $subscription->frequency_id);
 |  | ||||||
| 
 |  | ||||||
|         // $this->assertEquals(3.23, $upgrade);
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // public function testProrataDiscountRatioPercentage()
 |     // public function testProrataDiscountRatioPercentage()
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user