diff --git a/app/Http/Controllers/ExpenseController.php b/app/Http/Controllers/ExpenseController.php index 09653dfb5b..00d97ad933 100644 --- a/app/Http/Controllers/ExpenseController.php +++ b/app/Http/Controllers/ExpenseController.php @@ -11,7 +11,14 @@ namespace App\Http\Controllers; +use App\Factory\ExpenseFactory; use App\Filters\ExpenseFilters; +use App\Http\Requests\Expense\CreateExpenseRequest; +use App\Http\Requests\Expense\DestroyExpenseRequest; +use App\Http\Requests\Expense\EditExpenseRequest; +use App\Http\Requests\Expense\ShowExpenseRequest; +use App\Http\Requests\Expense\StoreExpenseRequest; +use App\Http\Requests\Expense\UpdateExpenseRequest; use App\Jobs\Entity\ActionEntity; use App\Jobs\Util\ProcessBulk; use App\Jobs\Util\UploadAvatar; @@ -266,7 +273,7 @@ class ExpenseController extends BaseController return $request->disallowUpdate(); } - $expense = $this->client_repo->save($request->all(), $expense); + $expense = $this->expense_repo->save($request->all(), $expense); $this->uploadLogo($request->file('company_logo'), $expense->company, $expense); @@ -359,11 +366,7 @@ class ExpenseController extends BaseController */ public function store(StoreExpenseRequest $request) { - $expense = $this->client_repo->save($request->all(), ExpenseFactory::create(auth()->user()->company()->id, auth()->user()->id)); - - $expense->load('contacts', 'primary_contact'); - - $this->uploadLogo($request->file('company_logo'), $expense->company, $expense); + $expense = $this->expense_repo->save($request->all(), ExpenseFactory::create(auth()->user()->company()->id, auth()->user()->id)); return $this->itemResponse($expense); } @@ -485,7 +488,7 @@ class ExpenseController extends BaseController $expenses->each(function ($expense, $key) use ($action) { if (auth()->user()->can('edit', $expense)) { - $this->client_repo->{$action}($expense); + $this->expense_repo->{$action}($expense); } }); diff --git a/app/Http/Requests/Expense/BulkExpenseRequest.php b/app/Http/Requests/Expense/BulkExpenseRequest.php new file mode 100644 index 0000000000..ac0fade159 --- /dev/null +++ b/app/Http/Requests/Expense/BulkExpenseRequest.php @@ -0,0 +1,47 @@ +has('action')) { + return false; + } + + if (! in_array($this->action, $this->getBulkOptions(), true)) { + return false; + } + + return auth()->user()->can(auth()->user()->isAdmin(), Expense::class); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + $rules = $this->getGlobalRules(); + + /* We don't require IDs on bulk storing. */ + if ($this->action !== self::$STORE_METHOD) { + $rules['ids'] = ['required']; + } + + return $rules; + } +} diff --git a/app/Http/Requests/Expense/CreateExpenseRequest.php b/app/Http/Requests/Expense/CreateExpenseRequest.php new file mode 100644 index 0000000000..12f5667420 --- /dev/null +++ b/app/Http/Requests/Expense/CreateExpenseRequest.php @@ -0,0 +1,28 @@ +user()->can('create', Expense::class); + } +} diff --git a/app/Http/Requests/Expense/DestroyExpenseRequest.php b/app/Http/Requests/Expense/DestroyExpenseRequest.php new file mode 100644 index 0000000000..c5ba3194f3 --- /dev/null +++ b/app/Http/Requests/Expense/DestroyExpenseRequest.php @@ -0,0 +1,28 @@ +user()->can('edit', $this->expense); + } +} diff --git a/app/Http/Requests/Expense/EditExpenseRequest.php b/app/Http/Requests/Expense/EditExpenseRequest.php new file mode 100644 index 0000000000..25e4fabba5 --- /dev/null +++ b/app/Http/Requests/Expense/EditExpenseRequest.php @@ -0,0 +1,38 @@ +user()->can('edit', $this->expense); + } + + // public function prepareForValidation() + // { + // $input = $this->all(); + + // //$input['id'] = $this->encodePrimaryKey($input['id']); + + // $this->replace($input); + + // } +} diff --git a/app/Http/Requests/Expense/ShowExpenseRequest.php b/app/Http/Requests/Expense/ShowExpenseRequest.php new file mode 100644 index 0000000000..bf636c1c4a --- /dev/null +++ b/app/Http/Requests/Expense/ShowExpenseRequest.php @@ -0,0 +1,28 @@ +user()->can('view', $this->expense); + } +} diff --git a/app/Http/Requests/Expense/StoreExpenseRequest.php b/app/Http/Requests/Expense/StoreExpenseRequest.php new file mode 100644 index 0000000000..22aee8ca7c --- /dev/null +++ b/app/Http/Requests/Expense/StoreExpenseRequest.php @@ -0,0 +1,76 @@ +user()->can('create', Expense::class); + } + + public function rules() + { + + /* Ensure we have a client name, and that all emails are unique*/ + //$rules['name'] = 'required|min:1'; + $rules['id_number'] = 'unique:expenses,id_number,'.$this->id.',id,company_id,'.$this->company_id; + //$rules['settings'] = new ValidExpenseGroupSettingsRule(); + $rules['contacts.*.email'] = 'nullable|distinct'; + + $rules['number'] = new UniqueExpenseNumberRule($this->all()); + + // $contacts = request('contacts'); + + // if (is_array($contacts)) { + // for ($i = 0; $i < count($contacts); $i++) { + + // //$rules['contacts.' . $i . '.email'] = 'nullable|email|distinct'; + // } + // } + + return $rules; + } + + protected function prepareForValidation() + { + // $input = $this->all(); + + + // $this->replace($input); + } + + public function messages() + { + return [ + 'unique' => ctrans('validation.unique', ['attribute' => 'email']), + //'required' => trans('validation.required', ['attribute' => 'email']), + 'contacts.*.email.required' => ctrans('validation.email', ['attribute' => 'email']), + ]; + } +} diff --git a/app/Http/Requests/Expense/UpdateExpenseRequest.php b/app/Http/Requests/Expense/UpdateExpenseRequest.php new file mode 100644 index 0000000000..62ca15c72b --- /dev/null +++ b/app/Http/Requests/Expense/UpdateExpenseRequest.php @@ -0,0 +1,77 @@ +user()->can('edit', $this->expense); + } + + public function rules() + { + /* Ensure we have a client name, and that all emails are unique*/ + + $rules['country_id'] = 'integer|nullable'; + //$rules['id_number'] = 'unique:clients,id_number,,id,company_id,' . auth()->user()->company()->id; + $rules['contacts.*.email'] = 'nullable|distinct'; + + if ($this->input('number')) { + $rules['number'] = 'unique:expenses,number,'.$this->id.',id,company_id,'.$this->expense->company_id; + } + + $contacts = request('contacts'); + + if (is_array($contacts)) { + // for ($i = 0; $i < count($contacts); $i++) { + // // $rules['contacts.' . $i . '.email'] = 'nullable|email|unique:client_contacts,email,' . isset($contacts[$i]['id'].',company_id,'.$this->company_id); + // //$rules['contacts.' . $i . '.email'] = 'nullable|email'; + // } + } + + return $rules; + } + + public function messages() + { + return [ + 'unique' => ctrans('validation.unique', ['attribute' => 'email']), + 'email' => ctrans('validation.email', ['attribute' => 'email']), + 'name.required' => ctrans('validation.required', ['attribute' => 'name']), + 'required' => ctrans('validation.required', ['attribute' => 'email']), + ]; + } + + protected function prepareForValidation() + { + $input = $this->all(); + + $this->replace($input); + } +} diff --git a/app/Http/ValidationRules/Expense/UniqueExpenseNumberRule.php b/app/Http/ValidationRules/Expense/UniqueExpenseNumberRule.php new file mode 100644 index 0000000000..4dfbde5e22 --- /dev/null +++ b/app/Http/ValidationRules/Expense/UniqueExpenseNumberRule.php @@ -0,0 +1,69 @@ +input = $input; + } + + /** + * @param string $attribute + * @param mixed $value + * @return bool + */ + public function passes($attribute, $value) + { + return $this->checkIfExpenseNumberUnique(); //if it exists, return false! + } + + /** + * @return string + */ + public function message() + { + return 'Expense number already taken'; + } + + /** + * @param $email + * + * //off,when_sent,when_paid + * + * @return bool + */ + private function checkIfExpenseNumberUnique() : bool + { + $expense = Expense::where('client_id', $this->input['client_id']) + ->where('number', $this->input['number']) + ->withTrashed() + ->exists(); + + if ($expense) { + return false; + } + + return true; + } +} diff --git a/tests/Feature/ExpenseApiTest.php b/tests/Feature/ExpenseApiTest.php new file mode 100644 index 0000000000..f31b7eb500 --- /dev/null +++ b/tests/Feature/ExpenseApiTest.php @@ -0,0 +1,152 @@ +makeTestData(); + + Session::start(); + + $this->faker = \Faker\Factory::create(); + + Model::reguard(); + } + + public function testExpensePost() + { + $data = [ + 'public_notes' => $this->faker->firstName, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/expenses', $data); + + $response->assertStatus(200); + } + + public function testExpensePut() + { + $data = [ + 'public_notes' => $this->faker->firstName, + 'id_number' => 'Coolio', + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->put('/api/v1/expenses/'.$this->encodePrimaryKey($this->expense->id), $data); + + $response->assertStatus(200); + } + + public function testExpenseGet() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/expenses/'.$this->encodePrimaryKey($this->expense->id)); + + $response->assertStatus(200); + } + + public function testExpenseNotArchived() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/expenses/'.$this->encodePrimaryKey($this->expense->id)); + + $arr = $response->json(); + + $this->assertEquals(0, $arr['data']['archived_at']); + } + + public function testExpenseArchived() + { + $data = [ + 'ids' => [$this->encodePrimaryKey($this->expense->id)], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/expenses/bulk?action=archive', $data); + + $arr = $response->json(); + + $this->assertNotNull($arr['data'][0]['archived_at']); + } + + public function testExpenseRestored() + { + $data = [ + 'ids' => [$this->encodePrimaryKey($this->expense->id)], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/expenses/bulk?action=restore', $data); + + $arr = $response->json(); + + $this->assertEquals(0, $arr['data'][0]['archived_at']); + } + + public function testExpenseDeleted() + { + $data = [ + 'ids' => [$this->encodePrimaryKey($this->expense->id)], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/expenses/bulk?action=delete', $data); + + $arr = $response->json(); + + $this->assertTrue($arr['data'][0]['is_deleted']); + } +} diff --git a/tests/MockAccountData.php b/tests/MockAccountData.php index c79c756d75..a7d99c0266 100644 --- a/tests/MockAccountData.php +++ b/tests/MockAccountData.php @@ -67,6 +67,8 @@ trait MockAccountData public $vendor; + public $expense; + public function makeTestData() { @@ -151,7 +153,7 @@ trait MockAccountData $this->client = factory(\App\Models\Client::class)->create([ 'user_id' => $this->user->id, 'company_id' => $this->company->id, - ]); + ]); $contact = factory(\App\Models\ClientContact::class)->create([ 'user_id' => $this->user->id, @@ -159,14 +161,14 @@ trait MockAccountData 'company_id' => $this->company->id, 'is_primary' => 1, 'send_email' => true, - ]); + ]); $contact2 = factory(\App\Models\ClientContact::class)->create([ 'user_id' => $this->user->id, 'client_id' => $this->client->id, 'company_id' => $this->company->id, 'send_email' => true, - ]); + ]); $this->vendor = factory(\App\Models\Vendor::class)->create([ 'user_id' => $this->user->id, @@ -179,14 +181,19 @@ trait MockAccountData 'company_id' => $this->company->id, 'is_primary' => 1, 'send_email' => true, - ]); + ]); $vendor_contact2 = factory(\App\Models\VendorContact::class)->create([ 'user_id' => $this->user->id, 'vendor_id' => $this->vendor->id, 'company_id' => $this->company->id, 'send_email' => true, - ]); + ]); + + $this->expense = factory(\App\Models\Expense::class)->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + ]); // $rels = collect($contact, $contact2); // $this->client->setRelation('contacts', $rels);