Skip to content

Commit

Permalink
feat: 1671 kick off post authorization (#219)
Browse files Browse the repository at this point in the history
* feat(pisp-authorization): extend OutboundAuthorizationModel to call pisp authorization after receiving the quote

* lint: fix smells
  • Loading branch information
eoln authored Oct 8, 2020
1 parent ce5af89 commit 58fc8b3
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 22 deletions.
2 changes: 1 addition & 1 deletion src/OutboundServer/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ const postRequestToPayTransfer = async (ctx) => {
...ctx.state.conf,
cache: ctx.state.cache,
logger: ctx.state.logger,
wso2Auth: ctx.state.wso2Auth,
wso2Auth: ctx.state.wso2Auth
});

// initialize the transfer model and start it running
Expand Down
8 changes: 3 additions & 5 deletions src/lib/model/OutboundAuthorizationsModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,19 +135,18 @@ async function onRequestAuthorization() {
// in InboundServer/handlers is implemented putAuthorizationsById handler
// where this event is fired but only if env ENABLE_PISP_MODE=true
subId = await cache.subscribe(channel, async (channel, message, sid) => {

try {
const parsed = JSON.parse(message);
this.context.data = {
...parsed.data,
...parsed,
currentState: this.state
};
resolve();
} catch(err) {
reject(err);
} finally {
if(sid) {
cache.unsubscribe(sid);
cache.unsubscribe(channel, sid);
}
}
});
Expand All @@ -161,7 +160,7 @@ async function onRequestAuthorization() {
} catch(error) {
logger.push(error).error('Authorization request error');
if(subId) {
cache.unsubscribe(subId);
cache.unsubscribe(channel, subId);
}
reject(error);
}
Expand Down Expand Up @@ -190,7 +189,6 @@ function buildPostAuthorizationsRequest(data/** , config */) {
* @returns {Object} - the altered specStateMachine
*/
function injectHandlersContext(config, specStateMachine) {
// TODO: postAuthorizations is a mocked method until this feature arrive in MojaloopRequests
return {
...specStateMachine,
data: {
Expand Down
108 changes: 96 additions & 12 deletions src/lib/model/OutboundRequestToPayTransferModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ const StateMachine = require('javascript-state-machine');
const { Ilp, MojaloopRequests } = require('@mojaloop/sdk-standard-components');
const shared = require('@internal/shared');
const { BackendError } = require('./common');

const { BackendRequests } = require('@internal/requests');
const OutboundAuthorizationsModel = require('./OutboundAuthorizationsModel.js');
const requestToPayTransferStateEnum = {
'WAITING_FOR_QUOTE_ACCEPTANCE': 'WAITING_FOR_QUOTE_ACCEPTANCE',
'WAITING_FOR_OTP_ACCEPTANCE': 'WAITING_FOR_OTP_ACCEPTANCE',
'WAITING_FOR_AUTHORIZATION_ACCEPTANCE': 'WAITING_FOR_AUTHORIZATION_ACCEPTANCE',
'ERROR_OCCURRED': 'ERROR_OCCURRED',
'COMPLETED': 'COMPLETED',
};
Expand All @@ -30,6 +32,7 @@ const requestToPayTransferStateEnum = {
*/
class OutboundRequestToPayTransferModel {
constructor(config) {
this._config = { ...config };
this._cache = config.cache;
this._logger = config.logger;
this._requestProcessingTimeoutSeconds = config.requestProcessingTimeoutSeconds;
Expand Down Expand Up @@ -58,9 +61,18 @@ class OutboundRequestToPayTransferModel {
wso2Auth: config.wso2Auth
});

this._backendRequests = new BackendRequests({
logger: this._logger,
backendEndpoint: config.backendEndpoint,
dfspId: config.dfspId
});

this._ilp = new Ilp({
secret: config.ilpSecret
});

this._enablePISPMode = config.enablePISPMode;
this._logger.info('enablePISPMode: ', this._enablePISPMode);
}
/**
* Initializes the requestToPayTransfer model
Expand Down Expand Up @@ -93,7 +105,9 @@ class OutboundRequestToPayTransferModel {
transitions: [
{ name: 'requestQuote', from: 'start', to: 'quoteReceived' },
{ name: 'requestOTP', from: 'quoteReceived', to: 'otpReceived' },
{ name: 'requestAuthorization', from: 'quoteReceived', to: 'authorizationReceived' },
{ name: 'executeTransfer', from: 'otpReceived', to: 'succeeded' },
{ name: 'executeAuthorizedTransfer', from: 'authorizationReceived', to: 'succeeded' },
{ name: 'error', from: '*', to: 'errored' },
],
methods: {
Expand Down Expand Up @@ -122,7 +136,10 @@ class OutboundRequestToPayTransferModel {
// next transition is to requestQuote
await this.stateMachine.requestQuote();
this._logger.log(`Quote received for transfer ${this.data.transferId}`);


if(this.stateMachine.state === 'quoteReceived' && this.data.initiatorType === 'BUSINESS' && !this._autoAcceptR2PBusinessQuotes) {
// kick-off postAuthorizations here for PISP flow
//we break execution here and return the quote response details to allow asynchronous accept or reject
//of the quote
await this._save();
Expand All @@ -131,19 +148,31 @@ class OutboundRequestToPayTransferModel {
break;

case 'quoteReceived':
// next transition is requestOTP
await this.stateMachine.requestOTP();
if(this.data.initiatorType !== 'BUSINESS') {
this._logger.log(`OTP received for transactionId: ${this.data.requestToPayTransactionId} and transferId: ${this.data.transferId}`);
if(this.stateMachine.state === 'otpReceived' && !this._autoAcceptR2PDeviceOTP) {
//we break execution here and return the otp response details to allow asynchronous accept or reject
//of the quote
await this._save();
return this.getResponse();
// decide PISP or OTP flow
if (this._enablePISPMode) {
await this.stateMachine.requestAuthorization();
await this._save();
// let executeTransfer in recursive call of run()
} else {
await this.stateMachine.requestOTP();
if (this.data.initiatorType !== 'BUSINESS') {
this._logger.log(`OTP received for transactionId: ${this.data.requestToPayTransactionId} and transferId: ${this.data.transferId}`);
if (this.stateMachine.state === 'otpReceived' && !this._autoAcceptR2PDeviceOTP) {
//we break execution here and return the otp response details to allow asynchronous accept or reject
//of the quote
await this._save();
return this.getResponse();
}
}
}
break;

case 'authorizationReceived':
// next transition is executeTransfer
await this.stateMachine.executeAuthorizedTransfer();
this._logger.log(`Transfer ${this.data.transferId} has been completed`);
break;

case 'otpReceived':
// next transition is executeTransfer
await this.stateMachine.executeTransfer();
Expand All @@ -163,7 +192,7 @@ class OutboundRequestToPayTransferModel {
}

// now call ourslves recursively to deal with the next transition
this._logger.log(`RequestToPay Transfer model state machine transition completed in state: ${this.stateMachine.state}. Recusring to handle next transition.`);
this._logger.log(`RequestToPay Transfer model state machine transition completed in state: ${this.stateMachine.state}. Recursing to handle next transition.`);
return this.run();
}
catch(err) {
Expand Down Expand Up @@ -209,11 +238,16 @@ class OutboundRequestToPayTransferModel {
// request a quote
return this._requestQuote();

case 'requestAuthorization':
// request an OTP
return this._requestAuthorization();

case 'requestOTP':
// request an OTP
return this._requestOTP();

case 'executeTransfer':
case 'executeAuthorizedTransfer':
// prepare a transfer and wait for fulfillment
return this._executeTransfer();

Expand Down Expand Up @@ -465,6 +499,52 @@ class OutboundRequestToPayTransferModel {
});
}

async _requestAuthorization() {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve) => {
// let use here OutboundAuthorizationsModel which allows to do synchronous call to get Authorization from PISP
this._logger.log('OutboundRequestToPayTransferModel._requestAuthorization');
// prepare request
const authorizationsRequest = {
// here is hardcoded address of PISP because in this flow there is no way to get it so we are mocking it out
// take a look in thirdparty-scheme-adapter PISPTransactionModel(WIP)
toParticipantId: 'pisp',
authenticationType: 'U2F',
retriesLeft: '1',
amount: {
currency: 'USD',
amount: this.data.amount
},
transactionId: this.data.transferId,
transactionRequestId: this.data.requestToPayTransactionId,
quote: { ...this.data.quoteResponse },
};

const modelConfig = { ...this._config };

const cacheKey = `post_authorizations_${authorizationsRequest.transactionRequestId}`;

// use the authorizations model to execute asynchronous stages with the switch
const model = await OutboundAuthorizationsModel.create(authorizationsRequest, cacheKey, modelConfig);

// run model's workflow

this.data.authorizationResponse = await model.run();
// here is POC: happy flow
// the authorizationResponse should be analyzed
// and the pinValue should be validated but this is out of the scope of this POC
this._logger.push({ authorizationResponse: this.data.authorizationResponse }).log('authorizationResponse received');

// let call backend service which will validate pinValue
// TODO: add `/validate-authorization` path to mojaloop_simulator
// const validateResponse = await this._backendRequests.validateAuthorization(this.data.authorizationResponse);
// if (validateResponse.validationResult !== 'OK') {
// return reject(new Error('Invalid Authorization of Transaction'));
// }
resolve(this.data.authorizationResponse);
});
}

/**
* Sends request for
* Starts the quote resolution process by sending a POST /quotes request to the switch;
Expand Down Expand Up @@ -821,7 +901,11 @@ class OutboundRequestToPayTransferModel {
case 'quoteReceived':
resp.currentState = requestToPayTransferStateEnum.WAITING_FOR_QUOTE_ACCEPTANCE;
break;


case 'authorizationReceived':
resp.currentState = requestToPayTransferStateEnum.WAITING_FOR_AUTHORIZATION_ACCEPTANCE;
break;

case 'otpReceived':
resp.currentState = requestToPayTransferStateEnum.WAITING_FOR_OTP_ACCEPTANCE;
break;
Expand Down
4 changes: 4 additions & 0 deletions src/lib/model/lib/requests/backendRequests.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ class BackendRequests {
return this._post('signchallenge', authorizationReq);
}

async validateAuthorization(authorizationResponse) {
return this._post('validate-authorization', authorizationResponse);
}

/**
* Executes a GET /otp request for the specified transaction request id
*
Expand Down
9 changes: 5 additions & 4 deletions src/test/unit/lib/model/OutboundAuthorizationsModel.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ describe('authorizationsModel', () => {

// handler should unsubscribe from notification channel
expect(cache.unsubscribe).toBeCalledTimes(1);
expect(cache.unsubscribe).toBeCalledWith(subId);
expect(cache.unsubscribe).toBeCalledWith(channel, subId);
});

it('should unsubscribe from cache in case when error happens in workflow run', async () => {
Expand All @@ -215,7 +215,7 @@ describe('authorizationsModel', () => {
model.onRequestAuthorization().catch((err) => {
expect(err.message).toEqual('Unexpected token u in JSON at position 0');
expect(cache.unsubscribe).toBeCalledTimes(1);
expect(cache.unsubscribe).toBeCalledWith(subId);
expect(cache.unsubscribe).toBeCalledWith(channel, subId);
});

// fire publication to channel with invalid message
Expand All @@ -228,7 +228,8 @@ describe('authorizationsModel', () => {
// simulate error
MojaloopRequests.__postAuthorizations = jest.fn(() => Promise.reject('postAuthorization failed'));
data.transactionRequestId = uuid();


const channel = Model.notificationChannel(data.transactionRequestId);
const model = await Model.create(data, cacheKey, modelConfig);
const { cache } = model.context;

Expand All @@ -243,7 +244,7 @@ describe('authorizationsModel', () => {
expect(theError).toEqual('postAuthorization failed');
// handler should unsubscribe from notification channel
expect(cache.unsubscribe).toBeCalledTimes(1);
expect(cache.unsubscribe).toBeCalledWith(subId);
expect(cache.unsubscribe).toBeCalledWith(channel, subId);
});

});
Expand Down

0 comments on commit 58fc8b3

Please sign in to comment.