Fixes for timezone issues with recurring entities

This commit is contained in:
David Bomba 2022-06-02 13:49:29 +10:00
parent 38b77e72fe
commit 3bf56af37f
15 changed files with 224 additions and 23 deletions

View File

@ -204,9 +204,9 @@ class RecurringInvoiceController extends BaseController
{ {
$recurring_invoice = $this->recurring_invoice_repo->save($request->all(), RecurringInvoiceFactory::create(auth()->user()->company()->id, auth()->user()->id)); $recurring_invoice = $this->recurring_invoice_repo->save($request->all(), RecurringInvoiceFactory::create(auth()->user()->company()->id, auth()->user()->id));
$offset = $recurring_invoice->client->timezone_offset(); // $offset = $recurring_invoice->client->timezone_offset();
$recurring_invoice->next_send_date = Carbon::parse($recurring_invoice->next_send_date)->startOfDay()->addSeconds($offset); // $recurring_invoice->next_send_date = Carbon::parse($recurring_invoice->next_send_date)->startOfDay()->addSeconds($offset);
$recurring_invoice->saveQuietly(); // $recurring_invoice->saveQuietly();
$recurring_invoice->service() $recurring_invoice->service()
->triggeredActions($request) ->triggeredActions($request)

View File

@ -55,6 +55,10 @@ class StoreRecurringExpenseRequest extends Request
$input = $this->decodePrimaryKeys($input); $input = $this->decodePrimaryKeys($input);
if (array_key_exists('next_send_date', $input) && is_string($input['next_send_date'])) {
$input['next_send_date_client'] = $input['next_send_date'];
}
if (array_key_exists('category_id', $input) && is_string($input['category_id'])) { if (array_key_exists('category_id', $input) && is_string($input['category_id'])) {
$input['category_id'] = $this->decodePrimaryKey($input['category_id']); $input['category_id'] = $this->decodePrimaryKey($input['category_id']);
} }

View File

@ -66,6 +66,10 @@ class UpdateRecurringExpenseRequest extends Request
$input = $this->decodePrimaryKeys($input); $input = $this->decodePrimaryKeys($input);
if (array_key_exists('next_send_date', $input) && is_string($input['next_send_date'])) {
$input['next_send_date_client'] = $input['next_send_date'];
}
if (array_key_exists('category_id', $input) && is_string($input['category_id'])) { if (array_key_exists('category_id', $input) && is_string($input['category_id'])) {
$input['category_id'] = $this->decodePrimaryKey($input['category_id']); $input['category_id'] = $this->decodePrimaryKey($input['category_id']);
} }

View File

@ -67,6 +67,10 @@ class StoreRecurringInvoiceRequest extends Request
{ {
$input = $this->all(); $input = $this->all();
if (array_key_exists('next_send_date', $input) && is_string($input['next_send_date'])) {
$input['next_send_date_client'] = $input['next_send_date'];
}
if (array_key_exists('design_id', $input) && is_string($input['design_id'])) { if (array_key_exists('design_id', $input) && is_string($input['design_id'])) {
$input['design_id'] = $this->decodePrimaryKey($input['design_id']); $input['design_id'] = $this->decodePrimaryKey($input['design_id']);
} }

View File

@ -61,6 +61,10 @@ class UpdateRecurringInvoiceRequest extends Request
{ {
$input = $this->all(); $input = $this->all();
if (array_key_exists('next_send_date', $input) && is_string($input['next_send_date'])) {
$input['next_send_date_client'] = $input['next_send_date'];
}
if (array_key_exists('design_id', $input) && is_string($input['design_id'])) { if (array_key_exists('design_id', $input) && is_string($input['design_id'])) {
$input['design_id'] = $this->decodePrimaryKey($input['design_id']); $input['design_id'] = $this->decodePrimaryKey($input['design_id']);
} }

View File

@ -94,6 +94,8 @@ class RecurringExpensesCron
$expense->save(); $expense->save();
$recurring_expense->next_send_date = $recurring_expense->nextSendDate(); $recurring_expense->next_send_date = $recurring_expense->nextSendDate();
$recurring_expense->next_send_date_client = $recurring_expense->next_send_date;
$recurring_expense->remaining_cycles = $recurring_expense->remainingCycles(); $recurring_expense->remaining_cycles = $recurring_expense->remainingCycles();
$recurring_expense->save(); $recurring_expense->save();
} }

View File

@ -105,6 +105,7 @@ class SendRecurring implements ShouldQueue
nlog("updating recurring invoice dates"); nlog("updating recurring invoice dates");
/* Set next date here to prevent a recurring loop forming */ /* Set next date here to prevent a recurring loop forming */
$this->recurring_invoice->next_send_date = $this->recurring_invoice->nextSendDate(); $this->recurring_invoice->next_send_date = $this->recurring_invoice->nextSendDate();
$this->recurring_invoice->next_send_date_client = $this->recurring_invoice->nextSendDateClient();
$this->recurring_invoice->remaining_cycles = $this->recurring_invoice->remainingCycles(); $this->recurring_invoice->remaining_cycles = $this->recurring_invoice->remainingCycles();
$this->recurring_invoice->last_sent_date = now(); $this->recurring_invoice->last_sent_date = now();

View File

@ -667,6 +667,8 @@ class Client extends BaseModel implements HasLocalePreference
$offset -= $timezone->utc_offset; $offset -= $timezone->utc_offset;
$offset += ($entity_send_time * 3600); $offset += ($entity_send_time * 3600);
nlog("offset = {$offset}");
return $offset; return $offset;
} }

View File

@ -63,6 +63,7 @@ class RecurringExpense extends BaseModel
'last_sent_date', 'last_sent_date',
'next_send_date', 'next_send_date',
'remaining_cycles', 'remaining_cycles',
'next_send_date_client',
]; ];
protected $casts = [ protected $casts = [
@ -153,6 +154,43 @@ class RecurringExpense extends BaseModel
} }
} }
public function nextSendDateClient() :?Carbon
{
if (!$this->next_send_date) {
return null;
}
switch ($this->frequency_id) {
case RecurringInvoice::FREQUENCY_DAILY:
return Carbon::parse($this->next_send_date)->startOfDay()->addDay();
case RecurringInvoice::FREQUENCY_WEEKLY:
return Carbon::parse($this->next_send_date)->startOfDay()->addWeek();
case RecurringInvoice::FREQUENCY_TWO_WEEKS:
return Carbon::parse($this->next_send_date)->startOfDay()->addWeeks(2);
case RecurringInvoice::FREQUENCY_FOUR_WEEKS:
return Carbon::parse($this->next_send_date)->startOfDay()->addWeeks(4);
case RecurringInvoice::FREQUENCY_MONTHLY:
return Carbon::parse($this->next_send_date)->startOfDay()->addMonthNoOverflow();
case RecurringInvoice::FREQUENCY_TWO_MONTHS:
return Carbon::parse($this->next_send_date)->startOfDay()->addMonthsNoOverflow(2);
case RecurringInvoice::FREQUENCY_THREE_MONTHS:
return Carbon::parse($this->next_send_date)->startOfDay()->addMonthsNoOverflow(3);
case RecurringInvoice::FREQUENCY_FOUR_MONTHS:
return Carbon::parse($this->next_send_date)->startOfDay()->addMonthsNoOverflow(4);
case RecurringInvoice::FREQUENCY_SIX_MONTHS:
return Carbon::parse($this->next_send_date)->startOfDay()->addMonthsNoOverflow(6);
case RecurringInvoice::FREQUENCY_ANNUALLY:
return Carbon::parse($this->next_send_date)->startOfDay()->addYear();
case RecurringInvoice::FREQUENCY_TWO_YEARS:
return Carbon::parse($this->next_send_date)->startOfDay()->addYears(2);
case RecurringInvoice::FREQUENCY_THREE_YEARS:
return Carbon::parse($this->next_send_date)->startOfDay()->addYears(3);
default:
return null;
}
}
public function remainingCycles() : int public function remainingCycles() : int
{ {
if ($this->remaining_cycles == 0) { if ($this->remaining_cycles == 0) {

View File

@ -108,6 +108,7 @@ class RecurringInvoice extends BaseModel
'assigned_user_id', 'assigned_user_id',
'exchange_rate', 'exchange_rate',
'vendor_id', 'vendor_id',
'next_send_date_client',
]; ];
protected $casts = [ protected $casts = [
@ -224,7 +225,7 @@ class RecurringInvoice extends BaseModel
public function nextSendDate() :?Carbon public function nextSendDate() :?Carbon
{ {
if (!$this->next_send_date) { if (!$this->next_send_date_client) {
return null; return null;
} }
@ -236,7 +237,7 @@ class RecurringInvoice extends BaseModel
/* Lets set the next send date to now so we increment from today, rather than in the past*/ /* Lets set the next send date to now so we increment from today, rather than in the past*/
if(Carbon::parse($this->next_send_date)->lt(now()->subDays(3))) if(Carbon::parse($this->next_send_date)->lt(now()->subDays(3)))
$this->next_send_date = now()->format('Y-m-d'); $this->next_send_date_client = now()->format('Y-m-d');
} }
@ -244,34 +245,80 @@ class RecurringInvoice extends BaseModel
As we are firing at UTC+0 if our offset is negative it is technically firing the day before so we always need As we are firing at UTC+0 if our offset is negative it is technically firing the day before so we always need
to add ON a day - a day = 86400 seconds to add ON a day - a day = 86400 seconds
*/ */
if($offset < 0) // if($offset < 0)
$offset += 86400; // $offset += 86400;
switch ($this->frequency_id) { switch ($this->frequency_id) {
case self::FREQUENCY_DAILY: case self::FREQUENCY_DAILY:
return Carbon::parse($this->next_send_date)->startOfDay()->addDay()->addSeconds($offset); return Carbon::parse($this->next_send_date_client)->startOfDay()->addDay()->addSeconds($offset);
case self::FREQUENCY_WEEKLY: case self::FREQUENCY_WEEKLY:
return Carbon::parse($this->next_send_date)->startOfDay()->addWeek()->addSeconds($offset); return Carbon::parse($this->next_send_date_client)->startOfDay()->addWeek()->addSeconds($offset);
case self::FREQUENCY_TWO_WEEKS: case self::FREQUENCY_TWO_WEEKS:
return Carbon::parse($this->next_send_date)->startOfDay()->addWeeks(2)->addSeconds($offset); return Carbon::parse($this->next_send_date_client)->startOfDay()->addWeeks(2)->addSeconds($offset);
case self::FREQUENCY_FOUR_WEEKS: case self::FREQUENCY_FOUR_WEEKS:
return Carbon::parse($this->next_send_date)->startOfDay()->addWeeks(4)->addSeconds($offset); return Carbon::parse($this->next_send_date_client)->startOfDay()->addWeeks(4)->addSeconds($offset);
case self::FREQUENCY_MONTHLY: case self::FREQUENCY_MONTHLY:
return Carbon::parse($this->next_send_date)->startOfDay()->addMonthNoOverflow()->addSeconds($offset); return Carbon::parse($this->next_send_date_client)->startOfDay()->addMonthNoOverflow()->addSeconds($offset);
case self::FREQUENCY_TWO_MONTHS: case self::FREQUENCY_TWO_MONTHS:
return Carbon::parse($this->next_send_date)->startOfDay()->addMonthsNoOverflow(2)->addSeconds($offset); return Carbon::parse($this->next_send_date_client)->startOfDay()->addMonthsNoOverflow(2)->addSeconds($offset);
case self::FREQUENCY_THREE_MONTHS: case self::FREQUENCY_THREE_MONTHS:
return Carbon::parse($this->next_send_date)->startOfDay()->addMonthsNoOverflow(3)->addSeconds($offset); return Carbon::parse($this->next_send_date_client)->startOfDay()->addMonthsNoOverflow(3)->addSeconds($offset);
case self::FREQUENCY_FOUR_MONTHS: case self::FREQUENCY_FOUR_MONTHS:
return Carbon::parse($this->next_send_date)->startOfDay()->addMonthsNoOverflow(4)->addSeconds($offset); return Carbon::parse($this->next_send_date_client)->startOfDay()->addMonthsNoOverflow(4)->addSeconds($offset);
case self::FREQUENCY_SIX_MONTHS: case self::FREQUENCY_SIX_MONTHS:
return Carbon::parse($this->next_send_date)->startOfDay()->addMonthsNoOverflow(6)->addSeconds($offset); return Carbon::parse($this->next_send_date_client)->startOfDay()->addMonthsNoOverflow(6)->addSeconds($offset);
case self::FREQUENCY_ANNUALLY: case self::FREQUENCY_ANNUALLY:
return Carbon::parse($this->next_send_date)->startOfDay()->addYear()->addSeconds($offset); return Carbon::parse($this->next_send_date_client)->startOfDay()->addYear()->addSeconds($offset);
case self::FREQUENCY_TWO_YEARS: case self::FREQUENCY_TWO_YEARS:
return Carbon::parse($this->next_send_date)->startOfDay()->addYears(2)->addSeconds($offset); return Carbon::parse($this->next_send_date_client)->startOfDay()->addYears(2)->addSeconds($offset);
case self::FREQUENCY_THREE_YEARS: case self::FREQUENCY_THREE_YEARS:
return Carbon::parse($this->next_send_date)->startOfDay()->addYears(3)->addSeconds($offset); return Carbon::parse($this->next_send_date_client)->startOfDay()->addYears(3)->addSeconds($offset);
default:
return null;
}
}
public function nextSendDateClient() :?Carbon
{
if (!$this->next_send_date_client) {
return null;
}
/* If this setting is enabled, the recurring invoice may be set in the past */
if($this->company->stop_on_unpaid_recurring) {
/* Lets set the next send date to now so we increment from today, rather than in the past*/
if(Carbon::parse($this->next_send_date)->lt(now()->subDays(3)))
$this->next_send_date_client = now()->format('Y-m-d');
}
switch ($this->frequency_id) {
case self::FREQUENCY_DAILY:
return Carbon::parse($this->next_send_date_client)->startOfDay()->addDay();
case self::FREQUENCY_WEEKLY:
return Carbon::parse($this->next_send_date_client)->startOfDay()->addWeek();
case self::FREQUENCY_TWO_WEEKS:
return Carbon::parse($this->next_send_date_client)->startOfDay()->addWeeks(2);
case self::FREQUENCY_FOUR_WEEKS:
return Carbon::parse($this->next_send_date_client)->startOfDay()->addWeeks(4);
case self::FREQUENCY_MONTHLY:
return Carbon::parse($this->next_send_date_client)->startOfDay()->addMonthNoOverflow();
case self::FREQUENCY_TWO_MONTHS:
return Carbon::parse($this->next_send_date_client)->startOfDay()->addMonthsNoOverflow(2);
case self::FREQUENCY_THREE_MONTHS:
return Carbon::parse($this->next_send_date_client)->startOfDay()->addMonthsNoOverflow(3);
case self::FREQUENCY_FOUR_MONTHS:
return Carbon::parse($this->next_send_date_client)->startOfDay()->addMonthsNoOverflow(4);
case self::FREQUENCY_SIX_MONTHS:
return Carbon::parse($this->next_send_date_client)->startOfDay()->addMonthsNoOverflow(6);
case self::FREQUENCY_ANNUALLY:
return Carbon::parse($this->next_send_date_client)->startOfDay()->addYear();
case self::FREQUENCY_TWO_YEARS:
return Carbon::parse($this->next_send_date_client)->startOfDay()->addYears(2);
case self::FREQUENCY_THREE_YEARS:
return Carbon::parse($this->next_send_date_client)->startOfDay()->addYears(3);
default: default:
return null; return null;
} }
@ -461,11 +508,11 @@ class RecurringInvoice extends BaseModel
$data = []; $data = [];
if (!Carbon::parse($this->next_send_date)) { if (!Carbon::parse($this->next_send_date_client)) {
return $data; return $data;
} }
$next_send_date = Carbon::parse($this->next_send_date)->copy(); $next_send_date = Carbon::parse($this->next_send_date_client)->copy();
for ($x=0; $x<$iterations; $x++) { for ($x=0; $x<$iterations; $x++) {
// we don't add the days... we calc the day of the month!! // we don't add the days... we calc the day of the month!!

View File

@ -106,6 +106,12 @@ class RecurringService
$this->stop(); $this->stop();
} }
if(isset($this->recurring_entity->client))
{
$offset = $this->recurring_entity->client->timezone_offset();
$this->recurring_entity->next_send_date = Carbon::parse($this->recurring_entity->next_send_date_client)->startOfDay()->addSeconds($offset);
}
return $this; return $this;
} }

View File

@ -100,7 +100,8 @@ class RecurringExpenseTransformer extends EntityTransformer
'frequency_id' => (string) $recurring_expense->frequency_id, 'frequency_id' => (string) $recurring_expense->frequency_id,
'remaining_cycles' => (int) $recurring_expense->remaining_cycles, 'remaining_cycles' => (int) $recurring_expense->remaining_cycles,
'last_sent_date' => $recurring_expense->last_sent_date ?: '', 'last_sent_date' => $recurring_expense->last_sent_date ?: '',
'next_send_date' => $recurring_expense->next_send_date ?: '', // 'next_send_date' => $recurring_expense->next_send_date ?: '',
'next_send_date' => $recurring_expense->next_send_date_client ?: '',
'recurring_dates' => (array) [], 'recurring_dates' => (array) [],
]; ];

