Skip to content

Commit

Permalink
GRWT-3133 / Kate / [DTrader-V2] Tech Debt: Refactor Stake component (#…
Browse files Browse the repository at this point in the history
…17620)

* refactor: create new stake component

* feat: add function for forming proposal request body

* feat: add be and fe validation

* refactor: decomposition

* chore: uncommited trade param

* chore: structure refactoring

* feat: handle turbos and vanillas cases

* refactor: rely on is_fetching flag

* feat: add error handling in snackbar

* revert: extra non-aligned file from master

* feat: add logic for multipliers

* refactor: stake delais structure

* feat: add another proposal for rise fall

* fix: types

* revert: extra non-aligned file from master

* feat: add logic for exctraction payout and max payout

* feat: add rise fall

* refactor: improve snackbar behaviour

* chore: exctract function to utils

* refactor: add tests for stake

* refactor: add tests for payout function

* chore: replace array method

* chore: spelling

* fix: reset commission and stop out

* refactor: usereducer, created hook for fetching data and exctract logic for handling response

* refactor: extract logic for state initiation into a function

* refactor: handle more cases with fe error

* feat: add max length restriction

* refactor: rewrite with the usage of reducer

* chore: add string wrapper
  • Loading branch information
kate-deriv authored Jan 2, 2025
1 parent 2a4fac4 commit 6564b3a
Show file tree
Hide file tree
Showing 14 changed files with 876 additions and 451 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import { CONTRACT_TYPES, TRADE_TYPES } from '@deriv/shared';
import { mockStore } from '@deriv/stores';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import { useDtraderQuery } from 'AppV2/Hooks/useDtraderQuery';
import ModulesProvider from 'Stores/Providers/modules-providers';

import TraderProviders from '../../../../../trader-providers';
import Stake from '../stake';

Expand All @@ -29,6 +33,24 @@ jest.mock('AppV2/Hooks/useContractsForCompany', () => ({
},
})),
}));
jest.mock('@deriv/shared', () => ({
...jest.requireActual('@deriv/shared'),
WS: {
send: jest.fn(),
authorized: {
send: jest.fn(),
},
},
}));
jest.mock('AppV2/Hooks/useDtraderQuery', () => ({
...jest.requireActual('AppV2/Hooks/useDtraderQuery'),
useDtraderQuery: jest.fn(() => ({
data: {
proposal: {},
error: {},
},
})),
}));

