diff --git a/app/Models/RecurringInvoice.php b/app/Models/RecurringInvoice.php index 128b704fda9a..a650ba3c7ea4 100644 --- a/app/Models/RecurringInvoice.php +++ b/app/Models/RecurringInvoice.php @@ -16,6 +16,7 @@ use App\Helpers\Invoice\InvoiceSumInclusive; use App\Models\Filterable; use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesHash; +use App\Utils\Traits\Recurring\HasRecurrence; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Carbon; @@ -29,6 +30,8 @@ class RecurringInvoice extends BaseModel use SoftDeletes; use Filterable; use MakesDates; + use HasRecurrence; + /** * Invoice Statuses. */ @@ -353,7 +356,7 @@ class RecurringInvoice extends BaseModel */ public function recurringDates() { - info($this->next_send_date); + /* Return early if nothing to send back! */ if( $this->status_id == self::STATUS_COMPLETED || $this->status_id == self::STATUS_DRAFT || @@ -387,7 +390,7 @@ class RecurringInvoice extends BaseModel 'due_date' => $next_due_date->format('Y-m-d'), ]; - $next_send_date = $this->nextDateByFrequency($next_send_date); + $next_send_date = $this->calculateDueDate($next_send_date); } @@ -404,4 +407,35 @@ class RecurringInvoice extends BaseModel } + private function calculateDueDate($date) + { + + switch ($this->due_date_days) { + case 'terms': + return $this->calculateDateFromTerms($date); + break; + default: + return $this->setDayOfMonth($date, $this->due_date_days); + break; + } + } + + /** + * Calculates a date based on the client payment terms. + * + * @param Carbon $date A given date + * @return NULL|Carbon The date + */ + public function calculateDateFromTerms($date) + { + + $client_payment_terms = $this->client->getSetting('payment_terms'); + + if($client_payment_terms == '')//no due date! return null; + return null; + + return $date->copy()->addDays($client_payment_terms); //add the number of days in the payment terms to the date + } + + } diff --git a/app/Transformers/RecurringInvoiceTransformer.php b/app/Transformers/RecurringInvoiceTransformer.php index 1458595b8792..e8af2751ef00 100644 --- a/app/Transformers/RecurringInvoiceTransformer.php +++ b/app/Transformers/RecurringInvoiceTransformer.php @@ -131,7 +131,7 @@ class RecurringInvoiceTransformer extends EntityTransformer 'custom_surcharge_tax3' => (bool) $invoice->custom_surcharge_tax3, 'custom_surcharge_tax4' => (bool) $invoice->custom_surcharge_tax4, 'line_items' => $invoice->line_items ?: (array) [], - 'entity_type' => 'recurring_invoice', + 'entity_type' => 'recurringInvoice', 'frequency_id' => (string) $invoice->frequency_id, 'remaining_cycles' => (int) $invoice->remaining_cycles, 'recurring_dates' => (array) $invoice->recurringDates(), diff --git a/app/Utils/Traits/Recurring/HasRecurrence.php b/app/Utils/Traits/Recurring/HasRecurrence.php new file mode 100644 index 000000000000..6560e18dd956 --- /dev/null +++ b/app/Utils/Traits/Recurring/HasRecurrence.php @@ -0,0 +1,71 @@ +copy()->startOfMonth()->addMonth(); + } + + /** + * Calculates the last day of the month. + * + * If it is the last day of the month - we add a month on. + * + * @param Carbon $date The start date + * @return Carbon The last day of month + */ + public function calculateLastDayOfMonth($date) + { + if($date->isLastOfMonth()) + return $date->copy()->addMonthNoOverflow()->endOfMonth(); + + return $date->copy()->endOfMonth(); + } + + /** + * Sets the day of the month, if in the past we ADD a month + * + * @param Carbon $date The start date + * @param String|Int $day_of_month The day of the month + */ + public function setDayOfMonth($date, $day_of_month) + { + + $set_date = $date->copy()->setUnitNoOverflow('day', $day_of_month, 'month'); + + //If the set date is less than the original date we need to add a month. + //If we are overflowing dates, then we need to diff the dates and ensure it doesn't equal 0 + if($set_date->lte($date) || $set_date->diffInDays($date) == 0) + $set_date->addMonthNoOverflow(); + + if($day_of_month == '31') + $set_date->endOfMonth(); + + + return $set_date; + } + +} \ No newline at end of file diff --git a/database/migrations/2020_08_18_140557_add_is_public_to_documents_table.php b/database/migrations/2020_08_18_140557_add_is_public_to_documents_table.php index e7e86ec690fb..8713851f8e66 100644 --- a/database/migrations/2020_08_18_140557_add_is_public_to_documents_table.php +++ b/database/migrations/2020_08_18_140557_add_is_public_to_documents_table.php @@ -70,7 +70,7 @@ class AddIsPublicToDocumentsTable extends Migration Schema::table('recurring_invoices', function (Blueprint $table) { $table->integer('remaining_cycles')->nullable()->change(); $table->dropColumn('start_date'); - $table->integer('due_date_days')->nullable(); + $table->string('due_date_days')->nullable(); $table->date('partial_due_date')->nullable(); }); } diff --git a/tests/Feature/CancelInvoiceTest.php b/tests/Feature/CancelInvoiceTest.php index 89667fb75739..654f165a8560 100644 --- a/tests/Feature/CancelInvoiceTest.php +++ b/tests/Feature/CancelInvoiceTest.php @@ -1,5 +1,13 @@ status_id = RecurringInvoice::STATUS_PENDING; $recurring_invoice->frequency_id = RecurringInvoice::FREQUENCY_MONTHLY; $recurring_invoice->remaining_cycles = 5; - $recurring_invoice->due_date_days = 5; + $recurring_invoice->due_date_days = '5'; $recurring_invoice->next_send_date = now(); $recurring_invoice->save(); diff --git a/tests/Unit/RecurringDueDatesTest.php b/tests/Unit/RecurringDueDatesTest.php new file mode 100644 index 000000000000..fa153791a076 --- /dev/null +++ b/tests/Unit/RecurringDueDatesTest.php @@ -0,0 +1,175 @@ +calculateFirstDayOfMonth($date); + + $this->assertEquals('2020-03-01', $due_date->format('Y-m-d')); + } + + public function testFirstOfMonthOnFirst() + { + + $date = Carbon::parse('2020-02-01'); + + $due_date = $this->calculateFirstDayOfMonth($date); + + $this->assertEquals('2020-03-01', $due_date->format('Y-m-d')); + + } + + + public function testFirstOfMonthOnLast() + { + + $date = Carbon::parse('2020-03-31'); + + $due_date = $this->calculateFirstDayOfMonth($date); + + $this->assertEquals('2020-04-01', $due_date->format('Y-m-d')); + + } + + public function testLastOfMonth() + { + + $date = Carbon::parse('2020-02-15'); + + $due_date = $this->calculateLastDayOfMonth($date); + + $this->assertEquals('2020-02-29', $due_date->format('Y-m-d')); + + } + + public function testLastOfMonthOnFirst() + { + + $date = Carbon::parse('2020-02-1'); + + $due_date = $this->calculateLastDayOfMonth($date); + + $this->assertEquals('2020-02-29', $due_date->format('Y-m-d')); + + } + + public function testLastOfMonthOnLast() + { + + $date = Carbon::parse('2020-02-29'); + + $due_date = $this->calculateLastDayOfMonth($date); + + $this->assertEquals('2020-03-31', $due_date->format('Y-m-d')); + + } + + public function testDayOfMonth() + { + $date = Carbon::parse('2020-02-01'); + + $due_date = $this->setDayOfMonth($date, '15'); + + $this->assertEquals('2020-02-15', $due_date->format('Y-m-d')); + + } + + public function testDayOfMonthInFuture() + { + $date = Carbon::parse('2020-02-16'); + + $due_date = $this->setDayOfMonth($date, '15'); + + $this->assertEquals('2020-03-15', $due_date->format('Y-m-d')); + + } + + public function testDayOfMonthSameDay() + { + $date = Carbon::parse('2020-02-01'); + + $due_date = $this->setDayOfMonth($date, '1'); + + $this->assertEquals('2020-03-01', $due_date->format('Y-m-d')); + + } + + + public function testDayOfMonthWithOverflow() + { + $date = Carbon::parse('2020-1-31'); + + $due_date = $this->setDayOfMonth($date, '31'); + + $this->assertEquals('2020-02-29', $due_date->format('Y-m-d')); + + } + + public function testDayOfMonthWithOverflow2() + { + $date = Carbon::parse('2020-02-29'); + + $due_date = $this->setDayOfMonth($date, '31'); + + $this->assertEquals('2020-03-31', $due_date->format('Y-m-d')); + + } + + public function testDayOfMonthWithOverflow3() + { + $date = Carbon::parse('2020-01-30'); + + $due_date = $this->setDayOfMonth($date, '30'); + + $this->assertEquals('2020-02-29', $due_date->format('Y-m-d')); + + } + + public function testDayOfMonthWithOverflow4() + { + $date = Carbon::parse('2019-02-28'); + + $due_date = $this->setDayOfMonth($date, '31'); + + $this->assertEquals('2019-03-31', $due_date->format('Y-m-d')); + + } + + public function testDayOfMonthWithOverflow5() + { + $date = Carbon::parse('2019-1-31'); + + $due_date = $this->setDayOfMonth($date, '31'); + + $this->assertEquals('2019-02-28', $due_date->format('Y-m-d')); + + } + +} diff --git a/tests/Unit/SentryTest.php b/tests/Unit/SentryTest.php index 9b8fa2ef3103..42abb51ced57 100644 --- a/tests/Unit/SentryTest.php +++ b/tests/Unit/SentryTest.php @@ -1,5 +1,13 @@