View File

@ -95,7 +95,8 @@ class RecurringInvoiceTransformer extends EntityTransformer
'po_number' => $invoice->po_number ?: '', 'po_number' => $invoice->po_number ?: '',
'date' => $invoice->date ?: '', 'date' => $invoice->date ?: '',
'last_sent_date' => $invoice->last_sent_date ?: '', 'last_sent_date' => $invoice->last_sent_date ?: '',
'next_send_date' => $invoice->next_send_date ?: '', // 'next_send_date' => $invoice->next_send_date ?: '',
'next_send_date' => $invoice->next_send_date_client ?: '',
'due_date' => $invoice->due_date ?: '', 'due_date' => $invoice->due_date ?: '',
'terms' => $invoice->terms ?: '', 'terms' => $invoice->terms ?: '',
'public_notes' => $invoice->public_notes ?: '', 'public_notes' => $invoice->public_notes ?: '',

View File

@ -0,0 +1,47 @@
<?php
use App\Models\RecurringExpense;
use App\Models\RecurringInvoice;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class SetRecurringClientTimestamp extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('recurring_invoices', function (Blueprint $table) {
$table->datetime('next_send_date_client')->nullable();
});
Schema::table('recurring_expenses', function (Blueprint $table) {
$table->datetime('next_send_date_client')->nullable();
});
RecurringInvoice::whereNotNull('next_send_date')->cursor()->each(function ($recurring_invoice){
$recurring_invoice->next_send_date_client = $recurring_invoice->next_send_date;
$recurring_invoice->saveQuietly();
});
RecurringExpense::whereNotNull('next_send_date')->cursor()->each(function ($recurring_expense){
$recurring_expense->next_send_date_client = $recurring_expense->next_send_date;
$recurring_expense->saveQuietly();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@ -50,6 +50,46 @@ class RecurringInvoiceTest extends TestCase
$this->makeTestData(); $this->makeTestData();
} }
public function testTimezoneNextSendDateCalculations()
{
$settings = $this->company->settings;
$settings->timezone_id = '112';
$this->company->settings = $settings;
$this->company->save();
$data = [
'frequency_id' => 1,
'status_id' => 1,
'discount' => 0,
'is_amount_discount' => 1,
'po_number' => '3434343',
'public_notes' => 'notes',
'next_send_date' => now()->addDay()->format('Y-m-d'),
'is_deleted' => 0,
'custom_value1' => 0,
'custom_value2' => 0,
'custom_value3' => 0,
'custom_value4' => 0,
'status' => 1,
'client_id' => $this->encodePrimaryKey($this->client->id),
'line_items' => $this->buildLineItems(),
'remaining_cycles' => -1,
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/recurring_invoices?start=true', $data)
->assertStatus(200);
$arr = $response->json();
$this->assertEquals(RecurringInvoice::STATUS_ACTIVE, $arr['data']['status_id']);
$this->assertEquals(now()->addDay()->format('Y-m-d'), $arr['data']['next_send_date']);
}
public function testPostRecurringInvoice() public function testPostRecurringInvoice()
{ {
$data = [ $data = [