mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-11-10 05:02:36 +01:00
rewrite connect/confirm for complete flow usage with only redirects
This commit is contained in:
parent
e349f1515d
commit
c138f2f211
@ -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)
|
||||
{
|
||||
|
||||
|
@ -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
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -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));
|
||||
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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']);
|
||||
|
Loading…
Reference in New Issue
Block a user