diff --git a/app/Models/Subscription.php b/app/Models/Subscription.php index 960817cac99c..9fd27cbfee2f 100644 --- a/app/Models/Subscription.php +++ b/app/Models/Subscription.php @@ -11,7 +11,9 @@ namespace App\Models; +use App\Services\Subscription\PaymentLinkService; use App\Services\Subscription\SubscriptionService; +use App\Services\Subscription\SubscriptionStatus; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\SoftDeletes; @@ -132,6 +134,16 @@ class Subscription extends BaseModel return new SubscriptionService($this); } + public function link_service(): PaymentLinkService + { + return new PaymentLinkService($this); + } + + public function status(RecurringInvoice $recurring_invoice): SubscriptionStatus + { + return (new SubscriptionStatus($this, $recurring_invoice))->run(); + } + public function company(): \Illuminate\Database\Eloquent\Relations\BelongsTo { return $this->belongsTo(Company::class); diff --git a/app/Services/Subscription/PaymentLinkService.php b/app/Services/Subscription/PaymentLinkService.php new file mode 100644 index 000000000000..baff45ea74dd --- /dev/null +++ b/app/Services/Subscription/PaymentLinkService.php @@ -0,0 +1,81 @@ + "Success", "status_code" => 200]; + * @param ClientContact $contact + * @return array{"message": string, "status_code": int} + */ + public function isEligible(ClientContact $contact): array + { + + } + + /* Starts the process to create a trial + - we create a recurring invoice, which has its next_send_date as now() + trial_duration + - we then hit the client API end point to advise the trial payload + - we then return the user to either a predefined user endpoint, OR we return the user to the recurring invoice page. + + * startTrial + * + * @param array $data{contact_id: int, client_id: int, bundle: \Illuminate\Support\Collection, coupon?: string, } + * @return Illuminate\Routing\Redirector + */ + public function startTrial(array $data): \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse + { + + } + + /** + * calculateUpdatePriceV2 + * + * Need to change the naming of the method + * + * @param RecurringInvoice $recurring_invoice - The Current Recurring Invoice for the subscription. + * @param Subscription $target - The new target subscription to move to + * @return float - the upgrade price + */ + public function calculateUpgradePriceV2(RecurringInvoice $recurring_invoice, Subscription $target): ?float + { + return (new UpgradePrice($recurring_invoice, $target))->run(); + } +} \ No newline at end of file diff --git a/app/Services/Subscription/ProRata.php b/app/Services/Subscription/ProRata.php index da69dd7e560c..9dfc355ba2f0 100644 --- a/app/Services/Subscription/ProRata.php +++ b/app/Services/Subscription/ProRata.php @@ -121,7 +121,7 @@ class ProRata extends AbstractService */ private function checkRefundPeriod(): self { - if(!$this->subscription->refund_period || $this->subscription->refund_period === 0) + if(!$this->recurring_invoice->subscription->refund_period || $this->recurring_invoice->subscription->refund_period === 0) return $this->setRefundable(false); $primary_invoice = $this->recurring_invoice @@ -133,7 +133,7 @@ class ProRata extends AbstractService if($primary_invoice && $primary_invoice->status_id == Invoice::STATUS_PAID && - Carbon::parse($primary_invoice->date)->addSeconds($this->subscription->refund_period)->lte(now()->startOfDay()->addSeconds($primary_invoice->client->timezone_offset())) + Carbon::parse($primary_invoice->date)->addSeconds($this->recurring_invoice->subscription->refund_period)->lte(now()->startOfDay()->addSeconds($primary_invoice->client->timezone_offset())) ){ return $this->setRefundable(true); } @@ -213,7 +213,7 @@ class ProRata extends AbstractService private function isInTrialPeriod(): self { - if(!$this->subscription->trial_enabled) + if(!$this->recurring_invoice->subscription->trial_enabled) return $this->setIsTrial(false); $primary_invoice = $this->recurring_invoice @@ -223,7 +223,7 @@ class ProRata extends AbstractService ->orderBy('id', 'asc') ->first(); - 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($primary_invoice->date)->addSeconds($this->recurring_invoice->subscription->trial_duration)->lte(now()->startOfDay()->addSeconds($primary_invoice->client->timezone_offset()))) return $this->setIsTrial(true); $this->setIsTrial(false); diff --git a/app/Services/Subscription/SubscriptionStatus.php b/app/Services/Subscription/SubscriptionStatus.php new file mode 100644 index 000000000000..e59bb51c8303 --- /dev/null +++ b/app/Services/Subscription/SubscriptionStatus.php @@ -0,0 +1,169 @@ +checkTrial() + ->checkRefundable() + ->checkInGoodStanding(); + + return $this; + } + + public function getProRataRatio():float + { + //calculate how much used. + + $primary_invoice = $this->recurring_invoice + ->invoices() + ->where('is_deleted', 0) + ->where('is_proforma', 0) + ->orderBy('id', 'desc') + ->first(); + + if(!$primary_invoice) + return 0; + + $subscription_start_date = Carbon::parse($primary_invoice->date)->startOfDay(); + $subscription_interval_end_date = Carbon::parse($this->recurring_invoice->next_send_date_client); + + $seconds_of_subscription_used = $subscription_start_date->diffInDays(now()); + $total_seconds_in_subscription_interval = $subscription_start_date->diffInDays($subscription_interval_end_date); + + return $seconds_of_subscription_used / $total_seconds_in_subscription_interval; + + } + + /** + * checkInGoodStanding + * + * @return self + */ + private function checkInGoodStanding(): self + { + + $this->is_in_good_standing = $this->recurring_invoice + ->invoices() + ->where('is_deleted', 0) + ->where('is_proform', 0) + ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PAID]) + ->where('balance', '>', 0) + ->doesntExist(); + + return $this; + + } + + /** + * checkTrial + * + * @return self + */ + private function checkTrial(): self + { + + if(!$this->subscription->trial_enabled) + $this->setIsTrial(false); + + $primary_invoice = $this->recurring_invoice + ->invoices() + ->where('is_deleted', 0) + ->where('is_proforma', 0) + ->orderBy('id', 'asc') + ->first(); + + if($primary_invoice && Carbon::parse($primary_invoice->date)->addSeconds($this->subscription->trial_duration)->lte(now()->startOfDay()->addSeconds($primary_invoice->client->timezone_offset()))) { + return $this->setIsTrial(true); + } + + $this->setIsTrial(false); + + return $this; + + } + + /** + * Determines if this subscription + * is eligible for a refund. + * + * @return self + */ + private function checkRefundable(): self + { + if(!$this->recurring_invoice->subscription->refund_period || $this->recurring_invoice->subscription->refund_period === 0) + return $this->setRefundable(false); + + $primary_invoice = $this->recurring_invoice + ->invoices() + ->where('is_deleted', 0) + ->where('is_proforma', 0) + ->orderBy('id', 'desc') + ->first(); + + if($primary_invoice && + $primary_invoice->status_id == Invoice::STATUS_PAID && + Carbon::parse($primary_invoice->date)->addSeconds($this->recurring_invoice->subscription->refund_period)->lte(now()->startOfDay()->addSeconds($primary_invoice->client->timezone_offset())) + ){ + return $this->setRefundable(true); + } + + return $this->setRefundable(false); + + } + + /** + * setRefundable + * + * @param bool $refundable + * @return self + */ + private function setRefundable(bool $refundable): self + { + $this->is_refundable = $refundable; + + return $this; + } + + /** + * Sets the is_trial flag + * + * @param bool $is_trial + * @return self + */ + private function setIsTrial(bool $is_trial): self + { + $this->is_trial = $is_trial; + + return $this; + } + +} diff --git a/app/Services/Subscription/UpgradePrice.php b/app/Services/Subscription/UpgradePrice.php new file mode 100644 index 000000000000..cca93953e3f8 --- /dev/null +++ b/app/Services/Subscription/UpgradePrice.php @@ -0,0 +1,91 @@ +status = $this->recurring_invoice + ->subscription + ->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) + return $this->calculateUpgrade(); + + } + + private function calculateUpgrade(): float + { + $ratio = $this->status->getProRataRatio(); + + $last_invoice = $this->recurring_invoice + ->invoices() + ->where('is_deleted', 0) + ->where('is_proforma', 0) + ->orderBy('id', 'desc') + ->first(); + + $refund = $this->getRefundableAmount($last_invoice, $ratio); + $outstanding_credit = $this->getCredits(); + + nlog("{$this->subscription->price} - {$refund} - {$outstanding_credit}"); + + return $this->subscription->price - $refund - $outstanding_credit; + + } + + private function getRefundableAmount(?Invoice $invoice, float $ratio): float + { + if (!$invoice || !$invoice->date || $invoice->status_id != Invoice::STATUS_PAID || $ratio == 0) + return 0; + + return max(0, round(($invoice->paid_to_date*$ratio),2)); + } + + private function getCredits(): float + { + $outstanding_credits = 0; + + $use_credit_setting = $this->recurring_invoice->client->getSetting('use_credits_payment'); + + if($use_credit_setting){ + + $outstanding_credits = Credit::query() + ->where('client_id', $this->recurring_invoice->client_id) + ->whereIn('status_id', [Credit::STATUS_SENT,Credit::STATUS_PARTIAL]) + ->where('is_deleted', 0) + ->where('balance', '>', 0) + ->sum('balance'); + + } + + return $outstanding_credits; + } + +} \ No newline at end of file