describe('Stake', () => {
let default_mock_store: ReturnType<typeof mockStore>;
Expand All @@ -45,14 +67,12 @@ describe('Stake', () => {
currency: 'USD',
proposal_info: {
[CONTRACT_TYPES.CALL]: {
id: '53e8cb91-8c13-60a3-289f-778e8386367c',
has_error: false,
message:
'Win payout if Volatility 100 (1s) Index is strictly higher than entry spot at 5 minutes after contract start time.',
payout: 19.55,
},
[CONTRACT_TYPES.PUT]: {
id: '2b5dd806-7505-8af7-1bbb-5e24ac48bbbc',
has_error: false,
message:
'Win payout if Volatility 100 (1s) Index is strictly lower than entry spot at 5 minutes after contract start time.',
Expand All @@ -63,14 +83,11 @@ describe('Stake', () => {
[CONTRACT_TYPES.CALL]: 'Higher',
[CONTRACT_TYPES.PUT]: 'Lower',
},
validation_errors: { amount: [] },
trade_type_tab: 'CALL',
validation_params: {
[CONTRACT_TYPES.CALL]: { payout: { max: '50000.00' } },
[CONTRACT_TYPES.PUT]: { payout: { max: '50000.00' } },
},
v2_params_initial_values: {
stake: 10,
},
},
},
}))
Expand All @@ -84,15 +101,6 @@ describe('Stake', () => {
</TraderProviders>
);

it('switches basis to stake if it is different', () => {
default_mock_store.modules.trade.basis = 'payout';
render(<MockedStake />);

expect(default_mock_store.modules.trade.onChange).toHaveBeenCalledWith({
target: { name: 'basis', value: 'stake' },
});
});

it('renders trade param with "Stake" label and input with a value equal to the current stake amount value', () => {
render(<MockedStake />);
const { amount, currency } = default_mock_store.modules.trade;
Expand All @@ -109,16 +117,19 @@ describe('Stake', () => {

expect(screen.getByTestId('dt-actionsheet-overlay')).toBeInTheDocument();
expect(screen.getByPlaceholderText(input_placeholder)).toBeInTheDocument();
expect(screen.getAllByText(/payout/i)).toHaveLength(3);
expect(screen.getAllByText(/payout/i)).toHaveLength(2);
expect(screen.getByRole('button', { name: save_button_label })).toBeInTheDocument();
});

it('calls onChange when stake input changes', async () => {
it('calls onChange if user clicks on Save', async () => {
render(<MockedStake />);

await userEvent.click(screen.getByText(stake_param_label));
await userEvent.type(screen.getByPlaceholderText(input_placeholder), '0');
await userEvent.type(screen.getByPlaceholderText(input_placeholder), '10');
await userEvent.click(screen.getByRole('button', { name: save_button_label }));

expect(default_mock_store.modules.trade.onChange).toHaveBeenCalledWith({
target: { name: 'amount', value: '100' },
target: { name: 'amount', value: 10 },
});
});

Expand Down Expand Up @@ -157,14 +168,12 @@ describe('Stake', () => {
is_multiplier: true,
proposal_info: {
[CONTRACT_TYPES.MULTIPLIER.UP]: {
id: '3b09df15-b0b7-70a8-15c9-fad8e2bda5ef',
has_error: false,
message:
"If you select 'Up', your total profit/loss will be the percentage increase in Volatility 100 (1s) Index, multiplied by 1000, minus commissions.",
payout: 0,
},
[CONTRACT_TYPES.MULTIPLIER.DOWN]: {
id: '873af3a9-a0da-5486-d1f9-fce8206e6ba2',
has_error: false,
message:
"If you select 'Down', your total profit/loss will be the percentage decrease in Volatility 100 (1s) Index, multiplied by 1000, minus commissions.",
Expand Down Expand Up @@ -202,84 +211,25 @@ describe('Stake', () => {
expect(screen.getByText('Commission')).toBeInTheDocument();
});

it('calls setV2ParamsInitialValues if v2_params_initial_values.stake !== amount on mount and on Save button click if no error', async () => {
default_mock_store.modules.trade.amount = '30';
render(<MockedStake />);
await userEvent.click(screen.getByText(stake_param_label));
await userEvent.type(screen.getByPlaceholderText(input_placeholder), '0');

expect(default_mock_store.modules.trade.setV2ParamsInitialValues).toHaveBeenCalledTimes(2);

await userEvent.click(screen.getByRole('button', { name: save_button_label }));
expect(default_mock_store.modules.trade.setV2ParamsInitialValues).toHaveBeenCalledTimes(3);
});

it('calls onChange on component mount if v2_params_initial_values.stake is not equal to amount', () => {
default_mock_store.modules.trade.amount = '30';
render(<MockedStake />);
expect(default_mock_store.modules.trade.onChange).toHaveBeenCalledWith({
target: { name: 'amount', value: 10 },
});
});

it('shows error in case of a validation error if input is non-empty', async () => {
const error_text = "Please enter a stake amount that's at least 0.35.";
default_mock_store.modules.trade.contract_type = TRADE_TYPES.HIGH_LOW;
default_mock_store.modules.trade.trade_type_tab = 'CALL';
default_mock_store.modules.trade.proposal_info = {
PUT: { id: '', has_error: true, message: error_text },
CALL: { id: '', has_error: true, message: error_text },
CALL: { has_error: true, message: error_text, error_field: 'amount' },
};
default_mock_store.modules.trade.validation_errors.amount = [error_text];
default_mock_store.modules.trade.amount = 0;

render(<MockedStake />);
await userEvent.click(screen.getByText(stake_param_label));
expect(screen.getByText(error_text)).toBeInTheDocument();
expect(screen.getAllByText('- USD')).toHaveLength(2);
});

it('shows max payout error with the least current payout when both of the 2 contract types exceed max payout', async () => {
const error_text_rise = 'Minimum stake of 0.35 and maximum payout of 50000.00. Current payout is 50631.97.';
const error_text_fall = 'Minimum stake of 0.35 and maximum payout of 50000.00. Current payout is 50513.21.';
default_mock_store.modules.trade.proposal_info = {
CALL: { id: '', has_error: true, message: error_text_rise, error_field: 'amount' },
PUT: { id: '', has_error: true, message: error_text_fall, error_field: 'amount' },
};
default_mock_store.modules.trade.validation_errors.amount = [error_text_fall];
default_mock_store.modules.trade.amount = '26500';

render(<MockedStake />);
await userEvent.click(screen.getByText(stake_param_label));

expect(screen.getByText(error_text_fall)).toBeInTheDocument();
expect(screen.queryByText('- USD')).not.toBeInTheDocument();
});

it('does not show max payout error if one of the 2 contract types satisfies max payout', async () => {
const error_text_rise = 'Minimum stake of 0.35 and maximum payout of 50000.00. Current payout is 50058.77.';
const success_text_fall =
'Win payout if Volatility 100 (1s) Index is strictly lower than entry spot at 5 minutes after contract start time.';
default_mock_store.modules.trade.proposal_info = {
CALL: { id: '', has_error: true, message: error_text_rise },
PUT: {
id: 'b608baf2-ba5d-00e0-8035-9af5c0769664',
has_error: false,
message: success_text_fall,
payout: 49942.7,
(useDtraderQuery as jest.Mock).mockReturnValue({
data: {
error: { has_error: true, message: error_text, details: { error_field: 'amount' } },
proposal: {},
},
};
default_mock_store.modules.trade.amount = '26200';

render(<MockedStake />);
await userEvent.click(screen.getByText(stake_param_label));

expect(screen.queryByText(error_text_rise)).not.toBeInTheDocument();
});

it('sets default stake if available_contract_types object contains it', () => {
default_mock_store.modules.trade.contract_type = TRADE_TYPES.VANILLA.CALL;
});
render(<MockedStake />);

expect(default_mock_store.modules.trade.setDefaultStake).toHaveBeenCalledWith(10);
await userEvent.click(screen.getByText(stake_param_label));
expect(screen.getByText(error_text)).toBeInTheDocument();
expect(screen.getByText('- USD')).toBeInTheDocument();
});

it('disables trade param if is_market_closed == true', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Stake from './stake';

import './stake.scss';

export default Stake;
Original file line number Diff line number Diff line change
@@ -1,95 +1,95 @@
import React from 'react';
import clsx from 'clsx';
import { Text } from '@deriv-com/quill-ui';

import { formatMoney, getCurrencyDisplayCode, getTradeTypeName, TRADE_TYPES } from '@deriv/shared';
import { Localize } from '@deriv/translations';
import { Text } from '@deriv-com/quill-ui';

import { TTradeStore } from 'Types';

type TStakeDetailsProps = Pick<
TTradeStore,
'commission' | 'contract_type' | 'currency' | 'has_stop_loss' | 'is_multiplier' | 'stop_out'
> & {
type TStakeDetailsProps = Pick<TTradeStore, 'contract_type' | 'currency' | 'has_stop_loss' | 'is_multiplier'> & {
contract_types: string[];
details: {
commission?: string | number;
error_1?: string;
error_2?: string;
first_contract_payout: number;
is_first_payout_exceeded?: boolean;
is_second_payout_exceeded?: boolean;
max_payout: string | number;
max_stake: string | number;
min_stake: string | number;
second_contract_payout: number;
stop_out?: number | string;
};
is_loading_proposal: boolean;
is_max_payout_exceeded: boolean;
is_empty?: boolean;
should_show_payout_details: boolean;
stake_error: string;
};

const StakeDetails = ({
commission,
contract_type,
contract_types,
currency,
details,
has_stop_loss,
is_loading_proposal,
is_multiplier,
is_max_payout_exceeded,
is_empty,
should_show_payout_details,
stake_error,
stop_out,
}: TStakeDetailsProps) => {
const [displayed_values, setDisplayedValues] = React.useState({
max_payout: '',
is_first_payout_exceeded: false,
is_second_payout_exceeded: false,
commission: '',
first_contract_payout: '',
max_payout: '',
second_contract_payout: '',
stop_out: '',
});

React.useEffect(() => {
const getDisplayedValue = (new_value?: number | string, current_value?: string) => {
return ((current_value === '-' && is_loading_proposal) || stake_error) && !is_max_payout_exceeded
return (current_value === '-' && is_loading_proposal) || !new_value || is_empty
? '-'
: formatMoney(currency, Number(new_value), true);
};

const {
commission: commission_value,
first_contract_payout,
is_first_payout_exceeded,
is_second_payout_exceeded,
second_contract_payout,
stop_out: stop_out_value,
max_payout,
} = displayed_values;
const new_commission = getDisplayedValue(Math.abs(Number(commission)), commission_value);
const new_commission = getDisplayedValue(Math.abs(Number(details.commission)), commission_value);
const new_payout_1 = getDisplayedValue(details.first_contract_payout, first_contract_payout);
const new_payout_2 = getDisplayedValue(details.second_contract_payout, second_contract_payout);
const new_stop_out = getDisplayedValue(Math.abs(Number(stop_out)), stop_out_value);
const new_stop_out = getDisplayedValue(Math.abs(Number(details.stop_out)), stop_out_value);
const new_max_payout = getDisplayedValue(details.max_payout, max_payout);

if (
commission_value !== new_commission ||
first_contract_payout !== new_payout_1 ||
displayed_values.is_first_payout_exceeded !== is_first_payout_exceeded ||
displayed_values.is_second_payout_exceeded !== is_second_payout_exceeded ||
second_contract_payout !== new_payout_2 ||
stop_out_value !== new_stop_out ||
max_payout !== new_max_payout
) {
setDisplayedValues({
commission: new_commission,
first_contract_payout: new_payout_1,
is_first_payout_exceeded,
is_second_payout_exceeded,
second_contract_payout: new_payout_2,
stop_out: new_stop_out,
max_payout: new_max_payout,
});
}
}, [
commission,
currency,
details,
displayed_values,
is_loading_proposal,
is_max_payout_exceeded,
stake_error,
stop_out,
]);
}, [currency, details, displayed_values, is_loading_proposal, is_empty]);

const payout_title = <Localize i18n_default_text='Payout' />;
const content = [
Expand All @@ -114,7 +114,7 @@ const StakeDetails = ({
}),
is_displayed: !!contract_types.length && should_show_payout_details,
label: payout_title,
has_error: details.first_contract_payout > +details.max_payout && is_max_payout_exceeded,
has_error: details.is_first_payout_exceeded,
value: displayed_values.first_contract_payout,
},
{
Expand All @@ -123,7 +123,7 @@ const StakeDetails = ({
}),
is_displayed: contract_types.length > 1 && should_show_payout_details,
label: payout_title,
has_error: details.second_contract_payout > +details.max_payout && is_max_payout_exceeded,
has_error: details.is_second_payout_exceeded,
value: displayed_values.second_contract_payout,
},
];
Expand Down
Loading

0 comments on commit 6564b3a

Please sign in to comment.