diff --git a/app/Factory/SchedulerFactory.php b/app/Factory/SchedulerFactory.php index c6603e148d4f..5f60fa6dd7ec 100644 --- a/app/Factory/SchedulerFactory.php +++ b/app/Factory/SchedulerFactory.php @@ -26,7 +26,9 @@ class SchedulerFactory $scheduler->is_paused = false; $scheduler->is_deleted = false; $scheduler->template = ''; - + $scheduler->next_run = now()->format('Y-m-d'); + $scheduler->next_run_client = now()->format('Y-m-d'); + return $scheduler; } } diff --git a/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php b/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php index 0c63f95a9542..4eae43c59d23 100644 --- a/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php +++ b/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php @@ -12,10 +12,14 @@ namespace App\Http\Requests\TaskScheduler; use App\Http\Requests\Request; +use App\Http\ValidationRules\Scheduler\ValidClientIds; +use App\Models\Client; +use App\Utils\Traits\MakesHash; use Illuminate\Validation\Rule; class StoreSchedulerRequest extends Request { + use MakesHash; /** * Determine if the user is authorized to make this request. * @@ -33,12 +37,27 @@ class StoreSchedulerRequest extends Request 'name' => ['bail', 'required', Rule::unique('schedulers')->where('company_id', auth()->user()->company()->id)], 'is_paused' => 'bail|sometimes|boolean', 'frequency_id' => 'bail|required|integer|digits_between:1,12', - 'next_run' => 'bail|required|date:Y-m-d', + 'next_run' => 'bail|required|date:Y-m-d|after_or_equal:today', + 'next_run_client' => 'bail|sometimes|date:Y-m-d', 'template' => 'bail|required|string', 'parameters' => 'bail|array', + 'parameters.clients' => ['bail','sometimes', 'array', new ValidClientIds()], ]; return $rules; } + + public function prepareForValidation() + { + + $input = $this->all(); + + if (array_key_exists('next_run', $input) && is_string($input['next_run'])) + $this->merge(['next_run_client' => $input['next_run']]); + + return $input; + } + + } diff --git a/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php b/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php index 7e3ec3267152..af335500c98e 100644 --- a/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php +++ b/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php @@ -32,7 +32,8 @@ class UpdateSchedulerRequest extends Request 'name' => ['bail', 'sometimes', Rule::unique('schedulers')->where('company_id', auth()->user()->company()->id)->ignore($this->task_scheduler->id)], 'is_paused' => 'bail|sometimes|boolean', 'frequency_id' => 'bail|required|integer|digits_between:1,12', - 'next_run' => 'bail|required|date:Y-m-d', + 'next_run' => 'bail|required|date:Y-m-d|after_or_equal:today', + 'next_run_client' => 'bail|sometimes|date:Y-m-d', 'template' => 'bail|required|string', 'parameters' => 'bail|array', ]; @@ -40,4 +41,17 @@ class UpdateSchedulerRequest extends Request return $rules; } + + public function prepareForValidation() + { + + $input = $this->all(); + + if (array_key_exists('next_run', $input) && is_string($input['next_run'])) + $this->merge(['next_run_client' => $input['next_run']]); + + return $input; + + } + } diff --git a/app/Http/ValidationRules/Scheduler/ValidClientIds.php b/app/Http/ValidationRules/Scheduler/ValidClientIds.php new file mode 100644 index 000000000000..cedd7bba0cc8 --- /dev/null +++ b/app/Http/ValidationRules/Scheduler/ValidClientIds.php @@ -0,0 +1,43 @@ +user()->company()->id)->whereIn('id', $this->transformKeys($value))->count() == count($value); + + } + + /** + * @return string + */ + public function message() + { + return 'Invalid client ids'; + } +} diff --git a/app/Models/Company.php b/app/Models/Company.php index 0afac617a2ee..33f3f01046d7 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -649,6 +649,24 @@ class Company extends BaseModel return $data; } + public function timezone_offset() + { + $offset = 0; + + $entity_send_time = $this->getSetting('entity_send_time'); + + if ($entity_send_time == 0) { + return 0; + } + + $timezone = $this->timezone(); + + $offset -= $timezone->utc_offset; + $offset += ($entity_send_time * 3600); + + return $offset; + } + public function translate_entity() { return ctrans('texts.company'); @@ -666,4 +684,5 @@ class Company extends BaseModel return $item->id == $this->getSetting('date_format_id'); })->first()->format; } + } diff --git a/app/Models/RecurringInvoice.php b/app/Models/RecurringInvoice.php index a94c33d9aaed..167cd5a91ff2 100644 --- a/app/Models/RecurringInvoice.php +++ b/app/Models/RecurringInvoice.php @@ -257,13 +257,6 @@ 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 - to add ON a day - a day = 86400 seconds - */ - // if($offset < 0) - // $offset += 86400; - switch ($this->frequency_id) { case self::FREQUENCY_DAILY: return Carbon::parse($this->next_send_date_client)->startOfDay()->addDay()->addSeconds($offset); diff --git a/app/Models/Scheduler.php b/app/Models/Scheduler.php index 50c693cee6af..cc82ebb551ef 100644 --- a/app/Models/Scheduler.php +++ b/app/Models/Scheduler.php @@ -38,7 +38,7 @@ class Scheduler extends BaseModel 'name', 'frequency_id', 'next_run', - 'scheduled_run', + 'next_run_client', 'template', 'is_paused', 'parameters', @@ -46,6 +46,7 @@ class Scheduler extends BaseModel protected $casts = [ 'next_run' => 'datetime', + 'next_run_client' => 'datetime', 'created_at' => 'timestamp', 'updated_at' => 'timestamp', 'deleted_at' => 'timestamp', @@ -66,7 +67,7 @@ class Scheduler extends BaseModel return new SchedulerService($this); } - public function company(): \Illuminate\Database\Eloquent\Relations\BelongsTo + public function company() { return $this->belongsTo(Company::class); } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index bc53eb4067a8..ad099a102351 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -24,8 +24,10 @@ use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Mail\Mailer; use Illuminate\Queue\Events\JobProcessing; use Illuminate\Support\Facades\App; +use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Mail; +use Illuminate\Support\Facades\ParallelTesting; use Illuminate\Support\Facades\Queue; use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Facades\Schema; @@ -102,6 +104,11 @@ class AppServiceProvider extends ServiceProvider return $this; }); + + ParallelTesting::setUpTestDatabase(function ($database, $token) { + Artisan::call('db:seed'); + }); + } /** diff --git a/app/Services/Client/Statement.php b/app/Services/Client/Statement.php index 9b0d52e84ab9..8c7d957a1183 100644 --- a/app/Services/Client/Statement.php +++ b/app/Services/Client/Statement.php @@ -27,6 +27,7 @@ use App\Utils\Number; use App\Utils\PhantomJS\Phantom; use App\Utils\Traits\Pdf\PdfMaker as PdfMakerTrait; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\DB; class Statement { @@ -117,7 +118,7 @@ class Statement } if (\is_null($this->entity)) { - \DB::connection(config('database.default'))->beginTransaction(); + DB::connection(config('database.default'))->beginTransaction(); $this->rollback = true; diff --git a/app/Services/Scheduler/SchedulerService.php b/app/Services/Scheduler/SchedulerService.php index c149142dfeac..40de48ef3e6c 100644 --- a/app/Services/Scheduler/SchedulerService.php +++ b/app/Services/Scheduler/SchedulerService.php @@ -12,9 +12,11 @@ namespace App\Services\Scheduler; use App\Models\Client; +use App\Models\RecurringInvoice; use App\Models\Scheduler; use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesHash; +use Carbon\Carbon; class SchedulerService { @@ -51,18 +53,25 @@ class SchedulerService ->each(function ($_client){ $this->client = $_client; - $statement_properties = $this->calculateStatementProperties(); //work out the date range - $pdf = $_client->service()->statement($statement_properties,true); + $statement_properties = $this->calculateStatementProperties(); - //calculate next run dates; + $_client->service()->statement($statement_properties,true); }); + //calculate next run dates; + $this->calculateNextRun(); + } - private function calculateStatementProperties() + /** + * Hydrates the array needed to generate the statement + * + * @return array The statement options array + */ + private function calculateStatementProperties(): array { $start_end = $this->calculateStartAndEndDates(); @@ -76,7 +85,12 @@ class SchedulerService } - private function calculateStartAndEndDates() + /** + * Start and end date of the statement + * + * @return array [$start_date, $end_date]; + */ + private function calculateStartAndEndDates(): array { return match ($this->scheduler->parameters['date_range']) { 'this_month' => [now()->firstOfMonth()->format('Y-m-d'), now()->lastOfMonth()->format('Y-m-d')], @@ -91,6 +105,67 @@ class SchedulerService } + /** + * Sets the next run date of the scheduled task + * + */ + private function calculateNextRun() + { + if (! $this->scheduler->next_run) { + return null; + } + + $offset = $this->scheduler->company->timezone_offset(); + + switch ($this->scheduler->frequency_id) { + case RecurringInvoice::FREQUENCY_DAILY: + $next_run = Carbon::parse($this->scheduler->next_run)->startOfDay()->addDay(); + break; + case RecurringInvoice::FREQUENCY_WEEKLY: + $next_run = Carbon::parse($this->scheduler->next_run)->startOfDay()->addWeek(); + break; + case RecurringInvoice::FREQUENCY_TWO_WEEKS: + $next_run = Carbon::parse($this->scheduler->next_run)->startOfDay()->addWeeks(2); + break; + case RecurringInvoice::FREQUENCY_FOUR_WEEKS: + $next_run = Carbon::parse($this->scheduler->next_run)->startOfDay()->addWeeks(4); + break; + case RecurringInvoice::FREQUENCY_MONTHLY: + $next_run = Carbon::parse($this->scheduler->next_run)->startOfDay()->addMonthNoOverflow(); + break; + case RecurringInvoice::FREQUENCY_TWO_MONTHS: + $next_run = Carbon::parse($this->scheduler->next_run)->startOfDay()->addMonthsNoOverflow(2); + break; + case RecurringInvoice::FREQUENCY_THREE_MONTHS: + $next_run = Carbon::parse($this->scheduler->next_run)->startOfDay()->addMonthsNoOverflow(3); + break; + case RecurringInvoice::FREQUENCY_FOUR_MONTHS: + $next_run = Carbon::parse($this->scheduler->next_run)->startOfDay()->addMonthsNoOverflow(4); + break; + case RecurringInvoice::FREQUENCY_SIX_MONTHS: + $next_run = Carbon::parse($this->scheduler->next_run)->startOfDay()->addMonthsNoOverflow(6); + break; + case RecurringInvoice::FREQUENCY_ANNUALLY: + $next_run = Carbon::parse($this->scheduler->next_run)->startOfDay()->addYear(); + break; + case RecurringInvoice::FREQUENCY_TWO_YEARS: + $next_run = Carbon::parse($this->scheduler->next_run)->startOfDay()->addYears(2); + break; + case RecurringInvoice::FREQUENCY_THREE_YEARS: + $next_run = Carbon::parse($this->scheduler->next_run)->startOfDay()->addYears(3); + break; + default: + $next_run = null; + } + + + $this->scheduler->next_run_client = $next_run ?: null; + $this->scheduler->next_run = $next_run ? $next_run->copy()->addSeconds($offset) : null; + $this->scheduler->save(); + + } + + //handle when the scheduler has been paused. } \ No newline at end of file diff --git a/app/Transformers/SchedulerTransformer.php b/app/Transformers/SchedulerTransformer.php index 584ea4cbe9f9..3324cac4fddd 100644 --- a/app/Transformers/SchedulerTransformer.php +++ b/app/Transformers/SchedulerTransformer.php @@ -24,7 +24,7 @@ class SchedulerTransformer extends EntityTransformer 'id' => $this->encodePrimaryKey($scheduler->id), 'name' => (string) $scheduler->name, 'frequency_id' => (string) $scheduler->frequency_id, - 'next_run' => $scheduler->next_run, + 'next_run' => $scheduler->next_run_client->format('Y-m-d'), 'template' => (string) $scheduler->template, 'is_paused' => (bool) $scheduler->is_paused, 'is_deleted' => (bool) $scheduler->is_deleted, diff --git a/database/factories/SchedulerFactory.php b/database/factories/SchedulerFactory.php index 92c86d8ad8ce..e60a53211da3 100644 --- a/database/factories/SchedulerFactory.php +++ b/database/factories/SchedulerFactory.php @@ -31,6 +31,7 @@ class SchedulerFactory extends Factory 'parameters' => [], 'frequency_id' => RecurringInvoice::FREQUENCY_MONTHLY, 'next_run' => now()->addSeconds(rand(86400,8640000)), + 'next_run_client' => now()->addSeconds(rand(86400,8640000)), 'template' => 'statement_task', ]; } diff --git a/tests/Feature/CompanyTest.php b/tests/Feature/CompanyTest.php index 255d51bdff76..418321dc16bc 100644 --- a/tests/Feature/CompanyTest.php +++ b/tests/Feature/CompanyTest.php @@ -47,6 +47,42 @@ class CompanyTest extends TestCase $this->makeTestData(); } + public function testUpdateCompanyPropertyInvoiceTaskHours() + { + + $company_update = [ + 'invoice_task_hours' => true + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->putJson('/api/v1/companies/'.$this->encodePrimaryKey($this->company->id), $company_update) + ->assertStatus(200); + + + $arr = $response->json(); + + $this->assertTrue($arr['data']['invoice_task_hours']); + + + $company_update = [ + 'invoice_task_hours' => false + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->putJson('/api/v1/companies/'.$this->encodePrimaryKey($this->company->id), $company_update) + ->assertStatus(200); + + + $arr = $response->json(); + + $this->assertFalse($arr['data']['invoice_task_hours']); + + } + public function testCompanyList() { $this->withoutMiddleware(PasswordProtection::class); diff --git a/tests/Feature/Scheduler/SchedulerTest.php b/tests/Feature/Scheduler/SchedulerTest.php index f9eff751acf9..a743e7a1fcbb 100644 --- a/tests/Feature/Scheduler/SchedulerTest.php +++ b/tests/Feature/Scheduler/SchedulerTest.php @@ -11,21 +11,24 @@ namespace Tests\Feature\Scheduler; -use App\Export\CSV\ClientExport; +use App\Factory\SchedulerFactory; +use App\Models\Client; use App\Models\RecurringInvoice; -use App\Models\Scheduler; +use App\Services\Scheduler\SchedulerService; use App\Utils\Traits\MakesHash; use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; -use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\WithoutEvents; use Illuminate\Routing\Middleware\ThrottleRequests; use Illuminate\Support\Facades\Session; use Illuminate\Validation\ValidationException; use Tests\MockAccountData; -use Tests\MockUnitData; use Tests\TestCase; +/** + * @test + * @covers App\Services\Scheduler\SchedulerService + */ class SchedulerTest extends TestCase { use MakesHash; @@ -48,9 +51,217 @@ class SchedulerTest extends TestCase ThrottleRequests::class ); - $this->withoutExceptionHandling(); } + public function testClientsValidationInScheduledTask() + { + + $c = Client::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'number' => rand(1000,100000), + 'name' => 'A fancy client' + ]); + + $c2 = Client::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'number' => rand(1000,100000), + 'name' => 'A fancy client' + ]); + + $data = [ + 'name' => 'A test statement scheduler', + 'frequency_id' => RecurringInvoice::FREQUENCY_MONTHLY, + 'next_run' => now()->format('Y-m-d'), + 'template' => 'client_statement', + 'parameters' => [ + 'date_range' => 'previous_month', + 'show_payments_table' => true, + 'show_aging_table' => true, + 'status' => 'paid', + 'clients' => [ + $c2->hashed_id, + $c->hashed_id + ], + ], + ]; + + $response = false; + + try{ + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/task_schedulers', $data); + + } catch (ValidationException $e) { + $message = json_decode($e->validator->getMessageBag(), 1); + nlog($message); + } + + $response->assertStatus(200); + + + $data = [ + 'name' => 'A single Client', + 'frequency_id' => RecurringInvoice::FREQUENCY_MONTHLY, + 'next_run' => now()->addDay()->format('Y-m-d'), + 'template' => 'client_statement', + 'parameters' => [ + 'date_range' => 'previous_month', + 'show_payments_table' => true, + 'show_aging_table' => true, + 'status' => 'paid', + 'clients' => [ + $c2->hashed_id, + ], + ], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/task_schedulers', $data); + + $response->assertStatus(200); + + + $data = [ + 'name' => 'An invalid Client', + 'frequency_id' => RecurringInvoice::FREQUENCY_MONTHLY, + 'next_run' => now()->format('Y-m-d'), + 'template' => 'client_statement', + 'parameters' => [ + 'date_range' => 'previous_month', + 'show_payments_table' => true, + 'show_aging_table' => true, + 'status' => 'paid', + 'clients' => [ + 'xx33434', + ], + ], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/task_schedulers', $data); + + $response->assertStatus(422); + + + } + + + public function testCalculateNextRun() + { + + $scheduler = SchedulerFactory::create($this->company->id, $this->user->id); + + $data = [ + 'name' => 'A test statement scheduler', + 'frequency_id' => RecurringInvoice::FREQUENCY_MONTHLY, + 'next_run' => "2023-01-01", + 'template' => 'client_statement', + 'parameters' => [ + 'date_range' => 'previous_month', + 'show_payments_table' => true, + 'show_aging_table' => true, + 'status' => 'paid', + 'clients' => [], + ], + ]; + + $scheduler->fill($data); + $scheduler->save(); + + $service_object = new SchedulerService($scheduler); + + $reflectionMethod = new \ReflectionMethod(SchedulerService::class, 'calculateNextRun'); + $reflectionMethod->setAccessible(true); + $method = $reflectionMethod->invoke(new SchedulerService($scheduler)); + + $scheduler->fresh(); + + $this->assertEquals("2023-02-01", $scheduler->next_run->format('Y-m-d')); + + } + + public function testCalculateStartAndEndDates() + { + + $scheduler = SchedulerFactory::create($this->company->id, $this->user->id); + + $data = [ + 'name' => 'A test statement scheduler', + 'frequency_id' => RecurringInvoice::FREQUENCY_MONTHLY, + 'next_run' => "2023-01-01", + 'template' => 'client_statement', + 'parameters' => [ + 'date_range' => 'previous_month', + 'show_payments_table' => true, + 'show_aging_table' => true, + 'status' => 'paid', + 'clients' => [], + ], + ]; + + $scheduler->fill($data); + $scheduler->save(); + + $service_object = new SchedulerService($scheduler); + + $reflectionMethod = new \ReflectionMethod(SchedulerService::class, 'calculateStartAndEndDates'); + $reflectionMethod->setAccessible(true); + $method = $reflectionMethod->invoke(new SchedulerService($scheduler)); + + $this->assertIsArray($method); + + $this->assertEquals('previous_month', $scheduler->parameters['date_range']); + + $this->assertEqualsCanonicalizing(['2022-12-01','2022-12-31'], $method); + + } + + public function testCalculateStatementProperties() + { + + $scheduler = SchedulerFactory::create($this->company->id, $this->user->id); + + $data = [ + 'name' => 'A test statement scheduler', + 'frequency_id' => RecurringInvoice::FREQUENCY_MONTHLY, + 'next_run' => now()->format('Y-m-d'), + 'template' => 'client_statement', + 'parameters' => [ + 'date_range' => 'previous_month', + 'show_payments_table' => true, + 'show_aging_table' => true, + 'status' => 'paid', + 'clients' => [], + ], + ]; + + $scheduler->fill($data); + $scheduler->save(); + + $service_object = new SchedulerService($scheduler); + + // $reflection = new \ReflectionClass(get_class($service_object)); + // $method = $reflection->getMethod('calculateStatementProperties'); + // $method->setAccessible(true); + // $method->invokeArgs($service_object, []); + + $reflectionMethod = new \ReflectionMethod(SchedulerService::class, 'calculateStatementProperties'); + $reflectionMethod->setAccessible(true); + $method = $reflectionMethod->invoke(new SchedulerService($scheduler)); // 'baz' + + $this->assertIsArray($method); + + $this->assertEquals('paid', $method['status']); + + } public function testGetThisMonthRange() { @@ -82,24 +293,15 @@ class SchedulerTest extends TestCase }; } - /** - * 'name' => ['bail', 'required', Rule::unique('schedulers')->where('company_id', auth()->user()->company()->id)], - 'is_paused' => 'bail|sometimes|boolean', - 'frequency_id' => 'bail|required|integer|digits_between:1,12', - 'next_run' => 'bail|required|date:Y-m-d', - 'template' => 'bail|required|string', - 'parameters' => 'bail|array', - */ - public function testClientStatementGeneration() { $data = [ 'name' => 'A test statement scheduler', 'frequency_id' => RecurringInvoice::FREQUENCY_MONTHLY, - 'next_run' => '2023-01-14', + 'next_run' => now()->format('Y-m-d'), 'template' => 'client_statement', 'parameters' => [ - 'date_range' => 'last_month', + 'date_range' => 'previous_month', 'show_payments_table' => true, 'show_aging_table' => true, 'status' => 'paid', @@ -190,7 +392,7 @@ class SchedulerTest extends TestCase 'name' => 'A different Name', 'frequency_id' => 5, 'next_run' => now()->addDays(2)->format('Y-m-d'), - 'template' =>'statement', + 'template' =>'client_statement', 'parameters' => [], ]; @@ -209,7 +411,7 @@ class SchedulerTest extends TestCase 'name' => 'A different Name', 'frequency_id' => 5, 'next_run' => now()->addDays(2)->format('Y-m-d'), - 'template' =>'statement', + 'template' =>'client_statement', 'parameters' => [], ]; diff --git a/tests/Unit/ClientSettingsTest.php b/tests/Unit/ClientSettingsTest.php index f7ca65ca4eb1..befc88b2d62b 100644 --- a/tests/Unit/ClientSettingsTest.php +++ b/tests/Unit/ClientSettingsTest.php @@ -11,7 +11,6 @@ namespace Tests\Unit; -use App\DataMapper\ClientSettings; use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Validation\ValidationException; use Tests\MockAccountData;