From 85c0dbe0e456073072787e755c13d95a8bcba5cc Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 23 Nov 2022 10:01:37 +1100 Subject: [PATCH] Inovice tasks lockijng --- app/Exceptions/Handler.php | 2 +- app/Http/Requests/Task/UpdateTaskRequest.php | 12 +++ app/Models/Company.php | 1 + app/Models/Task.php | 1 + app/Transformers/CompanyTransformer.php | 1 + app/Transformers/TaskTransformer.php | 2 +- ..._11_22_215618_lock_tasks_when_invoiced.php | 41 +++++++++ lang/en/texts.php | 5 ++ tests/Feature/TaskApiTest.php | 88 ++++++++++++++++++- 9 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 database/migrations/2022_11_22_215618_lock_tasks_when_invoiced.php diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 9e439a815e..95f41705f6 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -182,7 +182,7 @@ class Handler extends ExceptionHandler } elseif ($exception instanceof FatalThrowableError && $request->expectsJson()) { return response()->json(['message'=>'Fatal error'], 500); } elseif ($exception instanceof AuthorizationException) { - return response()->json(['message'=>'You are not authorized to view or perform this action'], 401); + return response()->json(['message'=> $exception->getMessage()], 401); } elseif ($exception instanceof TokenMismatchException) { return redirect() ->back() diff --git a/app/Http/Requests/Task/UpdateTaskRequest.php b/app/Http/Requests/Task/UpdateTaskRequest.php index 7e4babdb3f..3b4bf5416c 100644 --- a/app/Http/Requests/Task/UpdateTaskRequest.php +++ b/app/Http/Requests/Task/UpdateTaskRequest.php @@ -15,6 +15,7 @@ use App\Http\Requests\Request; use App\Models\Project; use App\Utils\Traits\ChecksEntityStatus; use App\Utils\Traits\MakesHash; +use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Validation\Rule; class UpdateTaskRequest extends Request @@ -29,6 +30,10 @@ class UpdateTaskRequest extends Request */ public function authorize() : bool { + //prevent locked tasks from updating + if($this->task->invoice_lock && $this->task->invoice_id) + return false; + return auth()->user()->can('edit', $this->task); } @@ -87,4 +92,11 @@ class UpdateTaskRequest extends Request $this->replace($input); } + + + protected function failedAuthorization() + { + throw new AuthorizationException(ctrans('texts.task_update_authorization_error')); + } + } diff --git a/app/Models/Company.php b/app/Models/Company.php index d878ee575b..181f4dcf5a 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -123,6 +123,7 @@ class Company extends BaseModel 'enabled_expense_tax_rates', 'invoice_task_project', 'report_include_deleted', + 'invoice_task_lock', ]; protected $hidden = [ diff --git a/app/Models/Task.php b/app/Models/Task.php index 4e463e575c..024bba00c1 100644 --- a/app/Models/Task.php +++ b/app/Models/Task.php @@ -40,6 +40,7 @@ class Task extends BaseModel 'number', 'is_date_based', 'status_order', + 'invoice_lock' ]; protected $touches = []; diff --git a/app/Transformers/CompanyTransformer.php b/app/Transformers/CompanyTransformer.php index 45ef466f87..1cf9975f49 100644 --- a/app/Transformers/CompanyTransformer.php +++ b/app/Transformers/CompanyTransformer.php @@ -186,6 +186,7 @@ class CompanyTransformer extends EntityTransformer 'enabled_expense_tax_rates' => (int) $company->enabled_expense_tax_rates, 'invoice_task_project' => (bool) $company->invoice_task_project, 'report_include_deleted' => (bool) $company->report_include_deleted, + 'invoice_task_lock' => (bool) $company->invoice_task_lock, ]; } diff --git a/app/Transformers/TaskTransformer.php b/app/Transformers/TaskTransformer.php index 047d10ecc1..ed8b2700eb 100644 --- a/app/Transformers/TaskTransformer.php +++ b/app/Transformers/TaskTransformer.php @@ -72,7 +72,6 @@ class TaskTransformer extends EntityTransformer 'user_id' => (string) $this->encodePrimaryKey($task->user_id), 'assigned_user_id' => (string) $this->encodePrimaryKey($task->assigned_user_id), 'number' => (string) $task->number ?: '', - // 'start_time' => (int) $task->start_time, 'description' => (string) $task->description ?: '', 'duration' => (int) $task->duration ?: 0, 'rate' => (float) $task->rate ?: 0, @@ -93,6 +92,7 @@ class TaskTransformer extends EntityTransformer 'status_sort_order' => (int) $task->status_sort_order, //deprecated 5.0.34 'is_date_based' => (bool) $task->is_date_based, 'status_order' => is_null($task->status_order) ? null : (int) $task->status_order, + 'invoice_lock' => (bool) $task->invoice_lock, ]; } } diff --git a/database/migrations/2022_11_22_215618_lock_tasks_when_invoiced.php b/database/migrations/2022_11_22_215618_lock_tasks_when_invoiced.php new file mode 100644 index 0000000000..19f7f4b594 --- /dev/null +++ b/database/migrations/2022_11_22_215618_lock_tasks_when_invoiced.php @@ -0,0 +1,41 @@ +boolean('invoice_lock')->default(false); + }); + + Schema::table('companies', function (Blueprint $table) + { + $table->boolean('invoice_task_lock')->default(false); + }); + + Schema::table('bank_transactions', function (Blueprint $table) + { + $table->bigInteger('bank_rule_id')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +}; diff --git a/lang/en/texts.php b/lang/en/texts.php index 3817b0d651..9807483ad6 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -4843,6 +4843,11 @@ $LANG = array( 'refresh_accounts' => 'Refresh Accounts', 'upgrade_to_connect_bank_account' => 'Upgrade to Enterprise to connect your bank account', 'click_here_to_connect_bank_account' => 'Click here to connect your bank account', + 'task_update_authorization_error' => 'Insufficient permissions, or task may be locked', + 'cash_vs_accrual' => 'Accrual accounting', + 'cash_vs_accrual_help' => 'Turn on for accrual reporting, turn off for cash basis reporting.', + 'expense_paid_report' => 'Expensed reporting', + 'expense_paid_report_help' => 'Turn on for reporting all expense, turn off for reporting only paid expenses', ); return $LANG; diff --git a/tests/Feature/TaskApiTest.php b/tests/Feature/TaskApiTest.php index 0a7cd00545..b2bb253172 100644 --- a/tests/Feature/TaskApiTest.php +++ b/tests/Feature/TaskApiTest.php @@ -11,6 +11,7 @@ namespace Tests\Feature; +use App\Models\Task; use App\Utils\Traits\MakesHash; use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Testing\DatabaseTransactions; @@ -42,6 +43,90 @@ class TaskApiTest extends TestCase Model::reguard(); } + + public function testTaskLockingGate() + { + $data = [ + 'timelog' => [[1,2],[3,4]], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/tasks', $data); + + $arr = $response->json(); + $response->assertStatus(200); + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->putJson('/api/v1/tasks/' . $arr['data']['id'], $data); + + $arr = $response->json(); + + $response->assertStatus(200); + + $task = Task::find($this->decodePrimaryKey($arr['data']['id'])); + $task->invoice_id = $this->invoice->id; + $task->save(); + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->putJson('/api/v1/tasks/' . $arr['data']['id'], $data); + + $arr = $response->json(); + + $response->assertStatus(200); + + $task = Task::find($this->decodePrimaryKey($arr['data']['id'])); + $task->invoice_lock =true; + $task->invoice_id = $this->invoice->id; + $task->save(); + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->putJson('/api/v1/tasks/' . $arr['data']['id'], $data); + + $arr = $response->json(); + + $response->assertStatus(401); + + } + + + public function testTaskLocking() + { + $data = [ + 'timelog' => [[1,2],[3,4]], + 'invoice_lock' => true + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/tasks', $data); + + $arr = $response->json(); + $response->assertStatus(200); + + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->putJson('/api/v1/tasks/' . $arr['data']['id'], $data); + + $arr = $response->json(); + + $response->assertStatus(200); + + } + + + + public function testTimeLogValidation() { $data = [ @@ -75,9 +160,10 @@ class TaskApiTest extends TestCase $arr = $response->json(); $response->assertStatus(200); - } + + public function testTimeLogValidation2() { $data = [