Skip to content

Commit

Permalink
Enable/disable account (#61)
Browse files Browse the repository at this point in the history
* Initial implementation

* Minor bugfix to prevent saga cancellation

* Remove redundant portal backend APIs

* Fix financial positions page object

* Further parametrise voodoo timeout. Increase voodoo timeout. Remove redundant import.

* Test for account enable/disable

* 2.5.0

* Versions
  • Loading branch information
partiallyordered authored Sep 15, 2021
1 parent f1580c8 commit 3e92eb2
Show file tree
Hide file tree
Showing 18 changed files with 382 additions and 301 deletions.
4 changes: 2 additions & 2 deletions helm/finance-portal-v2-ui/Chart.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
apiVersion: v1
appVersion: "v2.4.1"
appVersion: "v2.5.0"
description: Mojaloop Finance Portal UI v2
name: finance-portal-v2-ui
version: 2.4.1
version: 2.5.0
2 changes: 1 addition & 1 deletion helm/finance-portal-v2-ui/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ replicaCount: 1

image:
repository: ghcr.io/mojaloop/finance-portal-v2-ui
tag: v2.4.1
tag: v2.5.0
pullPolicy: IfNotPresent

imagePullCredentials:
Expand Down
1 change: 1 addition & 0 deletions integration_test/tests/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ export const config = {
password: users[1].password,
},
},
voodooTimeoutMs: 30000,
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type FinancialPositionsRow = {
ndc: Selector,
ndcUsed: Selector,
updateButton: Selector,
enableDisableButton: Selector,
}

