diff --git a/app/Helpers/Bank/Nordigen/Nordigen.php b/app/Helpers/Bank/Nordigen/Nordigen.php index e0718e01fb5a..1e8e23ac0dfc 100644 --- a/app/Helpers/Bank/Nordigen/Nordigen.php +++ b/app/Helpers/Bank/Nordigen/Nordigen.php @@ -16,6 +16,8 @@ namespace App\Helpers\Bank\Nordigen; use App\Exceptions\NordigenApiException; use App\Helpers\Bank\Nordigen\Transformer\AccountTransformer; use App\Helpers\Bank\Nordigen\Transformer\IncomeTransformer; +use Log; +use Nordigen\NordigenPHP\Exceptions\NordigenExceptions\NordigenException; class Nordigen { @@ -31,6 +33,7 @@ class Nordigen $this->client = new \Nordigen\NordigenPHP\API\NordigenClient($secret_id, $secret_key); $this->client->createAccessToken(); // access_token is valid 24h -> so we dont have to implement a refresh-cycle + } // metadata-section for frontend @@ -56,52 +59,7 @@ class Nordigen return $this->client->requisition->getRequisition($requisitionId); } - // NOTE: this will only cleanup the requisitions from nordigen and not within the table: bank_integration_nordigen_requisitions - public function cleanupRequisitions() - { - $requisitions = $this->client->requisition->getRequisitions(); - - foreach ($requisitions as $requisition) { - // filter to expired OR older than 7 days created and no accounts - if ($requisition->status == "EXPIRED" || (sizeOf($requisition->accounts) != 0 && strtotime($requisition->created) > (new \DateTime())->modify('-7 days'))) - continue; - - $this->client->requisition->deleteRequisition($requisition->id); - } - } - - // account-section: these methods should be used to get data of connected accounts - public function getAccounts(?array $requisitionIds) - { - - // get all valid requisitions - $requisitions = $this->client->requisition->getRequisitions(); // no pagination used?! - - // fetch all valid accounts for activated requisitions - $nordigen_accountIds = []; - foreach ($requisitions["results"] as $requisition) { - // FILTER: for requisitionIds - if ($requisitionIds && !in_array($requisition["id"], $requisitionIds)) - continue; - - foreach ($requisition["accounts"] as $accountId) { - array_push($nordigen_accountIds, $accountId); - } - } - - $nordigen_accountIds = array_unique($nordigen_accountIds); - - $nordigen_accounts = []; - foreach ($nordigen_accountIds as $accountId) { - $nordigen_account = $this->getAccount($accountId); - - array_push($nordigen_accounts, $nordigen_account); - } - - return $nordigen_accounts; - - } - + // TODO: return null on not found public function getAccount(string $account_id) { diff --git a/app/Helpers/Bank/Nordigen/Transformer/IncomeTransformer.php b/app/Helpers/Bank/Nordigen/Transformer/IncomeTransformer.php index 3de6c3e7680f..43476923a4ee 100644 --- a/app/Helpers/Bank/Nordigen/Transformer/IncomeTransformer.php +++ b/app/Helpers/Bank/Nordigen/Transformer/IncomeTransformer.php @@ -12,151 +12,77 @@ namespace App\Helpers\Bank\Nordigen\Transformer; use App\Helpers\Bank\BankRevenueInterface; +use App\Models\BankIntegration; use App\Utils\Traits\AppSetup; use Illuminate\Support\Facades\Cache; /** -"date": "string", -"sourceId": "string", -"symbol": "string", -"cusipNumber": "string", -"highLevelCategoryId": 0, -"detailCategoryId": 0, -"description": {}, -"memo": "string", -"settleDate": "string", -"type": "string", -"intermediary": [], -"baseType": "CREDIT", -"categorySource": "SYSTEM", -"principal": {}, -"lastUpdated": "string", -"interest": {}, -"price": {}, -"commission": {}, -"id": 0, -"merchantType": "string", -"amount": { -"amount": 0, -"convertedAmount": 0, -"currency": "USD", -"convertedCurrency": "USD" -}, -"checkNumber": "string", -"isPhysical": true, -"quantity": 0, -"valoren": "string", -"isManual": true, -"merchant": { -"website": "string", -"address": {}, -"contact": {}, -"categoryLabel": [], -"coordinates": {}, -"name": "string", -"id": "string", -"source": "YODLEE", -"logoURL": "string" -}, -"sedol": "string", -"transactionDate": "string", -"categoryType": "TRANSFER", -"accountId": 0, -"createdDate": "string", -"sourceType": "AGGREGATED", -"CONTAINER": "bank", -"postDate": "string", -"parentCategoryId": 0, -"subType": "OVERDRAFT_CHARGE", -"category": "string", -"runningBalance": {}, -"categoryId": 0, -"holdingDescription": "string", -"isin": "string", -"status": "POSTED" - -( -[CONTAINER] => bank -[id] => 103953585 -[amount] => stdClass Object - ( - [amount] => 480.66 - [currency] => USD - ) - -[categoryType] => UNCATEGORIZE -[categoryId] => 1 -[category] => Uncategorized -[categorySource] => SYSTEM -[highLevelCategoryId] => 10000017 -[createdDate] => 2022-08-04T21:50:17Z -[lastUpdated] => 2022-08-04T21:50:17Z -[description] => stdClass Object - ( - [original] => CHEROKEE NATION TAX TA TAHLEQUAH OK - ) - -[isManual] => -[sourceType] => AGGREGATED -[date] => 2022-08-03 -[transactionDate] => 2022-08-03 -[postDate] => 2022-08-03 -[status] => POSTED -[accountId] => 12331794 -[runningBalance] => stdClass Object - ( - [amount] => 480.66 - [currency] => USD - ) - -[checkNumber] => 998 -) +{ + "transactions": { + "booked": [ + { + "transactionId": "string", + "debtorName": "string", + "debtorAccount": { + "iban": "string" + }, + "transactionAmount": { + "currency": "string", + "amount": "328.18" + }, + "bankTransactionCode": "string", + "bookingDate": "date", + "valueDate": "date", + "remittanceInformationUnstructured": "string" + }, + { + "transactionId": "string", + "transactionAmount": { + "currency": "string", + "amount": "947.26" + }, + "bankTransactionCode": "string", + "bookingDate": "date", + "valueDate": "date", + "remittanceInformationUnstructured": "string" + } + ], + "pending": [ + { + "transactionAmount": { + "currency": "string", + "amount": "99.20" + }, + "valueDate": "date", + "remittanceInformationUnstructured": "string" + } + ] + } +} */ class IncomeTransformer implements BankRevenueInterface { use AppSetup; - public function transform($transaction) + public function transform(BankIntegration $bank_integration, $transaction) { - $data = []; - - if (!property_exists($transaction, 'transaction')) - return $data; - - foreach ($transaction->transaction as $transaction) { - $data[] = $this->transformTransaction($transaction); - } - - return $data; - } - - public function transformTransaction($transaction) - { + if (!property_exists($transaction, 'transactionId') || !property_exists($transaction, 'transactionAmount') || !property_exists($transaction, 'balances') || !property_exists($transaction, 'institution')) + throw new \Exception('invalid dataset'); return [ - 'transaction_id' => $transaction->id, - 'amount' => $transaction->amount->amount, - 'currency_id' => $this->convertCurrency($transaction->amount->currency), - 'account_type' => $transaction->CONTAINER, + 'transaction_id' => $transaction->transactionId, + 'amount' => abs($transaction->transactionAmount->amount), + 'currency_id' => $this->convertCurrency($transaction->transactionAmount->currency), + 'account_type' => 'bank', 'category_id' => $transaction->highLevelCategoryId, 'category_type' => $transaction->categoryType, - 'date' => $transaction->date, - 'bank_account_id' => $transaction->accountId, - 'description' => $transaction->description->original, - 'base_type' => property_exists($transaction, 'baseType') ? $transaction->baseType : $this->calculateBaseType($transaction), + 'date' => $transaction->bookingDate, + 'bank_account_id' => $bank_integration->id, + 'description' => $transaction->remittanceInformationUnstructured, + 'base_type' => $transaction->transactionAmount->amount > 0 ? 'DEBIT' : 'CREDIT', ]; - } - - private function calculateBaseType($transaction) - { - //CREDIT / DEBIT - - if (property_exists($transaction, 'highLevelCategoryId') && $transaction->highLevelCategoryId == 10000012) - return 'CREDIT'; - - return 'DEBIT'; } @@ -180,7 +106,6 @@ class IncomeTransformer implements BankRevenueInterface } - } diff --git a/app/Http/Controllers/Bank/NordigenController.php b/app/Http/Controllers/Bank/NordigenController.php index 8bb976c3ea27..9d5089c8e0a2 100644 --- a/app/Http/Controllers/Bank/NordigenController.php +++ b/app/Http/Controllers/Bank/NordigenController.php @@ -22,6 +22,7 @@ use App\Models\Company; use Cache; use Illuminate\Http\Request; use Log; +use Nordigen\NordigenPHP\Exceptions\NordigenExceptions\NordigenException; class NordigenController extends BaseController { @@ -166,34 +167,42 @@ class NordigenController extends BaseController }*/ public function connect(ConnectNordigenBankIntegrationRequest $request) { - - $account = auth()->user()->account; - if (!$account->bank_integration_nordigen_secret_id || !$account->bank_integration_nordigen_secret_key) - return response()->json(['message' => 'Not yet authenticated with Nordigen Bank Integration service'], 400); - $data = $request->all(); - $context = Cache::get($data["hash"]); - Log::info($context); - if (!$context || $context["context"] != "nordigen" || array_key_exists("requisition", $context)) // TODO: check for requisition array key - return response()->json(['message' => 'Invalid context one_time_token. (not-found|invalid-context|already-used) Call /api/v1/one_time_token with context: \'nordigen\' first.'], 400); + $context = Cache::get($data["one_time_token"]); - Log::info(config('ninja.app_url') . '/api/v1/nordigen/confirm'); + if (!$context || $context["context"] != "nordigen" || array_key_exists("requisitionId", $context)) + return response()->redirectTo($data["redirect"] . "?action=nordigen_connect&status=failed&reason=one-time-token-invalid"); + + $company = Company::where('company_key', $context["company_key"])->first(); + $account = $company->account; + + if (!$account->bank_integration_nordigen_secret_id || !$account->bank_integration_nordigen_secret_key) + return response()->redirectTo($data["redirect"] . "?action=nordigen_connect&status=failed&reason=account-config-invalid"); $nordigen = new Nordigen($account->bank_integration_nordigen_secret_id, $account->bank_integration_nordigen_secret_key); - $requisition = $nordigen->createRequisition(config('ninja.app_url') . '/api/v1/nordigen/confirm', $data['institutionId'], $data["hash"]); + try { + $requisition = $nordigen->createRequisition(config('ninja.app_url') . '/api/v1/nordigen/confirm', $data['institution_id'], "1"); + } catch (NordigenException $e) { // TODO: property_exists returns null in these cases... => why => therefore we just get unknown error everytime $responseBody is typeof GuzzleHttp\Psr7\Stream + Log::error($e); + $responseBody = $e->getResponse()->getBody(); + Log::info($responseBody); + + if (property_exists($responseBody, "institution_id")) // provided institution_id was wrong + return response()->redirectTo($data["redirect"] . "?action=nordigen_connect&status=failed&reason=institution-invalid"); + else if (property_exists($responseBody, "reference")) // this error can occur, when a reference was used double or is invalid => therefor we suggest the frontend to use another one-time-token + return response()->redirectTo($data["redirect"] . "?action=nordigen_connect&status=failed&reason=one-time-token-invalid"); + else + return response()->redirectTo($data["redirect"] . "?action=nordigen_connect&status=failed&reason=unknown"); + } // save cache - if (array_key_exists("redirectUri", $data)) - $context["redirectUri"] = $data["redirectUri"]; + if (array_key_exists("redirect", $data)) + $context["redirect"] = $data["redirect"]; $context["requisitionId"] = $requisition["id"]; - Cache::put($data["hash"], $context, 3600); - - return response()->json([ - 'result' => $requisition, - 'redirectUri' => array_key_exists("redirectUri", $data) ? $data["redirectUri"] : null, - ]); + Cache::put($data["one_time_token"], $context, 3600); + return response()->redirectTo($requisition["link"]); } /** @@ -266,62 +275,27 @@ class NordigenController extends BaseController $data = $request->all(); $context = Cache::get($data["ref"]); - if (!$context || $context["context"] != "nordigen" || !array_key_exists("requisitionId", $context)) { - if ($context && array_key_exists("redirectUri", $context)) - return response()->redirectTo($context["redirectUri"] . "?action=nordigen_connect&status=failed&reason=ref-invalid"); + if (!$context || $context["context"] != "nordigen" || !array_key_exists("requisitionId", $context)) + return response()->redirectTo($context["redirect"] . "?action=nordigen_connect&status=failed&reason=ref-invalid"); - return response()->json([ - 'status' => 'failed', - 'reason' => 'ref-invalid', - ], 400); - } $company = Company::where('company_key', $context["company_key"])->first(); $account = $company->account; - if (!$account->bank_integration_nordigen_secret_id || !$account->bank_integration_nordigen_secret_key) { - if (array_key_exists("redirectUri", $context)) - return response()->redirectTo($context["redirectUri"] . "?action=nordigen_connect&status=failed&reason=account-config-invalid"); - - return response()->json([ - 'status' => 'failed', - 'reason' => 'account-config-invalid', - ], 400); - } + if (!$account->bank_integration_nordigen_secret_id || !$account->bank_integration_nordigen_secret_key) + return response()->redirectTo($context["redirect"] . "?action=nordigen_connect&status=failed&reason=account-config-invalid"); // fetch requisition $nordigen = new Nordigen($account->bank_integration_nordigen_secret_id, $account->bank_integration_nordigen_secret_key); $requisition = $nordigen->getRequisition($context["requisitionId"]); // check validity of requisition - if (!$requisition) { - if (array_key_exists("redirectUri", $context)) - return response()->redirectTo($context["redirectUri"] . "?action=nordigen_connect&status=failed&reason=requisition-not-found"); - - return response()->json([ - 'status' => 'failed', - 'reason' => 'requisition-not-found', - ], 400); - } - if ($requisition["status"] != "LN") { - if (array_key_exists("redirectUri", $context)) - return response()->redirectTo($context["redirectUri"] . "?action=nordigen_connect&status=failed&reason=requisition-invalid-status"); - - return response()->json([ - 'status' => 'failed', - 'reason' => 'requisition-invalid-status', - ], 400); - } - if (sizeof($requisition["accounts"]) == 0) { - if (array_key_exists("redirectUri", $context)) - return response()->redirectTo($context["redirectUri"] . "?action=nordigen_connect&status=failed&reason=requisition-no-accounts"); - - return response()->json([ - 'status' => 'failed', - 'reason' => 'requisition-no-accounts', - ], 400); - } - + if (!$requisition) + return response()->redirectTo($context["redirect"] . "?action=nordigen_connect&status=failed&reason=requisition-not-found"); + if ($requisition["status"] != "LN") + return response()->redirectTo($context["redirect"] . "?action=nordigen_connect&status=failed&reason=requisition-invalid-status"); + if (sizeof($requisition["accounts"]) == 0) + return response()->redirectTo($context["redirect"] . "?action=nordigen_connect&status=failed&reason=requisition-no-accounts"); // connect new accounts $bank_integration_ids = []; @@ -381,14 +355,8 @@ class NordigenController extends BaseController // prevent rerun of this method with same ref Cache::delete($data["ref"]); - // Successfull Response - if (array_key_exists("redirectUri", $context)) - return response()->redirectTo($context["redirectUri"] . "?action=nordigen_connect&status=success&bank_integrations=" . implode(',', $bank_integration_ids)); - - return response()->json([ - 'status' => 'success', - 'bank_integrations' => $bank_integration_ids, - ]); + // Successfull Response => Redirect + return response()->redirectTo($context["redirect"] . "?action=nordigen_connect&status=success&bank_integrations=" . implode(',', $bank_integration_ids)); } diff --git a/app/Http/Requests/Nordigen/ConnectNordigenBankIntegrationRequest.php b/app/Http/Requests/Nordigen/ConnectNordigenBankIntegrationRequest.php index de249b18e207..639e7c0c546f 100644 --- a/app/Http/Requests/Nordigen/ConnectNordigenBankIntegrationRequest.php +++ b/app/Http/Requests/Nordigen/ConnectNordigenBankIntegrationRequest.php @@ -12,6 +12,8 @@ namespace App\Http\Requests\Nordigen; use App\Http\Requests\Request; +use Cache; +use Log; class ConnectNordigenBankIntegrationRequest extends Request { @@ -33,9 +35,29 @@ class ConnectNordigenBankIntegrationRequest extends Request public function rules() { return [ - 'institutionId' => 'required|string', - 'hash' => 'required|string', // One Time Token - 'redirectUri' => 'string', // TODO: @turbo124 @todo validate, that this is a url without / at the end + 'institution_id' => 'required|string', + 'one_time_token' => 'required|string', // One Time Token + 'redirect' => 'string', // TODO: @turbo124 @todo validate, that this is a url without / at the end ]; } + + // @turbo124 @todo please check for validity, when issue request from frontend + public function prepareForValidation() + { + $input = $this->all(); + + if (!array_key_exists('redirect', $input)) { + $context = Cache::get($input['one_time_token']); + + if (array_key_exists('is_react', $context)) + $input["redirect"] = $context["is_react"] ? config("ninja.react_url") : config("ninja.app_url"); + else + $input["redirect"] = config("ninja.app_url"); + + Log::info($input); + + $this->replace($input); + + } + } } diff --git a/routes/api.php b/routes/api.php index c9202f65fb54..3cd306fdaa4d 100644 --- a/routes/api.php +++ b/routes/api.php @@ -378,7 +378,7 @@ Route::post('api/v1/yodlee/refresh_updates', [YodleeController::class, 'refreshU Route::post('api/v1/yodlee/balance', [YodleeController::class, 'balanceWebhook'])->middleware('throttle:100,1'); Route::get('api/v1/nordigen/institutions', [NordigenController::class, 'institutions'])->middleware('throttle:100,1')->middleware('token_auth')->name('nordigen_institutions'); -Route::post('api/v1/nordigen/connect', [NordigenController::class, 'connect'])->middleware('throttle:100,1')->middleware('token_auth')->name('nordigen_connect'); +Route::any('api/v1/nordigen/connect', [NordigenController::class, 'connect'])->middleware('throttle:100,1')->name('nordigen_connect'); Route::any('api/v1/nordigen/confirm', [NordigenController::class, 'confirm'])->middleware('throttle:100,1')->name('nordigen_callback'); Route::fallback([BaseController::class, 'notFound']);