2023-11-30 16:00:50 +01:00
< ? php
/**
* Invoice Ninja ( https :// invoiceninja . com ) .
*
* @ link https :// github . com / invoiceninja / invoiceninja source repository
*
2024-04-12 06:15:41 +02:00
* @ copyright Copyright ( c ) 2024. Invoice Ninja LLC ( https :// invoiceninja . com )
2023-11-30 16:00:50 +01:00
*
* @ license https :// www . elastic . co / licensing / elastic - license
*/
namespace App\Http\Controllers\Bank ;
use App\Helpers\Bank\Nordigen\Nordigen ;
use App\Http\Controllers\BaseController ;
2023-12-08 18:48:48 +01:00
use App\Http\Requests\Nordigen\ConfirmNordigenBankIntegrationRequest ;
use App\Http\Requests\Nordigen\ConnectNordigenBankIntegrationRequest ;
2023-12-01 14:30:33 +01:00
use App\Jobs\Bank\ProcessBankTransactionsNordigen ;
2023-11-30 16:00:50 +01:00
use App\Models\BankIntegration ;
2023-12-13 15:37:19 +01:00
use App\Utils\Ninja ;
2023-12-06 08:27:18 +01:00
use Cache ;
2023-11-30 16:00:50 +01:00
use Illuminate\Http\Request ;
2023-12-09 09:27:59 +01:00
use Nordigen\NordigenPHP\Exceptions\NordigenExceptions\NordigenException ;
2023-11-30 16:00:50 +01:00
2023-12-01 14:30:33 +01:00
class NordigenController extends BaseController
2023-11-30 16:00:50 +01:00
{
2023-12-18 16:08:41 +01:00
/**
* VIEW : Connect Nordigen Bank Integration
* @ param ConnectNordigenBankIntegrationRequest $request
*/
2023-12-08 18:48:48 +01:00
public function connect ( ConnectNordigenBankIntegrationRequest $request )
2023-12-04 08:14:52 +01:00
{
2023-12-09 09:27:59 +01:00
$data = $request -> all ();
2023-12-26 04:31:03 +01:00
/** @var array $context */
2023-12-09 15:13:00 +01:00
$context = $request -> getTokenContent ();
2023-12-27 04:05:19 +01:00
$company = $request -> getCompany ();
$lang = $company -> locale ();
2023-12-20 17:58:06 +01:00
$context [ " lang " ] = $lang ;
2023-12-04 08:14:52 +01:00
2024-01-14 05:05:00 +01:00
if ( ! $context ) {
2023-12-15 13:10:45 +01:00
return view ( 'bank.nordigen.handler' , [
2023-12-15 14:45:52 +01:00
'lang' => $lang ,
2023-12-15 13:10:45 +01:00
'failed_reason' => " token-invalid " ,
" redirectUrl " => config ( " ninja.app_url " ) . " ?action=nordigen_connect&status=failed&reason=token-invalid " ,
]);
2024-01-14 05:05:00 +01:00
}
2023-12-15 13:10:45 +01:00
$context [ " redirect " ] = $data [ " redirect " ];
2024-01-14 05:05:00 +01:00
if ( $context [ " context " ] != " nordigen " || array_key_exists ( " requisitionId " , $context )) {
2023-12-15 13:10:45 +01:00
return view ( 'bank.nordigen.handler' , [
2023-12-15 14:45:52 +01:00
'lang' => $lang ,
2023-12-15 13:10:45 +01:00
'failed_reason' => " token-invalid " ,
" redirectUrl " => ( $context [ " redirect " ]) . " ?action=nordigen_connect&status=failed&reason=token-invalid " ,
]);
2024-01-14 05:05:00 +01:00
}
2023-12-04 08:14:52 +01:00
2023-12-09 15:13:00 +01:00
$company = $request -> getCompany ();
2023-12-11 13:23:28 +01:00
$account = $company -> account ;
2023-12-06 08:27:18 +01:00
2024-01-14 05:05:00 +01:00
if ( ! ( config ( 'ninja.nordigen.secret_id' ) && config ( 'ninja.nordigen.secret_key' ))) {
2023-12-15 13:10:45 +01:00
return view ( 'bank.nordigen.handler' , [
2023-12-15 14:45:52 +01:00
'lang' => $lang ,
2023-12-15 13:10:45 +01:00
'company' => $company ,
'account' => $company -> account ,
'failed_reason' => " account-config-invalid " ,
" redirectUrl " => $context [ " redirect " ] . " ?action=nordigen_connect&status=failed&reason=account-config-invalid " ,
]);
2024-01-14 05:05:00 +01:00
}
2023-12-06 08:27:18 +01:00
2024-01-14 05:05:00 +01:00
if ( ! ( Ninja :: isSelfHost () || ( Ninja :: isHosted () && $account -> isEnterprisePaidClient ()))) {
2023-12-15 13:10:45 +01:00
return view ( 'bank.nordigen.handler' , [
2023-12-15 14:45:52 +01:00
'lang' => $lang ,
2023-12-15 13:10:45 +01:00
'company' => $company ,
'account' => $company -> account ,
'failed_reason' => " not-available " ,
" redirectUrl " => $context [ " redirect " ] . " ?action=nordigen_connect&status=failed&reason=not-available " ,
]);
2024-01-14 05:05:00 +01:00
}
2023-12-13 15:37:19 +01:00
$nordigen = new Nordigen ();
2023-12-09 15:13:00 +01:00
// show bank_selection_screen, when institution_id is not present
2024-01-14 05:05:00 +01:00
if ( ! array_key_exists ( " institution_id " , $data )) {
2023-12-18 16:08:41 +01:00
return view ( 'bank.nordigen.handler' , [
2023-12-15 14:45:52 +01:00
'lang' => $lang ,
2023-12-09 15:13:00 +01:00
'company' => $company ,
'account' => $company -> account ,
2023-12-15 13:10:45 +01:00
'institutions' => $nordigen -> getInstitutions (),
'redirectUrl' => $context [ " redirect " ] . " ?action=nordigen_connect&status=user-aborted "
2023-12-18 16:08:41 +01:00
]);
2024-01-14 05:05:00 +01:00
}
2023-12-09 15:13:00 +01:00
// redirect to requisition flow
2023-12-09 09:27:59 +01:00
try {
2023-12-27 04:05:19 +01:00
$requisition = $nordigen -> createRequisition ( config ( 'ninja.app_url' ) . '/nordigen/confirm' , $data [ 'institution_id' ], $request -> token , $lang );
2023-12-09 09:27:59 +01:00
} 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
2023-12-15 13:10:45 +01:00
$responseBody = ( string ) $e -> getResponse () -> getBody ();
2024-01-14 05:05:00 +01:00
if ( str_contains ( $responseBody , '"institution_id"' )) { // provided institution_id was wrong
2023-12-15 13:10:45 +01:00
return view ( 'bank.nordigen.handler' , [
2023-12-15 14:45:52 +01:00
'lang' => $lang ,
2023-12-15 13:10:45 +01:00
'company' => $company ,
'account' => $company -> account ,
'failed_reason' => " institution-invalid " ,
" redirectUrl " => $context [ " redirect " ] . " ?action=nordigen_connect&status=failed&reason=institution-invalid " ,
]);
2024-01-14 05:05:00 +01:00
} elseif ( str_contains ( $responseBody , '"reference"' )) { // this error can occur, when a reference was used double or is invalid => therefor we suggest the frontend to use another token
2023-12-15 13:10:45 +01:00
return view ( 'bank.nordigen.handler' , [
2023-12-15 14:45:52 +01:00
'lang' => $lang ,
2023-12-15 13:10:45 +01:00
'company' => $company ,
'account' => $company -> account ,
'failed_reason' => " token-invalid " ,
" redirectUrl " => $context [ " redirect " ] . " ?action=nordigen_connect&status=failed&reason=token-invalid " ,
]);
2024-01-14 05:05:00 +01:00
} else {
2023-12-18 16:08:41 +01:00
nlog ( " Unknown Error from nordigen: " . $e );
nlog ( $responseBody );
2023-12-15 13:10:45 +01:00
return view ( 'bank.nordigen.handler' , [
2023-12-15 14:45:52 +01:00
'lang' => $lang ,
2023-12-15 13:10:45 +01:00
'company' => $company ,
'account' => $company -> account ,
'failed_reason' => " unknown " ,
" redirectUrl " => $context [ " redirect " ] . " ?action=nordigen_connect&status=failed&reason=unknown " ,
]);
2023-12-18 16:08:41 +01:00
}
2023-12-09 09:27:59 +01:00
}
2023-12-08 18:42:06 +01:00
// save cache
$context [ " requisitionId " ] = $requisition [ " id " ];
2023-12-09 15:13:00 +01:00
Cache :: put ( $request -> token , $context , 3600 );
2023-12-08 18:42:06 +01:00
2023-12-09 09:27:59 +01:00
return response () -> redirectTo ( $requisition [ " link " ]);
2023-12-04 08:14:52 +01:00
}
/**
2023-12-18 16:08:41 +01:00
* VIEW : Confirm Nordigen Bank Integration ( redirect after nordigen flow )
2023-12-26 00:19:48 +01:00
* @ param ConfirmNordigenBankIntegrationRequest $request
2023-12-04 08:14:52 +01:00
*/
2023-12-08 18:48:48 +01:00
public function confirm ( ConfirmNordigenBankIntegrationRequest $request )
2023-12-04 08:14:52 +01:00
{
2023-12-05 06:56:52 +01:00
$data = $request -> all ();
2023-12-27 04:05:19 +01:00
$company = $request -> getCompany ();
$account = $company -> account ;
$lang = $company -> locale ();
2023-12-26 00:19:48 +01:00
/** @var array $context */
2023-12-20 16:42:29 +01:00
$context = $request -> getTokenContent ();
2024-01-14 05:05:00 +01:00
if ( ! array_key_exists ( 'lang' , $data ) && $context [ 'lang' ] != 'en' ) {
2023-12-27 04:05:19 +01:00
return redirect () -> route ( 'nordigen.confirm' , array_merge ([ " lang " => $context [ 'lang' ]], $request -> query ()));
2024-01-14 05:05:00 +01:00
}
2023-11-30 16:00:50 +01:00
2024-01-14 05:05:00 +01:00
if ( ! $context || $context [ " context " ] != " nordigen " || ! array_key_exists ( " requisitionId " , $context )) {
2023-12-15 13:10:45 +01:00
return view ( 'bank.nordigen.handler' , [
2023-12-15 14:45:52 +01:00
'lang' => $lang ,
2023-12-15 13:10:45 +01:00
'failed_reason' => " ref-invalid " ,
" redirectUrl " => ( $context && array_key_exists ( " redirect " , $context ) ? $context [ " redirect " ] : config ( 'ninja.app_url' )) . " ?action=nordigen_connect&status=failed&reason=ref-invalid " ,
]);
2024-01-14 05:05:00 +01:00
}
2023-12-09 09:27:59 +01:00
2024-01-14 05:05:00 +01:00
if ( ! ( config ( 'ninja.nordigen.secret_id' ) && config ( 'ninja.nordigen.secret_key' ))) {
2023-12-15 13:10:45 +01:00
return view ( 'bank.nordigen.handler' , [
2023-12-15 14:45:52 +01:00
'lang' => $lang ,
2023-12-15 13:10:45 +01:00
'company' => $company ,
'account' => $company -> account ,
'failed_reason' => " account-config-invalid " ,
" redirectUrl " => $context [ " redirect " ] . " ?action=nordigen_connect&status=failed&reason=account-config-invalid " ,
]);
2024-01-14 05:05:00 +01:00
}
2023-12-06 08:27:18 +01:00
2024-01-14 05:05:00 +01:00
if ( ! ( Ninja :: isSelfHost () || ( Ninja :: isHosted () && $account -> isEnterprisePaidClient ()))) {
2023-12-15 13:10:45 +01:00
return view ( 'bank.nordigen.handler' , [
2023-12-15 14:45:52 +01:00
'lang' => $lang ,
2023-12-15 13:10:45 +01:00
'company' => $company ,
'account' => $company -> account ,
'failed_reason' => " not-available " ,
" redirectUrl " => $context [ " redirect " ] . " ?action=nordigen_connect&status=failed&reason=not-available " ,
]);
2024-01-14 05:05:00 +01:00
}
2023-12-13 15:37:19 +01:00
2023-12-08 18:42:06 +01:00
// fetch requisition
2023-12-13 15:37:19 +01:00
$nordigen = new Nordigen ();
2023-12-08 18:42:06 +01:00
$requisition = $nordigen -> getRequisition ( $context [ " requisitionId " ]);
2023-12-06 08:27:18 +01:00
2023-12-08 18:42:06 +01:00
// check validity of requisition
2024-01-14 05:05:00 +01:00
if ( ! $requisition ) {
2023-12-15 13:10:45 +01:00
return view ( 'bank.nordigen.handler' , [
2023-12-15 14:45:52 +01:00
'lang' => $lang ,
2023-12-15 13:10:45 +01:00
'company' => $company ,
'account' => $company -> account ,
'failed_reason' => " requisition-not-found " ,
" redirectUrl " => $context [ " redirect " ] . " ?action=nordigen_connect&status=failed&reason=requisition-not-found " ,
]);
2024-01-14 05:05:00 +01:00
}
if ( $requisition [ " status " ] != " LN " ) {
2023-12-15 13:10:45 +01:00
return view ( 'bank.nordigen.handler' , [
2023-12-15 14:45:52 +01:00
'lang' => $lang ,
2023-12-15 13:10:45 +01:00
'company' => $company ,
'account' => $company -> account ,
'failed_reason' => " requisition-invalid-status " ,
" redirectUrl " => $context [ " redirect " ] . " ?action=nordigen_connect&status=failed&reason=requisition-invalid-status&status= " . $requisition [ " status " ],
]);
2024-01-14 05:05:00 +01:00
}
if ( sizeof ( $requisition [ " accounts " ]) == 0 ) {
2023-12-15 13:10:45 +01:00
return view ( 'bank.nordigen.handler' , [
2023-12-15 14:45:52 +01:00
'lang' => $lang ,
2023-12-15 13:10:45 +01:00
'company' => $company ,
'account' => $company -> account ,
'failed_reason' => " requisition-no-accounts " ,
" redirectUrl " => $context [ " redirect " ] . " ?action=nordigen_connect&status=failed&reason=requisition-no-accounts " ,
]);
2024-01-14 05:05:00 +01:00
}
2023-12-08 18:42:06 +01:00
// connect new accounts
$bank_integration_ids = [];
2023-12-08 19:05:49 +01:00
foreach ( $requisition [ " accounts " ] as $nordigenAccountId ) {
2023-12-06 08:27:18 +01:00
2023-12-08 19:05:49 +01:00
$nordigen_account = $nordigen -> getAccount ( $nordigenAccountId );
2023-12-06 08:27:18 +01:00
2024-01-10 15:34:25 +01:00
$existing_bank_integration = BankIntegration :: withTrashed () -> where ( 'nordigen_account_id' , $nordigen_account [ 'id' ]) -> where ( 'company_id' , $company -> id ) -> where ( 'is_deleted' , 0 ) -> first ();
2023-12-08 18:42:06 +01:00
if ( ! $existing_bank_integration ) {
2023-12-06 08:27:18 +01:00
$bank_integration = new BankIntegration ();
$bank_integration -> integration_type = BankIntegration :: INTEGRATION_TYPE_NORDIGEN ;
$bank_integration -> company_id = $company -> id ;
$bank_integration -> account_id = $company -> account_id ;
$bank_integration -> user_id = $company -> owner () -> id ;
2023-12-11 09:23:35 +01:00
$bank_integration -> nordigen_account_id = $nordigen_account [ 'id' ];
2023-12-08 19:05:49 +01:00
$bank_integration -> bank_account_type = $nordigen_account [ 'account_type' ];
$bank_integration -> bank_account_name = $nordigen_account [ 'account_name' ];
$bank_integration -> bank_account_status = $nordigen_account [ 'account_status' ];
$bank_integration -> bank_account_number = $nordigen_account [ 'account_number' ];
2023-12-11 16:13:26 +01:00
$bank_integration -> nordigen_institution_id = $nordigen_account [ 'provider_id' ];
2023-12-08 19:05:49 +01:00
$bank_integration -> provider_name = $nordigen_account [ 'provider_name' ];
$bank_integration -> nickname = $nordigen_account [ 'nickname' ];
$bank_integration -> balance = $nordigen_account [ 'current_balance' ];
$bank_integration -> currency = $nordigen_account [ 'account_currency' ];
2023-12-08 18:42:06 +01:00
$bank_integration -> disabled_upstream = false ;
$bank_integration -> auto_sync = true ;
2023-12-13 15:38:37 +01:00
$bank_integration -> from_date = now () -> subDays ( 90 ); // default max-fetch interval of nordigen is 90 days
2023-12-06 08:27:18 +01:00
$bank_integration -> save ();
2023-12-08 18:42:06 +01:00
array_push ( $bank_integration_ids , $bank_integration -> id );
} else {
// resetting metadata for account status
$existing_bank_integration -> balance = $account [ 'current_balance' ];
$existing_bank_integration -> bank_account_status = $account [ 'account_status' ];
$existing_bank_integration -> disabled_upstream = false ;
$existing_bank_integration -> auto_sync = true ;
2023-12-18 15:48:25 +01:00
$existing_bank_integration -> from_date = now () -> subDays ( 90 ); // default max-fetch interval of nordigen is 90 days
2023-12-19 08:37:04 +01:00
$existing_bank_integration -> deleted_at = null ;
2023-12-08 18:42:06 +01:00
$existing_bank_integration -> save ();
array_push ( $bank_integration_ids , $existing_bank_integration -> id );
2023-12-06 08:27:18 +01:00
}
}
2023-12-08 18:42:06 +01:00
// perform update in background
2023-12-19 08:37:04 +01:00
$company -> account -> bank_integrations -> where ( " integration_type " , BankIntegration :: INTEGRATION_TYPE_NORDIGEN ) -> where ( 'auto_sync' , true ) -> each ( function ( $bank_integration ) {
2023-12-13 15:37:19 +01:00
ProcessBankTransactionsNordigen :: dispatch ( $bank_integration );
2023-12-06 08:27:18 +01:00
});
2023-11-30 16:00:50 +01:00
2023-12-08 18:42:06 +01:00
// prevent rerun of this method with same ref
Cache :: delete ( $data [ " ref " ]);
2023-12-09 09:27:59 +01:00
// Successfull Response => Redirect
return response () -> redirectTo ( $context [ " redirect " ] . " ?action=nordigen_connect&status=success&bank_integrations= " . implode ( ',' , $bank_integration_ids ));
2023-11-30 16:00:50 +01:00
}
2023-12-09 15:13:00 +01:00
/**
* Process Nordigen Institutions GETTER .
*
*
* @ OA\Post (
* path = " /api/v1/nordigen/institutions " ,
* operationId = " nordigenRefreshWebhook " ,
* tags = { " nordigen " },
* summary = " Getting available institutions from nordigen " ,
* description = " Used to determine the available institutions for sending and creating a new connect-link " ,
* @ OA\Parameter ( ref = " #/components/parameters/X-Api-Token " ),
* @ OA\Parameter ( ref = " #/components/parameters/X-Requested-With " ),
* @ OA\Parameter ( ref = " #/components/parameters/include " ),
* @ OA\Response (
* response = 200 ,
* description = " " ,
* @ 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\JsonContent ( ref = " #/components/schemas/Credit " ),
* ),
* @ 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 institutions ( Request $request )
{
2024-01-14 05:05:00 +01:00
if ( ! ( config ( 'ninja.nordigen.secret_id' ) && config ( 'ninja.nordigen.secret_key' ))) {
2023-12-09 15:13:00 +01:00
return response () -> json ([ 'message' => 'Not yet authenticated with Nordigen Bank Integration service' ], 400 );
2024-01-14 05:05:00 +01:00
}
2023-12-09 15:13:00 +01:00
2023-12-13 15:37:19 +01:00
$nordigen = new Nordigen ();
2023-12-09 15:13:00 +01:00
return response () -> json ( $nordigen -> getInstitutions ());
}
2023-11-30 16:00:50 +01:00
}