From 17507fd2581bdb165e7a369bfac364906f7a4301 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 21 Oct 2021 11:26:19 +1100 Subject: [PATCH 1/9] Tests for pro rata refunds --- .../Subscription/SubscriptionService.php | 53 +++++++++++ tests/Unit/RefundUnitTest.php | 95 +++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 tests/Unit/RefundUnitTest.php diff --git a/app/Services/Subscription/SubscriptionService.php b/app/Services/Subscription/SubscriptionService.php index 8771b9034c8c..e303b3038ab3 100644 --- a/app/Services/Subscription/SubscriptionService.php +++ b/app/Services/Subscription/SubscriptionService.php @@ -299,6 +299,24 @@ class SubscriptionService } + /** + * Calculates the pro rata refund between two dates + * using a daily calculation. + * + * @param float $amount + * @param Carbon $from_date + * @param Carbon $to_date + * @param int $frequency The billing interval + * @return float + */ + private function proRataRefund(float $amount, Carbon $from_date, Carbon $to_date, int $frequency) :float + { + $days = $from_date->diffInDays($to_date); + $days_in_frequency = $this->getDaysInFrequencySpecific($frequency); + + return round( ($days/$days_in_frequency) * $amount); + } + /** * Returns refundable set of line items * transformed for direct injection into @@ -934,6 +952,41 @@ class SubscriptionService } + private function getDaysInFrequencySpecific($frequency) + { + + switch ($frequency) { + case RecurringInvoice::FREQUENCY_DAILY: + return 1; + case RecurringInvoice::FREQUENCY_WEEKLY: + return 7; + case RecurringInvoice::FREQUENCY_TWO_WEEKS: + return 14; + case RecurringInvoice::FREQUENCY_FOUR_WEEKS: + return now()->diffInDays(now()->addWeeks(4)); + case RecurringInvoice::FREQUENCY_MONTHLY: + return now()->diffInDays(now()->addMonthNoOverflow()); + case RecurringInvoice::FREQUENCY_TWO_MONTHS: + return now()->diffInDays(now()->addMonthNoOverflow(2)); + case RecurringInvoice::FREQUENCY_THREE_MONTHS: + return now()->diffInDays(now()->addMonthNoOverflow(3)); + case RecurringInvoice::FREQUENCY_FOUR_MONTHS: + return now()->diffInDays(now()->addMonthNoOverflow(4)); + case RecurringInvoice::FREQUENCY_SIX_MONTHS: + return now()->diffInDays(now()->addMonthNoOverflow(6)); + case RecurringInvoice::FREQUENCY_ANNUALLY: + return now()->diffInDays(now()->addYear()); + case RecurringInvoice::FREQUENCY_TWO_YEARS: + return now()->diffInDays(now()->addYears(2)); + case RecurringInvoice::FREQUENCY_THREE_YEARS: + return now()->diffInDays(now()->addYears(3)); + default: + return 0; + } + + } + + /** * 'email' => $this->email ?? $this->contact->email, diff --git a/tests/Unit/RefundUnitTest.php b/tests/Unit/RefundUnitTest.php new file mode 100644 index 000000000000..5679a534f8bc --- /dev/null +++ b/tests/Unit/RefundUnitTest.php @@ -0,0 +1,95 @@ +proRataRefund(10, Carbon::parse('2021-01-01'), Carbon::parse('2021-01-31'), RecurringInvoice::FREQUENCY_MONTHLY); + + $this->assertEquals(9.68, $refund); + + $this->assertEquals(30, Carbon::parse('2021-01-01')->diffInDays(Carbon::parse('2021-01-31'))); + + } + + public function testProRataRefundYearly() + { + + $refund = $this->proRataRefund(10, Carbon::parse('2021-01-01'), Carbon::parse('2021-01-31'), RecurringInvoice::FREQUENCY_ANNUALLY); + + $this->assertEquals(0.82, $refund); + } + + public function testDiffInDays() + { + + $this->assertEquals(30, Carbon::parse('2021-01-01')->diffInDays(Carbon::parse('2021-01-31'))); + + } + + private function proRataRefund(float $amount, Carbon $from_date, Carbon $to_date, int $frequency) :float + { + $days = $from_date->diffInDays($to_date); + $days_in_frequency = $this->getDaysInFrequencySpecific($frequency); + + return round( (($days/$days_in_frequency) * $amount),2); + } + + private function getDaysInFrequencySpecific($frequency) + { + + switch ($frequency) { + case RecurringInvoice::FREQUENCY_DAILY: + return 1; + case RecurringInvoice::FREQUENCY_WEEKLY: + return 7; + case RecurringInvoice::FREQUENCY_TWO_WEEKS: + return 14; + case RecurringInvoice::FREQUENCY_FOUR_WEEKS: + return now()->diffInDays(now()->addWeeks(4)); + case RecurringInvoice::FREQUENCY_MONTHLY: + return now()->diffInDays(now()->addMonthNoOverflow()); + case RecurringInvoice::FREQUENCY_TWO_MONTHS: + return now()->diffInDays(now()->addMonthNoOverflow(2)); + case RecurringInvoice::FREQUENCY_THREE_MONTHS: + return now()->diffInDays(now()->addMonthNoOverflow(3)); + case RecurringInvoice::FREQUENCY_FOUR_MONTHS: + return now()->diffInDays(now()->addMonthNoOverflow(4)); + case RecurringInvoice::FREQUENCY_SIX_MONTHS: + return now()->diffInDays(now()->addMonthNoOverflow(6)); + case RecurringInvoice::FREQUENCY_ANNUALLY: + return now()->diffInDays(now()->addYear()); + case RecurringInvoice::FREQUENCY_TWO_YEARS: + return now()->diffInDays(now()->addYears(2)); + case RecurringInvoice::FREQUENCY_THREE_YEARS: + return now()->diffInDays(now()->addYears(3)); + default: + return 0; + } + } + +} \ No newline at end of file From bcf34a6e62d5a2bf7e2ebc2aac763fa5d86a26d2 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 21 Oct 2021 12:40:59 +1100 Subject: [PATCH 2/9] Refactor Refunds --- app/Helpers/Invoice/Refund.php | 62 +++++++++++++++++++ .../Subscription/SubscriptionService.php | 54 ---------------- tests/Unit/RefundUnitTest.php | 48 ++------------ 3 files changed, 67 insertions(+), 97 deletions(-) create mode 100644 app/Helpers/Invoice/Refund.php diff --git a/app/Helpers/Invoice/Refund.php b/app/Helpers/Invoice/Refund.php new file mode 100644 index 000000000000..e4e12dd0b9c8 --- /dev/null +++ b/app/Helpers/Invoice/Refund.php @@ -0,0 +1,62 @@ +diffInDays($to_date); + $days_in_frequency = $this->getDaysInFrequency($frequency); + + return round( (($days/$days_in_frequency) * $amount),2); + } + + private function getDaysInFrequency($frequency) + { + + switch ($frequency) { + case RecurringInvoice::FREQUENCY_DAILY: + return 1; + case RecurringInvoice::FREQUENCY_WEEKLY: + return 7; + case RecurringInvoice::FREQUENCY_TWO_WEEKS: + return 14; + case RecurringInvoice::FREQUENCY_FOUR_WEEKS: + return now()->diffInDays(now()->addWeeks(4)); + case RecurringInvoice::FREQUENCY_MONTHLY: + return now()->diffInDays(now()->addMonthNoOverflow()); + case RecurringInvoice::FREQUENCY_TWO_MONTHS: + return now()->diffInDays(now()->addMonthNoOverflow(2)); + case RecurringInvoice::FREQUENCY_THREE_MONTHS: + return now()->diffInDays(now()->addMonthNoOverflow(3)); + case RecurringInvoice::FREQUENCY_FOUR_MONTHS: + return now()->diffInDays(now()->addMonthNoOverflow(4)); + case RecurringInvoice::FREQUENCY_SIX_MONTHS: + return now()->diffInDays(now()->addMonthNoOverflow(6)); + case RecurringInvoice::FREQUENCY_ANNUALLY: + return now()->diffInDays(now()->addYear()); + case RecurringInvoice::FREQUENCY_TWO_YEARS: + return now()->diffInDays(now()->addYears(2)); + case RecurringInvoice::FREQUENCY_THREE_YEARS: + return now()->diffInDays(now()->addYears(3)); + default: + return 0; + } + + } + +} \ No newline at end of file diff --git a/app/Services/Subscription/SubscriptionService.php b/app/Services/Subscription/SubscriptionService.php index e303b3038ab3..da820b59c75b 100644 --- a/app/Services/Subscription/SubscriptionService.php +++ b/app/Services/Subscription/SubscriptionService.php @@ -299,24 +299,6 @@ class SubscriptionService } - /** - * Calculates the pro rata refund between two dates - * using a daily calculation. - * - * @param float $amount - * @param Carbon $from_date - * @param Carbon $to_date - * @param int $frequency The billing interval - * @return float - */ - private function proRataRefund(float $amount, Carbon $from_date, Carbon $to_date, int $frequency) :float - { - $days = $from_date->diffInDays($to_date); - $days_in_frequency = $this->getDaysInFrequencySpecific($frequency); - - return round( ($days/$days_in_frequency) * $amount); - } - /** * Returns refundable set of line items * transformed for direct injection into @@ -952,42 +934,6 @@ class SubscriptionService } - private function getDaysInFrequencySpecific($frequency) - { - - switch ($frequency) { - case RecurringInvoice::FREQUENCY_DAILY: - return 1; - case RecurringInvoice::FREQUENCY_WEEKLY: - return 7; - case RecurringInvoice::FREQUENCY_TWO_WEEKS: - return 14; - case RecurringInvoice::FREQUENCY_FOUR_WEEKS: - return now()->diffInDays(now()->addWeeks(4)); - case RecurringInvoice::FREQUENCY_MONTHLY: - return now()->diffInDays(now()->addMonthNoOverflow()); - case RecurringInvoice::FREQUENCY_TWO_MONTHS: - return now()->diffInDays(now()->addMonthNoOverflow(2)); - case RecurringInvoice::FREQUENCY_THREE_MONTHS: - return now()->diffInDays(now()->addMonthNoOverflow(3)); - case RecurringInvoice::FREQUENCY_FOUR_MONTHS: - return now()->diffInDays(now()->addMonthNoOverflow(4)); - case RecurringInvoice::FREQUENCY_SIX_MONTHS: - return now()->diffInDays(now()->addMonthNoOverflow(6)); - case RecurringInvoice::FREQUENCY_ANNUALLY: - return now()->diffInDays(now()->addYear()); - case RecurringInvoice::FREQUENCY_TWO_YEARS: - return now()->diffInDays(now()->addYears(2)); - case RecurringInvoice::FREQUENCY_THREE_YEARS: - return now()->diffInDays(now()->addYears(3)); - default: - return 0; - } - - } - - - /** * 'email' => $this->email ?? $this->contact->email, * 'quantity' => $this->quantity, diff --git a/tests/Unit/RefundUnitTest.php b/tests/Unit/RefundUnitTest.php index 5679a534f8bc..bfecc4b616f5 100644 --- a/tests/Unit/RefundUnitTest.php +++ b/tests/Unit/RefundUnitTest.php @@ -10,6 +10,7 @@ */ namespace Tests\Unit; +use App\Helpers\Invoice\Refund; use App\Models\RecurringInvoice; use App\Utils\Ninja; use Illuminate\Support\Carbon; @@ -28,7 +29,8 @@ class RefundUnitTest extends TestCase public function testProRataRefundMonthly() { - $refund = $this->proRataRefund(10, Carbon::parse('2021-01-01'), Carbon::parse('2021-01-31'), RecurringInvoice::FREQUENCY_MONTHLY); + $r = new Refund(); + $refund = $r->proRata(10, Carbon::parse('2021-01-01'), Carbon::parse('2021-01-31'), RecurringInvoice::FREQUENCY_MONTHLY); $this->assertEquals(9.68, $refund); @@ -38,8 +40,9 @@ class RefundUnitTest extends TestCase public function testProRataRefundYearly() { + $r = new Refund(); - $refund = $this->proRataRefund(10, Carbon::parse('2021-01-01'), Carbon::parse('2021-01-31'), RecurringInvoice::FREQUENCY_ANNUALLY); + $refund = $r->proRata(10, Carbon::parse('2021-01-01'), Carbon::parse('2021-01-31'), RecurringInvoice::FREQUENCY_ANNUALLY); $this->assertEquals(0.82, $refund); } @@ -51,45 +54,4 @@ class RefundUnitTest extends TestCase } - private function proRataRefund(float $amount, Carbon $from_date, Carbon $to_date, int $frequency) :float - { - $days = $from_date->diffInDays($to_date); - $days_in_frequency = $this->getDaysInFrequencySpecific($frequency); - - return round( (($days/$days_in_frequency) * $amount),2); - } - - private function getDaysInFrequencySpecific($frequency) - { - - switch ($frequency) { - case RecurringInvoice::FREQUENCY_DAILY: - return 1; - case RecurringInvoice::FREQUENCY_WEEKLY: - return 7; - case RecurringInvoice::FREQUENCY_TWO_WEEKS: - return 14; - case RecurringInvoice::FREQUENCY_FOUR_WEEKS: - return now()->diffInDays(now()->addWeeks(4)); - case RecurringInvoice::FREQUENCY_MONTHLY: - return now()->diffInDays(now()->addMonthNoOverflow()); - case RecurringInvoice::FREQUENCY_TWO_MONTHS: - return now()->diffInDays(now()->addMonthNoOverflow(2)); - case RecurringInvoice::FREQUENCY_THREE_MONTHS: - return now()->diffInDays(now()->addMonthNoOverflow(3)); - case RecurringInvoice::FREQUENCY_FOUR_MONTHS: - return now()->diffInDays(now()->addMonthNoOverflow(4)); - case RecurringInvoice::FREQUENCY_SIX_MONTHS: - return now()->diffInDays(now()->addMonthNoOverflow(6)); - case RecurringInvoice::FREQUENCY_ANNUALLY: - return now()->diffInDays(now()->addYear()); - case RecurringInvoice::FREQUENCY_TWO_YEARS: - return now()->diffInDays(now()->addYears(2)); - case RecurringInvoice::FREQUENCY_THREE_YEARS: - return now()->diffInDays(now()->addYears(3)); - default: - return 0; - } - } - } \ No newline at end of file From dfa773d6b966585c3c3d2befe18669d7b3357711 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 21 Oct 2021 13:03:41 +1100 Subject: [PATCH 3/9] ProRata Refunds --- .../Invoice/{Refund.php => ProRata.php} | 62 ++++++++++++++++++- .../Subscription/SubscriptionCalculator.php | 48 ++++++++++++++ tests/Unit/RefundUnitTest.php | 10 +-- 3 files changed, 113 insertions(+), 7 deletions(-) rename app/Helpers/Invoice/{Refund.php => ProRata.php} (55%) create mode 100644 app/Helpers/Subscription/SubscriptionCalculator.php diff --git a/app/Helpers/Invoice/Refund.php b/app/Helpers/Invoice/ProRata.php similarity index 55% rename from app/Helpers/Invoice/Refund.php rename to app/Helpers/Invoice/ProRata.php index e4e12dd0b9c8..4026d41d303b 100644 --- a/app/Helpers/Invoice/Refund.php +++ b/app/Helpers/Invoice/ProRata.php @@ -11,13 +11,24 @@ namespace App\Helpers\Invoice; +use App\Models\Invoice; use App\Models\RecurringInvoice; +use Carbon\Exceptions\InvalidFormatException; +use Exception; +use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Support\Carbon; -class Refund +class ProRata { - public function proRata(float $amount, Carbon $from_date, Carbon $to_date, int $frequency) :float + /** + * @param float $amount + * @param Carbon $from_date + * @param Carbon $to_date + * @param int $frequency + * @return float + */ + public function refund(float $amount, Carbon $from_date, Carbon $to_date, int $frequency) :float { $days = $from_date->diffInDays($to_date); $days_in_frequency = $this->getDaysInFrequency($frequency); @@ -25,6 +36,53 @@ class Refund return round( (($days/$days_in_frequency) * $amount),2); } + + /** + * Prepares the line items of an invoice + * to be pro rata refunded. + * + * @param Invoice $invoice + * @param bool $is_credit + * @return array + * @throws Exception + */ + public function refundItems(Invoice $invoice, $is_credit = false) :array + { + if(!$invoice) + return []; + + $recurring_invoice = RecurringInvoice::find($invoice->recurring_id)->first(); + + if(!$recurring_invoice) + throw new \Exception("Invoice isn't attached to a recurring invoice"); + + /* depending on whether we are creating an invoice or a credit*/ + $multiplier = $is_credit ? 1 : -1; + + $start_date = Carbon::parse($invoice->date); + + $line_items = []; + + foreach($invoice->line_items as $item) + { + + if($item->product_key != ctrans('texts.refund')) + { + $item->quantity = 1; + $item->cost = $this->refund($item->cost*$multiplier, $start_date, now(), $recurring_invoice->frequency_id); + $item->product_key = ctrans('texts.refund'); + $item->notes = ctrans('texts.refund') . ": ". $item->notes; + + $line_items[] = $item; + + } + } + + return $line_items; + + } + + private function getDaysInFrequency($frequency) { diff --git a/app/Helpers/Subscription/SubscriptionCalculator.php b/app/Helpers/Subscription/SubscriptionCalculator.php new file mode 100644 index 000000000000..597f727ca612 --- /dev/null +++ b/app/Helpers/Subscription/SubscriptionCalculator.php @@ -0,0 +1,48 @@ +subscription = $subscription; + } + + /** + * Tests if the user is currently up + * to date with their payments for + * a given recurring invoice + * + * @return bool + */ + public function isPaidUp(RecurringInvoice $recurring_invoice) :bool + { + + $outstanding_invoices_exist = Invoice::whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) + ->where('recurring_id', $recurring_invoice->id) + ->where('balance', '>', 0) + ->exists(); + + return ! $outstanding_invoices_exist; + + } +} \ No newline at end of file diff --git a/tests/Unit/RefundUnitTest.php b/tests/Unit/RefundUnitTest.php index bfecc4b616f5..fe930e0a0c13 100644 --- a/tests/Unit/RefundUnitTest.php +++ b/tests/Unit/RefundUnitTest.php @@ -10,7 +10,7 @@ */ namespace Tests\Unit; -use App\Helpers\Invoice\Refund; +use App\Helpers\Invoice\ProRata; use App\Models\RecurringInvoice; use App\Utils\Ninja; use Illuminate\Support\Carbon; @@ -29,8 +29,8 @@ class RefundUnitTest extends TestCase public function testProRataRefundMonthly() { - $r = new Refund(); - $refund = $r->proRata(10, Carbon::parse('2021-01-01'), Carbon::parse('2021-01-31'), RecurringInvoice::FREQUENCY_MONTHLY); + $pro_rata = new ProRata(); + $refund = $pro_rata->refund(10, Carbon::parse('2021-01-01'), Carbon::parse('2021-01-31'), RecurringInvoice::FREQUENCY_MONTHLY); $this->assertEquals(9.68, $refund); @@ -40,9 +40,9 @@ class RefundUnitTest extends TestCase public function testProRataRefundYearly() { - $r = new Refund(); + $pro_rata = new ProRata(); - $refund = $r->proRata(10, Carbon::parse('2021-01-01'), Carbon::parse('2021-01-31'), RecurringInvoice::FREQUENCY_ANNUALLY); + $refund = $pro_rata->refund(10, Carbon::parse('2021-01-01'), Carbon::parse('2021-01-31'), RecurringInvoice::FREQUENCY_ANNUALLY); $this->assertEquals(0.82, $refund); } From 3de5665d944622bc5cf7abdf9a295627c968abf1 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 21 Oct 2021 15:08:46 +1100 Subject: [PATCH 4/9] Subscription calculations test' --- app/Helpers/Invoice/ProRata.php | 21 ++++++ .../Subscription/SubscriptionCalculator.php | 67 +++++++++++++++-- .../Subscription/SubscriptionService.php | 33 ++++++++ database/factories/SubscriptionFactory.php | 3 +- tests/Unit/SubscriptionsCalcTest.php | 75 +++++++++++++++++++ 5 files changed, 191 insertions(+), 8 deletions(-) create mode 100644 tests/Unit/SubscriptionsCalcTest.php diff --git a/app/Helpers/Invoice/ProRata.php b/app/Helpers/Invoice/ProRata.php index 4026d41d303b..c27394662c2c 100644 --- a/app/Helpers/Invoice/ProRata.php +++ b/app/Helpers/Invoice/ProRata.php @@ -21,7 +21,11 @@ use Illuminate\Support\Carbon; class ProRata { + /** + * Returns the amount to refund based on + * the time interval and the frequency duration + * * @param float $amount * @param Carbon $from_date * @param Carbon $to_date @@ -36,6 +40,23 @@ class ProRata return round( (($days/$days_in_frequency) * $amount),2); } + /** + * Returns the amount to charge based on + * the time interval and the frequency duration + * + * @param float $amount + * @param Carbon $from_date + * @param Carbon $to_date + * @param int $frequency + * @return float + */ + public function charge(float $amount, Carbon $from_date, Carbon $to_date, int $frequency) :float + { + $days = $from_date->diffInDays($to_date); + $days_in_frequency = $this->getDaysInFrequency($frequency); + + return round( (($days/$days_in_frequency) * $amount),2); + } /** * Prepares the line items of an invoice diff --git a/app/Helpers/Subscription/SubscriptionCalculator.php b/app/Helpers/Subscription/SubscriptionCalculator.php index 597f727ca612..3956ef4617ff 100644 --- a/app/Helpers/Subscription/SubscriptionCalculator.php +++ b/app/Helpers/Subscription/SubscriptionCalculator.php @@ -11,6 +11,7 @@ namespace App\Helpers\Subscription; +use App\Helpers\Invoice\ProRata; use App\Models\Invoice; use App\Models\RecurringInvoice; use App\Models\Subscription; @@ -20,11 +21,15 @@ use App\Models\Subscription; */ class SubscriptionCalculator { - public Subscription $subscription; - public function __construct(Subscription $subscription) + public Subscription $target_subscription; + + public Invoice $invoice + + public function __construct(Subscription $target_subscription, Invoice $invoice) { - $this->subscription = $subscription; + $this->target_subscription = $target_subscription; + $this->invoice = $invoice; } /** @@ -34,15 +39,63 @@ class SubscriptionCalculator * * @return bool */ - public function isPaidUp(RecurringInvoice $recurring_invoice) :bool + public function isPaidUp() :bool { $outstanding_invoices_exist = Invoice::whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) - ->where('recurring_id', $recurring_invoice->id) - ->where('balance', '>', 0) - ->exists(); + ->where('subscription_id', $this->invoice->subscription_id) + ->where('client_id', $this->invoice->client_id) + ->where('balance', '>', 0) + ->exists(); return ! $outstanding_invoices_exist; } + + public function calcUpgradePlan() + { + //set the starting refund amount + $refund_amount = 0; + + //are they paid up to date. + + //yes - calculate refund + if($this->isPaidUp()) + $refund_invoice = $this->getRefundInvoice(); + + if($refund_invoice) + { + $subscription = Subscription::find($this->invoice->subscription_id); + $pro_rata = new ProRata(); + + $to_date = $subscription->getNextDateForFrequency(Carbon::parse($refund_invoice->date), $subscription->frequency_id); + + $refund_amount = $pro_rata->refund($refund_invoice->amount, now(), $to_date, $subscription->frequency_id); + + $charge_amount = $pro_rata->charge($this->target_subscription->price, now(), $to_date, $this->target_subscription->frequency_id); + + return ($charge_amount - $refund_amount); + } + + //no - return full freight charge. + return $this->target_subscription->price; + } + + public function executeUpgradePlan() + { + + } + + private function getRefundInvoice() + { + + return Invoice::where('subscription_id', $this->invoice->subscription_id) + ->where('client_id', $this->invoice->client_id) + ->where('is_deleted', 0) + ->where('balance', '>', 0) + ->orderBy('id', 'desc') + ->first(); + + } + } \ No newline at end of file diff --git a/app/Services/Subscription/SubscriptionService.php b/app/Services/Subscription/SubscriptionService.php index da820b59c75b..0d6cd9f553a0 100644 --- a/app/Services/Subscription/SubscriptionService.php +++ b/app/Services/Subscription/SubscriptionService.php @@ -934,6 +934,39 @@ class SubscriptionService } + public function getNextDateForFrequency($date, $frequency) + { + switch ($frequency) { + case RecurringInvoice::FREQUENCY_DAILY: + return $date->addDay(); + case RecurringInvoice::FREQUENCY_WEEKLY: + return $date->addDays(7); + case RecurringInvoice::FREQUENCY_TWO_WEEKS: + return $date->addDays(13); + case RecurringInvoice::FREQUENCY_FOUR_WEEKS: + return $date->addWeeks(4); + case RecurringInvoice::FREQUENCY_MONTHLY: + return $date->addMonthNoOverflow(); + case RecurringInvoice::FREQUENCY_TWO_MONTHS: + return $date->addMonthNoOverflow(2); + case RecurringInvoice::FREQUENCY_THREE_MONTHS: + return $date->addMonthNoOverflow(3); + case RecurringInvoice::FREQUENCY_FOUR_MONTHS: + return $date->addMonthNoOverflow(4); + case RecurringInvoice::FREQUENCY_SIX_MONTHS: + return $date->addMonthNoOverflow(6); + case RecurringInvoice::FREQUENCY_ANNUALLY: + return $date->addYear(); + case RecurringInvoice::FREQUENCY_TWO_YEARS: + return $date->addYears(2); + case RecurringInvoice::FREQUENCY_THREE_YEARS: + return $date->addYears(3); + default: + return 0; + } + } + + /** * 'email' => $this->email ?? $this->contact->email, * 'quantity' => $this->quantity, diff --git a/database/factories/SubscriptionFactory.php b/database/factories/SubscriptionFactory.php index a9b008244c7f..63bff1c98914 100644 --- a/database/factories/SubscriptionFactory.php +++ b/database/factories/SubscriptionFactory.php @@ -12,6 +12,7 @@ namespace Database\Factories; +use App\Models\RecurringInvoice; use App\Models\Subscription; use Illuminate\Database\Eloquent\Factories\Factory; @@ -32,7 +33,7 @@ class SubscriptionFactory extends Factory public function definition() { return [ - + 'frequency_id' => RecurringInvoice::FREQUENCY_MONTHLY ]; } } diff --git a/tests/Unit/SubscriptionsCalcTest.php b/tests/Unit/SubscriptionsCalcTest.php new file mode 100644 index 000000000000..7100f25b8533 --- /dev/null +++ b/tests/Unit/SubscriptionsCalcTest.php @@ -0,0 +1,75 @@ +makeTestData(); + } + + public function testCalcUpgradePrice() + { + + $subscription = Subscription::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'price' => 10, + + ]); + + $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() + ]); + + $invoice = $invoice->calc()->getInvoice(); + + $this->assertEquals(10, $invoice->amount); + + $invoice->service()->markSent()->save(); + + $this->assertEquals(10, $invoice->amount); + $this->assertEquals(10, $invoice->balance); + + } + + +} \ No newline at end of file From 9f79b4b519eff928d53a295ca4bc66bd8fad83f9 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 21 Oct 2021 15:51:36 +1100 Subject: [PATCH 5/9] Working on tests for subscriptions --- app/Helpers/Invoice/ProRata.php | 9 +++-- .../Subscription/SubscriptionCalculator.php | 9 +++-- database/factories/SubscriptionFactory.php | 3 +- tests/Unit/SubscriptionsCalcTest.php | 38 ++++++++++++++++++- 4 files changed, 50 insertions(+), 9 deletions(-) diff --git a/app/Helpers/Invoice/ProRata.php b/app/Helpers/Invoice/ProRata.php index c27394662c2c..e3291b25c218 100644 --- a/app/Helpers/Invoice/ProRata.php +++ b/app/Helpers/Invoice/ProRata.php @@ -34,7 +34,7 @@ class ProRata */ public function refund(float $amount, Carbon $from_date, Carbon $to_date, int $frequency) :float { - $days = $from_date->diffInDays($to_date); + $days = $from_date->copy()->diffInDays($to_date); $days_in_frequency = $this->getDaysInFrequency($frequency); return round( (($days/$days_in_frequency) * $amount),2); @@ -52,9 +52,12 @@ class ProRata */ public function charge(float $amount, Carbon $from_date, Carbon $to_date, int $frequency) :float { - $days = $from_date->diffInDays($to_date); + $days = $from_date->copy()->diffInDays($to_date); $days_in_frequency = $this->getDaysInFrequency($frequency); - +nlog($from_date->format('Y-m-d')); +nlog($days); +nlog($days_in_frequency); +nlog($amount); return round( (($days/$days_in_frequency) * $amount),2); } diff --git a/app/Helpers/Subscription/SubscriptionCalculator.php b/app/Helpers/Subscription/SubscriptionCalculator.php index 3956ef4617ff..18dc20cd2d01 100644 --- a/app/Helpers/Subscription/SubscriptionCalculator.php +++ b/app/Helpers/Subscription/SubscriptionCalculator.php @@ -15,6 +15,7 @@ use App\Helpers\Invoice\ProRata; use App\Models\Invoice; use App\Models\RecurringInvoice; use App\Models\Subscription; +use Illuminate\Support\Carbon; /** * SubscriptionCalculator. @@ -24,7 +25,7 @@ class SubscriptionCalculator public Subscription $target_subscription; - public Invoice $invoice + public Invoice $invoice; public function __construct(Subscription $target_subscription, Invoice $invoice) { @@ -57,6 +58,8 @@ class SubscriptionCalculator //set the starting refund amount $refund_amount = 0; + $refund_invoice = false; + //are they paid up to date. //yes - calculate refund @@ -68,7 +71,7 @@ class SubscriptionCalculator $subscription = Subscription::find($this->invoice->subscription_id); $pro_rata = new ProRata(); - $to_date = $subscription->getNextDateForFrequency(Carbon::parse($refund_invoice->date), $subscription->frequency_id); + $to_date = $subscription->service()->getNextDateForFrequency(Carbon::parse($refund_invoice->date), $subscription->frequency_id); $refund_amount = $pro_rata->refund($refund_invoice->amount, now(), $to_date, $subscription->frequency_id); @@ -88,11 +91,9 @@ class SubscriptionCalculator private function getRefundInvoice() { - return Invoice::where('subscription_id', $this->invoice->subscription_id) ->where('client_id', $this->invoice->client_id) ->where('is_deleted', 0) - ->where('balance', '>', 0) ->orderBy('id', 'desc') ->first(); diff --git a/database/factories/SubscriptionFactory.php b/database/factories/SubscriptionFactory.php index 63bff1c98914..db73008bb34a 100644 --- a/database/factories/SubscriptionFactory.php +++ b/database/factories/SubscriptionFactory.php @@ -33,7 +33,8 @@ class SubscriptionFactory extends Factory public function definition() { return [ - 'frequency_id' => RecurringInvoice::FREQUENCY_MONTHLY + 'frequency_id' => RecurringInvoice::FREQUENCY_MONTHLY, + 'name' => $this->faker->company(), ]; } } diff --git a/tests/Unit/SubscriptionsCalcTest.php b/tests/Unit/SubscriptionsCalcTest.php index 7100f25b8533..61a0adad81bd 100644 --- a/tests/Unit/SubscriptionsCalcTest.php +++ b/tests/Unit/SubscriptionsCalcTest.php @@ -10,8 +10,11 @@ */ namespace Tests\Unit; +use App\Helpers\Invoice\ProRata; +use App\Helpers\Subscription\SubscriptionCalculator; use App\Models\Invoice; use App\Models\Subscription; +use Illuminate\Support\Carbon; use Tests\MockUnitData; use Tests\TestCase; @@ -41,7 +44,13 @@ class SubscriptionsCalcTest extends TestCase '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, ]); $invoice = Invoice::factory()->create([ @@ -57,7 +66,7 @@ class SubscriptionsCalcTest extends TestCase 'tax_name3' => '', 'discount' => 0, 'subscription_id' => $subscription->id, - 'date' => now() + 'date' => '2021-01-01', ]); $invoice = $invoice->calc()->getInvoice(); @@ -69,6 +78,33 @@ class SubscriptionsCalcTest extends TestCase $this->assertEquals(10, $invoice->amount); $this->assertEquals(10, $invoice->balance); + $sub_calculator = new SubscriptionCalculator($target->fresh(), $invoice->fresh()); + + $this->assertFalse($sub_calculator->isPaidUp()); + + $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); + + // $net_upgrade_price = $sub_calculator->calcUpgradePlan(); + + // $this->assertEquals(1.62, $net_upgrade_price); + } From e02dace9cc23f3caae1e7a1eb0e8ff39a02de5b4 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 21 Oct 2021 18:19:40 +1100 Subject: [PATCH 6/9] minor cleanup --- app/Helpers/Subscription/SubscriptionCalculator.php | 2 +- app/Utils/SystemHealth.php | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/app/Helpers/Subscription/SubscriptionCalculator.php b/app/Helpers/Subscription/SubscriptionCalculator.php index 18dc20cd2d01..77f14f16fd0e 100644 --- a/app/Helpers/Subscription/SubscriptionCalculator.php +++ b/app/Helpers/Subscription/SubscriptionCalculator.php @@ -69,7 +69,7 @@ class SubscriptionCalculator if($refund_invoice) { $subscription = Subscription::find($this->invoice->subscription_id); - $pro_rata = new ProRata(); + $pro_rata = new ProRata; $to_date = $subscription->service()->getNextDateForFrequency(Carbon::parse($refund_invoice->date), $subscription->frequency_id); diff --git a/app/Utils/SystemHealth.php b/app/Utils/SystemHealth.php index fb329a85e358..b6ae0d3c880c 100644 --- a/app/Utils/SystemHealth.php +++ b/app/Utils/SystemHealth.php @@ -25,17 +25,13 @@ use Illuminate\Support\Facades\Queue; class SystemHealth { private static $extensions = [ - // 'mysqli', 'gd', 'curl', 'zip', -// 'gmp', 'openssl', 'mbstring', 'xml', 'bcmath', - // 'mysqlnd', - //'intl', //todo double check whether we need this for email dns validation ]; private static $php_version = 7.4; From 0444c96a4778caf55cbfe74e0a8f0c96788505ee Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 21 Oct 2021 21:44:18 +1100 Subject: [PATCH 7/9] Subscription calculations --- app/DataMapper/ClientRegistrationFields.php | 2 +- app/Http/Controllers/Auth/LoginController.php | 16 +++++++------- app/Http/Controllers/CompanyController.php | 2 +- tests/Unit/SubscriptionsCalcTest.php | 21 +++++++------------ 4 files changed, 18 insertions(+), 23 deletions(-) diff --git a/app/DataMapper/ClientRegistrationFields.php b/app/DataMapper/ClientRegistrationFields.php index f56252f6fd70..23106c42b08d 100644 --- a/app/DataMapper/ClientRegistrationFields.php +++ b/app/DataMapper/ClientRegistrationFields.php @@ -32,7 +32,7 @@ class ClientRegistrationFields ], [ 'key' => 'phone', - 'required' => true + 'required' => false ], [ 'key' => 'password', diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 20634db141fa..000cfdf98a31 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -260,14 +260,14 @@ class LoginController extends BaseController ->increment() ->batch(); - SystemLogger::dispatch( - json_encode(['ip' => request()->getClientIp()]), - SystemLog::CATEGORY_SECURITY, - SystemLog::EVENT_USER, - SystemLog::TYPE_LOGIN_FAILURE, - null, - Company::first(), - ); + // SystemLogger::dispatch( + // json_encode(['ip' => request()->getClientIp()]), + // SystemLog::CATEGORY_SECURITY, + // SystemLog::EVENT_USER, + // SystemLog::TYPE_LOGIN_FAILURE, + // null, + // Company::first(), + // ); $this->incrementLoginAttempts($request); diff --git a/app/Http/Controllers/CompanyController.php b/app/Http/Controllers/CompanyController.php index 54b04308325e..5ac5ed157a78 100644 --- a/app/Http/Controllers/CompanyController.php +++ b/app/Http/Controllers/CompanyController.php @@ -499,7 +499,7 @@ class CompanyController extends BaseController $account->delete(); - if(Ninja::isHosted() && $request->has('cancellation_message') && strlen($request->input('cancellation_message')) > 1) + if(Ninja::isHosted()) \Modules\Admin\Jobs\Account\NinjaDeletedAccount::dispatch($account_key, $request->all()); LightLogs::create(new AccountDeleted()) diff --git a/tests/Unit/SubscriptionsCalcTest.php b/tests/Unit/SubscriptionsCalcTest.php index 61a0adad81bd..98e07240bef3 100644 --- a/tests/Unit/SubscriptionsCalcTest.php +++ b/tests/Unit/SubscriptionsCalcTest.php @@ -6,8 +6,9 @@ * * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com) * - * @license https://opensource.org/licenses/AAL + * @license https://www.elastic.co/licensing/elastic-license */ + namespace Tests\Unit; use App\Helpers\Invoice\ProRata; @@ -30,7 +31,7 @@ class SubscriptionsCalcTest extends TestCase * * No method can guarantee against false positives. */ - public function setUp() :void + public function setUp(): void { parent::setUp(); @@ -39,7 +40,7 @@ class SubscriptionsCalcTest extends TestCase public function testCalcUpgradePrice() { - + $subscription = Subscription::factory()->create([ 'company_id' => $this->company->id, 'user_id' => $this->user->id, @@ -67,7 +68,7 @@ class SubscriptionsCalcTest extends TestCase 'discount' => 0, 'subscription_id' => $subscription->id, 'date' => '2021-01-01', - ]); + ]); $invoice = $invoice->calc()->getInvoice(); @@ -89,23 +90,17 @@ class SubscriptionsCalcTest extends TestCase $this->assertEquals(10, $invoice->amount); $this->assertEquals(0, $invoice->balance); - $pro_rata = new ProRata(); + $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(); + $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); - // $net_upgrade_price = $sub_calculator->calcUpgradePlan(); - - // $this->assertEquals(1.62, $net_upgrade_price); - } - - -} \ No newline at end of file +} From ca1e4d8ec727cef236ff371926c0a5ca5420ff95 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 21 Oct 2021 22:05:23 +1100 Subject: [PATCH 8/9] Minor fixes for test PDF --- app/Http/Controllers/SetupController.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/SetupController.php b/app/Http/Controllers/SetupController.php index 83ed146b313e..903e350de92e 100644 --- a/app/Http/Controllers/SetupController.php +++ b/app/Http/Controllers/SetupController.php @@ -20,6 +20,7 @@ use App\Jobs\Util\SchedulerCheck; use App\Jobs\Util\VersionCheck; use App\Models\Account; use App\Utils\CurlUtils; +use App\Utils\HostedPDF\NinjaPdf; use App\Utils\Ninja; use App\Utils\SystemHealth; use App\Utils\Traits\AppSetup; @@ -245,7 +246,7 @@ class SetupController extends Controller public function checkPdf(Request $request) { try { - if (config('ninja.phantomjs_pdf_generation')) { + if (config('ninja.pdf_generator') == 'phantom') { return $this->testPhantom(); } From 5ee9a60899b0cc1b558e671f8e89e00b02469ba7 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 22 Oct 2021 06:57:07 +1100 Subject: [PATCH 9/9] Prevent docker installations from reaching some test data creation console commands --- app/Console/Commands/CreateSingleAccount.php | 4 +++ app/Console/Commands/CreateTestData.php | 3 ++ app/Console/Commands/DemoMode.php | 3 ++ app/Console/Commands/SubdomainFill.php | 31 -------------------- 4 files changed, 10 insertions(+), 31 deletions(-) diff --git a/app/Console/Commands/CreateSingleAccount.php b/app/Console/Commands/CreateSingleAccount.php index 92d1bacf3662..fe5c16f34891 100644 --- a/app/Console/Commands/CreateSingleAccount.php +++ b/app/Console/Commands/CreateSingleAccount.php @@ -90,6 +90,10 @@ class CreateSingleAccount extends Command */ public function handle() { + + if(config('ninja.is_docker')) + return; + MultiDB::setDb($this->option('database')); $this->info(date('r').' Create Single Sample Account...'); diff --git a/app/Console/Commands/CreateTestData.php b/app/Console/Commands/CreateTestData.php index b98205e70c2c..6d1426c5ed7b 100644 --- a/app/Console/Commands/CreateTestData.php +++ b/app/Console/Commands/CreateTestData.php @@ -78,6 +78,9 @@ class CreateTestData extends Command */ public function handle() { + if(config('ninja.is_docker')) + return; + $this->info(date('r').' Running CreateTestData...'); $this->count = $this->argument('count'); diff --git a/app/Console/Commands/DemoMode.php b/app/Console/Commands/DemoMode.php index c57d084b696b..3119ac21b63e 100644 --- a/app/Console/Commands/DemoMode.php +++ b/app/Console/Commands/DemoMode.php @@ -86,6 +86,9 @@ class DemoMode extends Command { set_time_limit(0); + if(config('ninja.is_docker')) + return; + $cached_tables = config('ninja.cached_tables'); foreach ($cached_tables as $name => $class) { diff --git a/app/Console/Commands/SubdomainFill.php b/app/Console/Commands/SubdomainFill.php index afa2624ae0e3..ad5471d7866c 100644 --- a/app/Console/Commands/SubdomainFill.php +++ b/app/Console/Commands/SubdomainFill.php @@ -59,37 +59,6 @@ class SubdomainFill extends Command }); - - // $db1 = Company::on('db-ninja-01')->get(); - - // $db1->each(function ($company){ - - // $db2 = Company::on('db-ninja-02a')->find($company->id); - - // if($db2) - // { - // $db2->subdomain = $company->subdomain; - // $db2->save(); - // } - - // }); - - - // $db1 = null; - // $db2 = null; - - // $db2 = Company::on('db-ninja-02')->get(); - - // $db2->each(function ($company){ - - // $db1 = Company::on('db-ninja-01a')->find($company->id); - - // if($db1) - // { - // $db1->subdomain = $company->subdomain; - // $db1->save(); - // } - // }); } }