diff --git a/app/Models/Subscription.php b/app/Models/Subscription.php index 9fd27cbfee2f..132b57cfe6e3 100644 --- a/app/Models/Subscription.php +++ b/app/Models/Subscription.php @@ -123,6 +123,8 @@ class Subscription extends BaseModel 'updated_at' => 'timestamp', 'created_at' => 'timestamp', 'deleted_at' => 'timestamp', + 'trial_enabled' => 'boolean', + 'allow_plan_changes' => 'boolean', ]; protected $with = [ diff --git a/app/Services/Subscription/ProRata.php b/app/Services/Subscription/ProRata.php index 9dfc355ba2f0..6a822c4d11ed 100644 --- a/app/Services/Subscription/ProRata.php +++ b/app/Services/Subscription/ProRata.php @@ -58,29 +58,6 @@ class ProRata extends AbstractService return $this; } - /** - * Calculates the number of seconds - * of the current interval that has been used. - * - * @return self - */ - private function checkProRataDuration(): self - { - - $primary_invoice = $this->recurring_invoice - ->invoices() - ->where('is_deleted', 0) - ->where('is_proforma', 0) - ->orderBy('id', 'desc') - ->first(); - - $duration = Carbon::parse($primary_invoice->date)->startOfDay()->diffInSeconds(now()); - - $this->setProRataDuration(max(0, $duration)); - - return $this; - } - private function calculateProRataRatio(): self { if($this->pro_rata_duration < $this->subscription_interval_duration) @@ -92,9 +69,7 @@ class ProRata extends AbstractService private function calculateSubscriptionIntervalDuration(): self { - if($this->getIsTrial()) - return $this->setSubscriptionIntervalDuration(0); - + $primary_invoice = $this->recurring_invoice ->invoices() ->where('is_deleted', 0) @@ -113,52 +88,6 @@ class ProRata extends AbstractService return $this; } - /** - * Determines if this subscription - * is eligible for a refund. - * - * @return self - */ - private function checkRefundPeriod(): 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); - - } - - /** - * Gathers any unpaid invoices for this subscription. - * - * @return self - */ - private function checkUnpaidInvoices(): self - { - $this->unpaid_invoices = $this->recurring_invoice - ->invoices() - ->where('is_deleted', 0) - ->where('is_proforma', 0) - ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) - ->where('balance', '>', 0) - ->get(); - - return $this; - } private function setProRataRatio(int $ratio): self { @@ -192,117 +121,6 @@ class ProRata extends AbstractService return $this; } - /** - * setRefundable - * - * @param bool $refundable - * @return self - */ - private function setRefundable(bool $refundable): self - { - $this->refundable = $refundable; - - return $this; - } - - /** - * Determines if this users is in their trial period - * - * @return self - */ - private function isInTrialPeriod(): self - { - - if(!$this->recurring_invoice->subscription->trial_enabled) - return $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->recurring_invoice->subscription->trial_duration)->lte(now()->startOfDay()->addSeconds($primary_invoice->client->timezone_offset()))) - return $this->setIsTrial(true); - - $this->setIsTrial(false); - - 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; - } - - /** - * Getter for unpaid invoices - * - * @return \Illuminate\Database\Eloquent\Collection | null - */ - public function getUnpaidInvoices(): ?\Illuminate\Database\Eloquent\Collection - { - return $this->unpaid_invoices; - } - /** - * Gets the is_trial flag - * - * @return bool - */ - public function getIsTrial(): bool - { - return $this->is_trial; - } - - /** - * Getter for refundable flag - * - * @return bool - */ - public function getRefundable(): bool - { - return $this->refundable; - } - - /** - * The number of seconds used in the current duration - * - * @return int - */ - public function getProRataDuration(): int - { - return $this->pro_rata_duration; - } - - /** - * The total number of seconds in this subscription interval - * - * @return int - */ - public function getSubscriptionIntervalDuration(): int - { - return $this->subscription_interval_duration; - } - - - /** - * Returns the pro rata ratio to be applied to any credit. - * - * @return int - */ - public function getProRataRatio(): int - { - return $this->pro_rata_ratio; - } } \ No newline at end of file diff --git a/app/Services/Subscription/SubscriptionService.php b/app/Services/Subscription/SubscriptionService.php index d9631b766ce3..0daf9adac60f 100644 --- a/app/Services/Subscription/SubscriptionService.php +++ b/app/Services/Subscription/SubscriptionService.php @@ -1352,7 +1352,7 @@ class SubscriptionService * * @return int Number of days */ - private function getDaysInFrequency(): int + public function getDaysInFrequency(): int { switch ($this->subscription->frequency_id) { case RecurringInvoice::FREQUENCY_DAILY: diff --git a/app/Services/Subscription/SubscriptionStatus.php b/app/Services/Subscription/SubscriptionStatus.php index e59bb51c8303..3d842675ab31 100644 --- a/app/Services/Subscription/SubscriptionStatus.php +++ b/app/Services/Subscription/SubscriptionStatus.php @@ -43,8 +43,15 @@ class SubscriptionStatus extends AbstractService { //calculate how much used. - $primary_invoice = $this->recurring_invoice - ->invoices() + $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') @@ -54,12 +61,10 @@ class SubscriptionStatus extends AbstractService 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); + $days_of_subscription_used = $subscription_start_date->copy()->diffInDays(now()); - return $seconds_of_subscription_used / $total_seconds_in_subscription_interval; + return $days_of_subscription_used / $this->recurring_invoice->subscription->service()->getDaysInFrequency(); } @@ -71,11 +76,13 @@ class SubscriptionStatus extends AbstractService private function checkInGoodStanding(): self { - $this->is_in_good_standing = $this->recurring_invoice - ->invoices() + $this->is_in_good_standing = 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) ->where('is_deleted', 0) - ->where('is_proform', 0) - ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PAID]) + ->where('is_proforma', 0) + ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) ->where('balance', '>', 0) ->doesntExist(); @@ -92,7 +99,7 @@ class SubscriptionStatus extends AbstractService { if(!$this->subscription->trial_enabled) - $this->setIsTrial(false); + return $this->setIsTrial(false); $primary_invoice = $this->recurring_invoice ->invoices() diff --git a/tests/Feature/PaymentLink/PaymentLinkTest.php b/tests/Feature/PaymentLink/PaymentLinkTest.php new file mode 100644 index 000000000000..185f45099b10 --- /dev/null +++ b/tests/Feature/PaymentLink/PaymentLinkTest.php @@ -0,0 +1,253 @@ +makeTestData(); + } + + public function testCalcUpgradePrice() + { + $subscription = Subscription::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'price' => 10, + ]); + + $target = Subscription::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'price' => 20, + ]); + + $recurring_invoice = RecurringInvoice::factory()->create([ + 'line_items' => $this->buildLineItems(), + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'client_id' => $this->client->id, + 'tax_rate1' => 0, + 'tax_name1' => '', + 'tax_rate2' => 0, + 'tax_name2' => '', + 'tax_rate3' => 0, + 'tax_name3' => '', + 'discount' => 0, + 'subscription_id' => $subscription->id, + 'date' => now()->subWeeks(2), + 'next_send_date_client' => now(), + ]); + + + $invoice = Invoice::factory()->create([ + 'line_items' => $this->buildLineItems(), + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'client_id' => $this->client->id, + 'tax_rate1' => 0, + 'tax_name1' => '', + 'tax_rate2' => 0, + 'tax_name2' => '', + 'tax_rate3' => 0, + 'tax_name3' => '', + 'discount' => 0, + 'subscription_id' => $subscription->id, + 'date' => now()->subWeeks(2), + 'recurring_id' => $recurring_invoice->id, + ]); + + $recurring_invoice = $recurring_invoice->calc()->getInvoice(); + $invoice = $invoice->calc()->getInvoice(); + $this->assertEquals(10, $invoice->amount); + $invoice->service()->markSent()->save(); + $this->assertEquals(10, $invoice->amount); + $this->assertEquals(10, $invoice->balance); + $invoice = $invoice->service()->markPaid()->save(); + $this->assertEquals(0, $invoice->balance); + $this->assertEquals(10, $invoice->paid_to_date); + + $status = $recurring_invoice + ->subscription + ->status($recurring_invoice); + + $this->assertFalse($status->is_trial); + $this->assertFalse($status->is_refundable); + $this->assertTrue($status->is_in_good_standing); + + $days = $recurring_invoice->subscription->service()->getDaysInFrequency(); + + $ratio = (14 / $days); + + $this->assertEquals($ratio, $status->getProRataRatio()); + + $price = $target->link_service()->calculateUpgradePriceV2($recurring_invoice, $target); + + $refund = round($invoice->paid_to_date*$ratio,2); + + $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() + // { + // $subscription = Subscription::factory()->create([ + // 'company_id' => $this->company->id, + // 'user_id' => $this->user->id, + // 'price' => 100, + // ]); + + // $item = InvoiceItemFactory::create(); + // $item->quantity = 1; + + // $item->cost = 100; + // $item->product_key = 'xyz'; + // $item->notes = 'test'; + // $item->custom_value1 = 'x'; + // $item->custom_value2 = 'x'; + // $item->custom_value3 = 'x'; + // $item->custom_value4 = 'x'; + + // $line_items[] = $item; + + // $invoice = Invoice::factory()->create([ + // 'line_items' => $line_items, + // 'company_id' => $this->company->id, + // 'user_id' => $this->user->id, + // 'client_id' => $this->client->id, + // 'tax_rate1' => 0, + // 'tax_name1' => '', + // 'tax_rate2' => 0, + // 'tax_name2' => '', + // 'tax_rate3' => 0, + // 'tax_name3' => '', + // 'discount' => 0, + // 'subscription_id' => $subscription->id, + // 'date' => '2021-01-01', + // 'discount' => 10, + // 'is_amount_discount' => false, + // 'status_id' => 1, + // ]); + + // $invoice = $invoice->calc()->getInvoice(); + // $this->assertEquals(90, $invoice->amount); + // $this->assertEquals(0, $invoice->balance); + + // $invoice->service()->markSent()->save(); + + // $this->assertEquals(90, $invoice->amount); + // $this->assertEquals(90, $invoice->balance); + + + // $ratio = $subscription->service()->calculateDiscountRatio($invoice); + + // $this->assertEquals(.1, $ratio); + // } + + // public function testProrataDiscountRatioAmount() + // { + // $subscription = Subscription::factory()->create([ + // 'company_id' => $this->company->id, + // 'user_id' => $this->user->id, + // 'price' => 100, + // ]); + + // $item = InvoiceItemFactory::create(); + // $item->quantity = 1; + + // $item->cost = 100; + // $item->product_key = 'xyz'; + // $item->notes = 'test'; + // $item->custom_value1 = 'x'; + // $item->custom_value2 = 'x'; + // $item->custom_value3 = 'x'; + // $item->custom_value4 = 'x'; + + // $line_items[] = $item; + + // $invoice = Invoice::factory()->create([ + // 'line_items' => $line_items, + // 'company_id' => $this->company->id, + // 'user_id' => $this->user->id, + // 'client_id' => $this->client->id, + // 'tax_rate1' => 0, + // 'tax_name1' => '', + // 'tax_rate2' => 0, + // 'tax_name2' => '', + // 'tax_rate3' => 0, + // 'tax_name3' => '', + // 'discount' => 0, + // 'subscription_id' => $subscription->id, + // 'date' => '2021-01-01', + // 'discount' => 20, + // 'is_amount_discount' => true, + // 'status_id' => 1, + // ]); + + // $invoice = $invoice->calc()->getInvoice(); + // $this->assertEquals(80, $invoice->amount); + // $this->assertEquals(0, $invoice->balance); + + // $invoice->service()->markSent()->save(); + + // $this->assertEquals(80, $invoice->amount); + // $this->assertEquals(80, $invoice->balance); + + + // $ratio = $subscription->service()->calculateDiscountRatio($invoice); + + // $this->assertEquals(.2, $ratio); + // } +}