const finPosUpdateConfirmRoot = ReactSelector('FinancialPositionUpdateConfirm Modal');
Expand Down Expand Up @@ -61,11 +62,13 @@ export const FinancialPositionsPage = {
.from({ length })
.map((_, i) => ({
dfsp: rows.nth(i).findReact('ItemCell').nth(0),
balance: rows.nth(i).findReact('ItemCell').nth(1),
position: rows.nth(i).findReact('ItemCell').nth(1),
ndc: rows.nth(i).findReact('ItemCell').nth(1),
ndcUsed: rows.nth(i).findReact('ItemCell').nth(1),
updateButton: rows.nth(i).findReact('Button'),
currency: rows.nth(i).findReact('ItemCell').nth(1),
balance: rows.nth(i).findReact('ItemCell').nth(2),
position: rows.nth(i).findReact('ItemCell').nth(3),
ndc: rows.nth(i).findReact('ItemCell').nth(4),
ndcUsed: rows.nth(i).findReact('ItemCell').nth(5),
updateButton: rows.nth(i).findReact('ItemCell').nth(6).findReact('Button'),
enableDisableButton: rows.nth(i).findReact('ItemCell').nth(7).findReact('Button'),
}));
},
};
26 changes: 24 additions & 2 deletions integration_test/tests/src/tests/DFSPFinancialPositions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { SideMenu } from '../page-objects/components/SideMenu';
import { LoginPage } from '../page-objects/pages/LoginPage';
import {
FinancialPositionsPage,
FinancialPositionsRow,
FinancialPositionUpdateConfirmModal,
PositionUpdateAction,
FinancialPositionUpdateModal
Expand All @@ -16,7 +15,7 @@ import { VoodooClient, protocol } from 'mojaloop-voodoo-client';
fixture`DFSPFinancialPositions`
.page`${config.financePortalEndpoint}`
.before(async (ctx) => {
const cli = new VoodooClient('ws://localhost:3030/voodoo', { defaultTimeout: 15000 });
const cli = new VoodooClient('ws://localhost:3030/voodoo', { defaultTimeout: config.voodooTimeoutMs });
await cli.connected();

const hubAccounts: protocol.HubAccount[] = [
Expand Down Expand Up @@ -82,6 +81,29 @@ test.meta({
}
)

test(
'Enable/disable account works correctly',
async (t) => {
const dfspRows = await FinancialPositionsPage.getDfspRowMap();
const testRow = dfspRows.get(t.fixtureCtx.participants[0].name);
assert(testRow, 'Expected to find the participant we created in the list of financial positions');

await t
.expect(testRow.enableDisableButton.innerText)
.eql('Disable', 'Expected new test participant to have enabled account');
await t.click(testRow.enableDisableButton);

await t
.expect(testRow.enableDisableButton.innerText)
.eql('Enable', 'Expected test participant to have disabled account after disable selected');
await t.click(testRow.enableDisableButton);

await t
.expect(testRow.enableDisableButton.innerText)
.eql('Disable', 'Expected test participant to have disabled account after enable selected');
}
)

test.skip.meta({
ID: 'MMD-T26',
STORY: 'MMD-376',
Expand Down
2 changes: 1 addition & 1 deletion integration_test/tests/src/tests/FindTransfersPage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { v4 as uuidv4 } from 'uuid';
fixture `Find Transfers Feature`
.page`${config.financePortalEndpoint}`
.before(async (ctx) => {
const cli = new VoodooClient('ws://localhost:3030/voodoo', { defaultTimeout: 15000 });
const cli = new VoodooClient('ws://localhost:3030/voodoo', { defaultTimeout: config.voodooTimeoutMs });
await cli.connected();

const hubAccounts: protocol.HubAccount[] = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ fixture `Settlement windows page`
// isn't handled correctly, causing the root page (i.e. login) to load again.
.page `${config.financePortalEndpoint}`
.before(async (ctx) => {
const cli = new VoodooClient('ws://localhost:3030/voodoo', { defaultTimeout: 15000 });
const cli = new VoodooClient('ws://localhost:3030/voodoo', { defaultTimeout: config.voodooTimeoutMs });
await cli.connected();

const hubAccounts: protocol.HubAccount[] = [
Expand Down
2 changes: 1 addition & 1 deletion integration_test/tests/src/tests/SettlementsPage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { v4 as uuidv4 } from 'uuid';
fixture `Settlements Feature`
.page`${config.financePortalEndpoint}`
.before(async (ctx) => {
const cli = new VoodooClient('ws://localhost:3030/voodoo', { defaultTimeout: 15000 });
const cli = new VoodooClient('ws://localhost:3030/voodoo', { defaultTimeout: config.voodooTimeoutMs });
await cli.connected();

const hubAccounts: protocol.HubAccount[] = [
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "finance-portal-v2-ui",
"version": "2.4.1",
"version": "2.5.0",
"description": "",
"main": "index.js",
"scripts": {
Expand Down
4 changes: 4 additions & 0 deletions src/App/FinancialPositions/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
CLOSE_FINANCIAL_POSITION_UPDATE_CONFIRM_MODAL,
SUBMIT_FINANCIAL_POSITION_UPDATE_CONFIRM_MODAL,
UPDATE_FINANCIAL_POSITION_NDC_AFTER_CONFIRM_MODAL,
TOGGLE_CURRENCY_ACTIVE,
} from './types';

/** Actions for "DFSP Financial Positions" */
Expand All @@ -37,3 +38,6 @@ export const submitFinancialPositionUpdateConfirmModal = createAction(SUBMIT_FIN
export const updateFinancialPositionNDCAfterConfirmModal = createAction(
UPDATE_FINANCIAL_POSITION_NDC_AFTER_CONFIRM_MODAL,
);

/** Actions for "DFSP Financial Positions" > "Enable"/"Disable" button */
export const toggleCurrencyActive = createAction<FinancialPosition>(TOGGLE_CURRENCY_ACTIVE);
1 change: 1 addition & 0 deletions src/App/FinancialPositions/connectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const stateProps = (state: State) => ({
const dispatchProps = (dispatch: Dispatch) => ({
onMount: () => dispatch(actions.requestFinancialPositions()),
onSelectFinancialPosition: (item: FinancialPosition) => dispatch(actions.selectFinancialPosition(item)),
onToggleCurrencyActive: (item: FinancialPosition) => dispatch(actions.toggleCurrencyActive(item)),
});

const connector = connect(stateProps, dispatchProps);
Expand Down
13 changes: 0 additions & 13 deletions src/App/FinancialPositions/helpers.ts

This file was deleted.

96 changes: 66 additions & 30 deletions src/App/FinancialPositions/sagas.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
import { strict as assert } from 'assert';
import { all, call, put, select, takeLatest } from 'redux-saga/effects';
import { getDfsps } from 'App/DFSPs/selectors';
import { DFSP } from 'App/DFSPs/types';
import { PayloadAction } from '@reduxjs/toolkit';
import { all, call, put, select, takeLatest, takeEvery } from 'redux-saga/effects';
import { getDfsps } from '../DFSPs/selectors';
import { DFSP } from '../DFSPs/types';
import { Currency } from '../types';
import apis from '../../utils/apis';
import {
REQUEST_FINANCIAL_POSITIONS,
SUBMIT_FINANCIAL_POSITION_UPDATE_MODAL,
SUBMIT_FINANCIAL_POSITION_UPDATE_CONFIRM_MODAL,
TOGGLE_CURRENCY_ACTIVE,
Account,
Limit,
FinancialPosition,
FinancialPositionsUpdateAction,
} from './types';
import {
closeFinancialPositionUpdateModal,
requestFinancialPositions,
setFinancialPositions,
setFinancialPositionsError,
closeFinancialPositionUpdateConfirmModal,
updateFinancialPositionNDCAfterConfirmModal,
} from './actions';
import * as helpers from './helpers';
import {
getFinancialPositions,
getSelectedFinancialPosition,
Expand All @@ -27,37 +30,70 @@ import {
} from './selectors';

function* fetchDFSPPositions(dfsp: DFSP) {
const { name, id } = dfsp;
const accounts = yield call(apis.participantAccounts.read, { participantName: dfsp.name });
assert.equal(accounts.status, 200, `Failed to retrieve accounts for ${dfsp.name}`);
const limits = yield call(apis.participantLimits.read, { participantName: dfsp.name });
assert.equal(limits.status, 200, `Failed to retrieve limits for ${dfsp.name}`);

const ndc = yield call(apis.netdebitcap.read, { dfspName: name });
const account = yield call(apis.settlementAccount.read, { dfspId: id });
const position = yield call(apis.position.read, { dfspId: id });
const currencies = new Set<Currency>(accounts.data.map((a: Account) => a.currency));

if (ndc.status !== 200 || account.status !== 200 || position.status !== 200) {
throw new Error('Unable to fetch data');
}

return {
return [...currencies].map((c) => ({
dfsp,
balance: parseFloat(account.data[0].settlementBalance),
limits: ndc.data[0].netDebitCap || 0,
positions: parseFloat(position.data[0].position),
};
currency: c,
ndc: limits.data.find((l: Limit) => l.currency === c)?.limit.value,
settlementAccount: accounts.data.find((a: Account) => a.currency === c && a.ledgerAccountType === 'SETTLEMENT'),
positionAccount: accounts.data.find((a: Account) => a.currency === c && a.ledgerAccountType === 'POSITION'),
}));
}

function* updateFinancialPositions(newPositions: FinancialPosition[]) {
const currentPositions: FinancialPosition[] = yield select(getFinancialPositions);
const updatedPositions = currentPositions.map(
(oldPos) =>
newPositions.find((newPos) => newPos.currency === oldPos.currency && newPos.dfsp.id === oldPos.dfsp.id) || oldPos,
);
yield put(setFinancialPositions(updatedPositions));
}

function* reloadFinancialPositionsParticipant(dfsp: DFSP) {
const newDfspPositions = yield call(fetchDFSPPositions, dfsp);
yield call(updateFinancialPositions, newDfspPositions);
}

function* fetchFinancialPositions() {
try {
const dfsps = yield select(getDfsps);
if (!dfsps) {
throw new Error('Unable to fetch data');
}
const data = yield all(dfsps.filter(helpers.isNotHUB).map((dfsp: DFSP) => call(fetchDFSPPositions, dfsp)));
yield put(setFinancialPositions(data));
const dfsps = (yield select(getDfsps)).filter((dfsp: DFSP) => dfsp.name !== 'Hub');
const data = yield all(dfsps.map((dfsp: DFSP) => call(fetchDFSPPositions, dfsp)));
yield put(setFinancialPositions(data.flat()));
} catch (e) {
yield put(setFinancialPositionsError(e.message));
}
}

function* toggleCurrencyActive(action: PayloadAction<FinancialPosition>) {
yield call(updateFinancialPositions, [
{
...action.payload,
positionAccount: {
...action.payload.positionAccount,
updateInProgress: true,
},
},
]);
const { positionAccount, dfsp } = action.payload;
const newIsActive = !positionAccount.isActive;
const description = newIsActive ? 'disable' : 'enable';
const result = yield call(apis.participantAccount.update, {
participantName: dfsp.name,
accountId: positionAccount.id,
body: {
isActive: newIsActive,
},
});
assert.equal(result.status, 200, `Failed to ${description} account ${positionAccount.id}`);
yield call(reloadFinancialPositionsParticipant, dfsp);
}

function* updateFinancialPositionsParticipant() {
const updateAmount = yield select(getFinancialPositionUpdateAmount);
assert(updateAmount !== 0, 'Value 0 is not valid for Amount');
Expand Down Expand Up @@ -113,17 +149,12 @@ function* updateFinancialPositionsParticipant() {
throw new Error('Action not expected on update Financial Position Balance');
}
}

const allPositions: FinancialPosition[] = yield select(getFinancialPositions);
const newPosition = yield call(fetchDFSPPositions, position.dfsp);
const newPositions = allPositions.map((pos) => (pos.dfsp.id === position.dfsp.id ? newPosition : pos));
yield put(setFinancialPositions(newPositions));
yield call(reloadFinancialPositionsParticipant, position.dfsp);
}

function* submitFinancialPositionsUpdateParticipant() {
try {
yield call(updateFinancialPositionsParticipant);
yield put(requestFinancialPositions()); // load position to update the screen
} catch (e) {
yield put(setFinancialPositionsError(e.message));
} finally {
Expand Down Expand Up @@ -157,10 +188,15 @@ export function* SubmitFinancialPositionsUpdateParticipantAndShowUpdateNDCSaga()
);
}

export function* ToggleCurrencyActiveSaga(): Generator {
yield takeEvery([TOGGLE_CURRENCY_ACTIVE], toggleCurrencyActive);
}

export default function* rootSaga(): Generator {
yield all([
FetchFinancialPositionsSaga(),
SubmitFinancialPositionsUpdateParticipantSaga(),
SubmitFinancialPositionsUpdateParticipantAndShowUpdateNDCSaga(),
ToggleCurrencyActiveSaga(),
]);
}
29 changes: 25 additions & 4 deletions src/App/FinancialPositions/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ErrorMessage } from 'App/types';
import { ErrorMessage, Currency } from 'App/types';
import { DFSP } from 'App/DFSPs/types';
// Removed because we reverted the version of this package due to a bug
// import { composeOptions } from '@modusbox/modusbox-ui-components/dist/utils/html';
Expand All @@ -13,6 +13,8 @@ export const SUBMIT_FINANCIAL_POSITION_UPDATE_MODAL = 'Financial Positions / Sub
export const SET_FINANCIAL_POSITION_UPDATE_AMOUNT = 'Financial Positions / Set Financial Position Update Amount';
export const SET_FINANCIAL_POSITION_UPDATE_ACTION = 'Financial Positions / Set Financial Position Update Action';

export const TOGGLE_CURRENCY_ACTIVE = 'Financial Positions / Toggle Currency Active';

export const SHOW_FINANCIAL_POSITION_UPDATE_CONFIRM_MODAL =
'Financial Positions / Show Financial Position Update Confirm Modal';
export const CLOSE_FINANCIAL_POSITION_UPDATE_CONFIRM_MODAL =
Expand All @@ -29,12 +31,31 @@ const composeOptions = (opts: any) => {
}));
};

export interface Limit {
currency: Currency;
limit: { value: number };
}

export interface Account {
updateInProgress: boolean;
changedDate: string;
currency: Currency;
id: number;
isActive: boolean;
ledgerAccountType: 'POSITION' | 'SETTLEMENT';
value: number;
}

export interface FinancialPosition {
dfsp: DFSP;
balance: number;
limits: number;
positions: number;
currency: Currency;
settlementAccount: Account;
positionAccount: Account;
// Optional because when the position account is set inactive the limit is not returned.
// TODO: raise an issue about this; it doesn't seem sensible
ndc?: number;
}

export enum FinancialPositionsUpdateAction {
AddFunds = 'ADD_FUNDS',
WithdrawFunds = 'WITHDRAW_FUNDS',
Expand Down
Loading

0 comments on commit 3e92eb2

Please sign in to comment.