2023-11-30 16:00:50 +01:00
< ? php
/**
* Invoice Ninja ( https :// invoiceninja . com ) .
*
* @ link https :// github . com / invoiceninja / invoiceninja source repository
*
* @ copyright Copyright ( c ) 2023. Invoice Ninja LLC ( https :// invoiceninja . com )
*
* @ 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-06 08:27:18 +01:00
use App\Models\Company ;
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-08 18:42:06 +01:00
use Log ;
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-01 14:56:11 +01:00
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-09 15:13:00 +01:00
$context = $request -> getTokenContent ();
2023-12-04 08:14:52 +01:00
2023-12-09 09:27:59 +01:00
if ( ! $context || $context [ " context " ] != " nordigen " || array_key_exists ( " requisitionId " , $context ))
2023-12-11 19:18:32 +01:00
return response () -> redirectTo (( $context && array_key_exists ( " redirect " , $context ) ? $context [ " redirect " ] : config ( 'ninja.app_url' )) . " ?action=nordigen_connect&status=failed&reason=token-invalid " );
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
2023-12-13 15:37:19 +01:00
if ( ! ( config ( 'ninja.nordigen.secret_id' ) && config ( 'ninja.nordigen.secret_key' )))
2023-12-09 09:27:59 +01:00
return response () -> redirectTo ( $data [ " redirect " ] . " ?action=nordigen_connect&status=failed&reason=account-config-invalid " );
2023-12-06 08:27:18 +01:00
2023-12-13 15:37:19 +01:00
if ( ! ( Ninja :: isSelfHost () || ( Ninja :: isHosted () && $account -> isPaid () && $account -> plan == 'enterprise' )))
return response () -> redirectTo ( $context [ " redirect " ] . " ?action=nordigen_connect&status=failed&reason=not-available " );
$nordigen = new Nordigen ();
2023-12-09 15:13:00 +01:00
// show bank_selection_screen, when institution_id is not present
if ( ! array_key_exists ( " institution_id " , $data )) {
$data = [
'token' => $request -> token ,
'context' => $context ,
'institutions' => $nordigen -> getInstitutions (),
'company' => $company ,
'account' => $company -> account ,
];
return view ( 'bank.nordigen.connect' , $data );
}
// redirect to requisition flow
2023-12-09 09:27:59 +01:00
try {
2023-12-09 15:13:00 +01:00
$requisition = $nordigen -> createRequisition ( config ( 'ninja.app_url' ) . '/api/v1/nordigen/confirm' , $data [ 'institution_id' ], $request -> token );
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
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 " );
2023-12-09 15:13:00 +01:00
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 token
return response () -> redirectTo ( $data [ " redirect " ] . " ?action=nordigen_connect&status=failed&reason=token-invalid " );
2023-12-09 09:27:59 +01:00
else
return response () -> redirectTo ( $data [ " redirect " ] . " ?action=nordigen_connect&status=failed&reason=unknown " );
}
2023-12-08 18:42:06 +01:00
// save cache
2023-12-09 09:27:59 +01:00
if ( array_key_exists ( " redirect " , $data ))
$context [ " redirect " ] = $data [ " redirect " ];
2023-12-08 18:42:06 +01:00
$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
}
/**
* Process Nordigen Institutions GETTER .
2023-12-08 18:48:48 +01:00
* @ param ConfirmNordigenBankIntegrationRequest $request
2023-12-04 08:14:52 +01:00
*
* @ 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 " ),
* ),
* )
*/
/*
{
" event " : {
" info " : " REFRESH.PROCESS_COMPLETED " ,
" loginName " : " fri21 " ,
" data " : {
" providerAccount " : [
{
" id " : 10995860 ,
" providerId " : 16441 ,
" isManual " : false ,
" createdDate " : " 2017-12-22T05:47:35Z " ,
" aggregationSource " : " USER " ,
" status " : " SUCCESS " ,
" requestId " : " NSyMGo+R4dktywIu3hBIkc3PgWA= " ,
" dataset " : [
{
" name " : " BASIC_AGG_DATA " ,
" additionalStatus " : " AVAILABLE_DATA_RETRIEVED " ,
" updateEligibility " : " ALLOW_UPDATE " ,
" lastUpdated " : " 2017-12-22T05:48:16Z " ,
" lastUpdateAttempt " : " 2017-12-22T05:48:16Z "
}
]
}
]
}
}
} */
2023-12-08 18:48:48 +01:00
public function confirm ( ConfirmNordigenBankIntegrationRequest $request )
2023-12-04 08:14:52 +01:00
{
2023-11-30 16:00:50 +01:00
2023-12-05 06:56:52 +01:00
$data = $request -> all ();
2023-11-30 16:00:50 +01:00
2023-12-08 18:42:06 +01:00
$context = Cache :: get ( $data [ " ref " ]);
2023-12-09 09:27:59 +01:00
if ( ! $context || $context [ " context " ] != " nordigen " || ! array_key_exists ( " requisitionId " , $context ))
2023-12-11 19:18:32 +01:00
return response () -> redirectTo (( $context && array_key_exists ( " redirect " , $context ) ? $context [ " redirect " ] : config ( 'ninja.app_url' )) . " ?action=nordigen_connect&status=failed&reason=ref-invalid " );
2023-12-09 09:27:59 +01:00
2023-12-06 08:27:18 +01:00
2023-12-11 21:21:42 +01:00
$company = Company :: where ( 'company_key' , $context [ " company_key " ]) -> firstOrFail ();
2023-12-06 08:27:18 +01:00
$account = $company -> account ;
2023-12-13 15:37:19 +01:00
if ( ! ( config ( 'ninja.nordigen.secret_id' ) && config ( 'ninja.nordigen.secret_key' )))
2023-12-09 09:27:59 +01:00
return response () -> redirectTo ( $context [ " redirect " ] . " ?action=nordigen_connect&status=failed&reason=account-config-invalid " );
2023-12-06 08:27:18 +01:00
2023-12-13 15:37:19 +01:00
if ( ! ( Ninja :: isSelfHost () || ( Ninja :: isHosted () && $account -> isPaid () && $account -> plan == 'enterprise' )))
return response () -> redirectTo ( $context [ " redirect " ] . " ?action=nordigen_connect&status=failed&reason=not-available " );
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
2023-12-09 09:27:59 +01:00
if ( ! $requisition )
return response () -> redirectTo ( $context [ " redirect " ] . " ?action=nordigen_connect&status=failed&reason=requisition-not-found " );
if ( $requisition [ " status " ] != " LN " )
2023-12-11 19:18:32 +01:00
return response () -> redirectTo ( $context [ " redirect " ] . " ?action=nordigen_connect&status=failed&reason=requisition-invalid-status&status= " . $requisition [ " status " ]);
2023-12-09 09:27:59 +01:00
if ( sizeof ( $requisition [ " accounts " ]) == 0 )
return response () -> redirectTo ( $context [ " redirect " ] . " ?action=nordigen_connect&status=failed&reason=requisition-no-accounts " );
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
2023-12-12 07:52:53 +01:00
$existing_bank_integration = BankIntegration :: where ( 'nordigen_account_id' , $nordigen_account [ 'id' ]) -> where ( 'company_id' , $company -> id ) -> 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-13 15:38:37 +01:00
$bank_integration -> from_date = now () -> subDays ( 90 ); // default max-fetch interval of nordigen is 90 days
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-13 15:37:19 +01:00
$company -> account -> bank_integrations -> where ( " integration_type " , BankIntegration :: INTEGRATION_TYPE_NORDIGEN ) -> where ( 'auto_sync' , true ) -> each ( function ( $bank_integration ) {
2023-12-06 08:27:18 +01:00
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 " ),
* ),
* )
*/
/*
{
" event " : {
" info " : " REFRESH.PROCESS_COMPLETED " ,
" loginName " : " fri21 " ,
" data " : {
" providerAccount " : [
{
" id " : 10995860 ,
" providerId " : 16441 ,
" isManual " : false ,
" createdDate " : " 2017-12-22T05:47:35Z " ,
" aggregationSource " : " USER " ,
" status " : " SUCCESS " ,
" requestId " : " NSyMGo+R4dktywIu3hBIkc3PgWA= " ,
" dataset " : [
{
" name " : " BASIC_AGG_DATA " ,
" additionalStatus " : " AVAILABLE_DATA_RETRIEVED " ,
" updateEligibility " : " ALLOW_UPDATE " ,
" lastUpdated " : " 2017-12-22T05:48:16Z " ,
" lastUpdateAttempt " : " 2017-12-22T05:48:16Z "
}
]
}
]
}
}
} */
public function institutions ( Request $request )
{
$account = auth () -> user () -> account ;
2023-12-13 15:37:19 +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 );
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
}