diff --git a/app/Factory/BankTransactionFactory.php b/app/Factory/BankTransactionFactory.php new file mode 100644 index 000000000000..120bbae5fd3d --- /dev/null +++ b/app/Factory/BankTransactionFactory.php @@ -0,0 +1,36 @@ +account_id = $account_id; + $bank_transaction->user_id = $user_id; + $bank_transaction->company_id = $company_id; + + $bank_transaction->amount = 0; + $bank_transaction->currency_code = ''; + $bank_transaction->account_type = ''; + $bank_transaction->category_type = ''; + $bank_transaction->date = now()->format('Y-m-d'); + $bank_transaction->description = ''; + $bank_transaction->is_matched = 0; + + return $bank_transaction; + } +} diff --git a/app/Http/Controllers/BankIntegrationController.php b/app/Http/Controllers/BankIntegrationController.php index 827fafa1e5f1..9a1cd189f737 100644 --- a/app/Http/Controllers/BankIntegrationController.php +++ b/app/Http/Controllers/BankIntegrationController.php @@ -24,12 +24,14 @@ use App\Models\BankIntegration; use App\Repositories\BankIntegrationRepository; use App\Services\Bank\BankService; use App\Transformers\BankIntegrationTransformer; +use App\Utils\Traits\MakesHash; use Illuminate\Http\Request; class BankIntegrationController extends BaseController { - + use MakesHash; + protected $entity_type = BankIntegration::class; protected $entity_transformer = BankIntegrationTransformer::class; @@ -418,6 +420,80 @@ class BankIntegrationController extends BaseController return $this->itemResponse($bank_integration->fresh()); } + +/** + * Perform bulk actions on the list view. + * + * @return Collection + * + * @OA\Post( + * path="/api/v1/invoices/bulk", + * operationId="bulkInvoices", + * tags={"invoices"}, + * summary="Performs bulk actions on an array of invoices", + * description="", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/index"), + * @OA\RequestBody( + * description="User credentials", + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema( + * type="array", + * @OA\Items( + * type="integer", + * description="Array of hashed IDs to be bulk 'actioned", + * example="[0,1,2,3]", + * ), + * ) + * ) + * ), + * @OA\Response( + * response=200, + * description="The Bulk Action response", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function bulk() + { + $action = request()->input('action'); + + if(!in_array($action, ['archive', 'restore', 'delete'])) + return response()->json(['message' => 'Unsupported action.'], 400); + + $ids = request()->input('ids'); + + $bank_integrations = BankIntegration::withTrashed()->whereIn('id', $this->transformKeys($ids))->company()->get(); + + $bank_integrations->each(function ($bank_integration, $key) use ($action) { + if (auth()->user()->can('edit', $bank_integration)) { + $this->bank_integration_repo->{$action}($bank_integration); + } + }); + + /* Need to understand which permission are required for the given bulk action ie. view / edit */ + + return $this->listResponse(BankIntegration::withTrashed()->whereIn('id', $this->transformKeys($ids))->company()); + } + + /** * Return the remote list of accounts stored on the third part provider. * diff --git a/app/Policies/BankTransactionPolicy.php b/app/Policies/BankTransactionPolicy.php new file mode 100644 index 000000000000..00b57861aa6d --- /dev/null +++ b/app/Policies/BankTransactionPolicy.php @@ -0,0 +1,31 @@ +isAdmin(); + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 9f996a513d2f..9b9f779eeb9d 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -13,6 +13,8 @@ namespace App\Providers; use App\Models\Activity; use App\Models\Bank; +use App\Models\BankIntegration; +use App\Models\BankTransaction; use App\Models\Client; use App\Models\Company; use App\Models\CompanyGateway; @@ -42,6 +44,7 @@ use App\Models\Vendor; use App\Models\Webhook; use App\Policies\ActivityPolicy; use App\Policies\BankIntegrationPolicy; +use App\Policies\BankTransactionPolicy; use App\Policies\ClientPolicy; use App\Policies\CompanyGatewayPolicy; use App\Policies\CompanyPolicy; @@ -81,7 +84,8 @@ class AuthServiceProvider extends ServiceProvider */ protected $policies = [ Activity::class => ActivityPolicy::class, - Bank::class => BankIntegrationPolicy::class, + BankIntegration::class => BankIntegrationPolicy::class, + BankTransaction::class => BankTransactionPolicy::class, Client::class => ClientPolicy::class, Company::class => CompanyPolicy::class, CompanyToken::class => CompanyTokenPolicy::class, diff --git a/database/factories/BankIntegrationFactory.php b/database/factories/BankIntegrationFactory.php new file mode 100644 index 000000000000..ad588f872a84 --- /dev/null +++ b/database/factories/BankIntegrationFactory.php @@ -0,0 +1,41 @@ + $this->faker->company(), + 'provider_id' => 1, + 'bank_account_name' => $this->faker->catchPhrase(), + 'bank_account_id' => 1, + 'bank_account_number' => $this->faker->randomNumber(9, true), + 'bank_account_status' => 'active', + 'bank_account_type' => 'creditCard', + 'balance' => $this->faker->randomFloat(2, 10, 10000), + 'currency' => 'USD', + 'nickname' => $this->faker->word(), + 'is_deleted' => false, + ]; + } +} \ No newline at end of file diff --git a/database/factories/BankTransactionFactory.php b/database/factories/BankTransactionFactory.php new file mode 100644 index 000000000000..8bd2b1bdce2c --- /dev/null +++ b/database/factories/BankTransactionFactory.php @@ -0,0 +1,40 @@ + $this->faker->randomNumber(9, true) , + 'amount' => $this->faker->randomFloat(2,10,10000) , + 'currency_code' => 'USD', + 'account_type' => 'creditCard', + 'category_id' => 1, + 'category_type' => 'Random' , + 'date' => $this->faker->date('Y-m-d') , + 'bank_account_id' => 1 , + 'description' =>$this->faker->words(5, true) , + 'is_matched'=> false + ]; + } +} diff --git a/database/migrations/2022_08_05_023357_bank_integration.php b/database/migrations/2022_08_05_023357_bank_integration.php index 643a51156819..a61aa17be3c3 100644 --- a/database/migrations/2022_08_05_023357_bank_integration.php +++ b/database/migrations/2022_08_05_023357_bank_integration.php @@ -63,7 +63,13 @@ return new class extends Migration $table->date('date'); $table->unsignedBigInteger('bank_account_id'); $table->text('description'); + $table->unsignedInteger('invoice_id')->nullable(); + $table->unsignedInteger('expense_id')->nullable(); + $table->boolean('is_matched')->default(0); + $table->timestamps(6); + + $table->softDeletes('deleted_at', 6); $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade')->onUpdate('cascade'); $table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade')->onUpdate('cascade'); diff --git a/routes/api.php b/routes/api.php index abd18b002516..80543e649816 100644 --- a/routes/api.php +++ b/routes/api.php @@ -110,6 +110,7 @@ Route::group(['middleware' => ['throttle:300,1', 'api_db', 'token_auth', 'locale Route::post('bank_integrations/refresh_accounts', [BankIntegrationController::class, 'refreshAccounts'])->name('bank_integrations.refresh_accounts'); Route::post('bank_integrations/transactions', [BankIntegrationController::class, 'getTransactions'])->name('bank_integrations.transactions'); Route::post('bank_integrations/remove_account/{acc_id}', [BankIntegrationController::class, 'removeAccount'])->name('bank_integrations.remove_account'); + Route::post('bank_integrations/bulk', [BankIntegrationController::class, 'bulk'])->name('bank_integrations.bulk'); Route::post('check_subdomain', [SubdomainController::class, 'index'])->name('check_subdomain'); Route::get('ping', [PingController::class, 'index'])->name('ping'); diff --git a/tests/Feature/BankIntegrationApiTest.php b/tests/Feature/BankIntegrationApiTest.php new file mode 100644 index 000000000000..2b4f711b0ad3 --- /dev/null +++ b/tests/Feature/BankIntegrationApiTest.php @@ -0,0 +1,102 @@ +makeTestData(); + + Session::start(); + + $this->faker = \Faker\Factory::create(); + + Model::reguard(); + } + + public function testBankIntegrationGet() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/bank_integrations/'.$this->encodePrimaryKey($this->bank_integration->id)); + + $response->assertStatus(200); + } + + public function testBankIntegrationArchived() + { + $data = [ + 'ids' => [$this->encodePrimaryKey($this->bank_integration->id)], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/bank_integrations/bulk?action=archive', $data); + + $arr = $response->json(); + + $this->assertNotNull($arr['data'][0]['archived_at']); + } + + public function testBankIntegrationRestored() + { + $data = [ + 'ids' => [$this->encodePrimaryKey($this->bank_integration->id)], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/bank_integrations/bulk?action=restore', $data); + + $arr = $response->json(); + + $this->assertEquals(0, $arr['data'][0]['archived_at']); + } + + public function testBankIntegrationDeleted() + { + $data = [ + 'ids' => [$this->encodePrimaryKey($this->bank_integration->id)], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/bank_integrations/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 db38a80a05a4..3e3ea1b51d2e 100644 --- a/tests/MockAccountData.php +++ b/tests/MockAccountData.php @@ -24,6 +24,8 @@ use App\Factory\PurchaseOrderFactory; use App\Helpers\Invoice\InvoiceSum; use App\Jobs\Company\CreateCompanyTaskStatuses; use App\Models\Account; +use App\Models\BankIntegration; +use App\Models\BankTransaction; use App\Models\Client; use App\Models\ClientContact; use App\Models\Company; @@ -141,6 +143,16 @@ trait MockAccountData */ public $cu; + /** + * @var + */ + public $bank_integration; + + /** + * @var + */ + public $bank_transaction; + /** * @var */ @@ -524,6 +536,37 @@ trait MockAccountData 'credit_id' => $this->credit->id, ]); + $this->bank_integration = BankIntegration::factory()->create([ + 'user_id' => $user_id, + 'company_id' => $this->company->id, + 'account_id' => $this->account->id, + ]); + + $this->bank_transaction = BankTransaction::factory()->create([ + 'user_id' => $user_id, + 'company_id' => $this->company->id, + ]); + + BankTransaction::factory()->create([ + 'user_id' => $user_id, + 'company_id' => $this->company->id, + ]); + + BankTransaction::factory()->create([ + 'user_id' => $user_id, + 'company_id' => $this->company->id, + ]); + + BankTransaction::factory()->create([ + 'user_id' => $user_id, + 'company_id' => $this->company->id, + ]); + + BankTransaction::factory()->create([ + 'user_id' => $user_id, + 'company_id' => $this->company->id, + ]); + $invitations = CreditInvitation::whereCompanyId($this->credit->company_id) ->whereCreditId($this->credit->id);