feat: implement auto entries rates re-calculation after change the exchange rate

This commit is contained in:
Ahmed Bouhuolia
2024-01-14 14:44:48 +02:00
parent 2cb8c2932f
commit 2b03ac7f16
24 changed files with 522 additions and 241 deletions

View File

@@ -0,0 +1,52 @@
import { useExchangeRate } from '@/hooks/query';
import { useCurrentOrganization } from '@/hooks/state';
import React from 'react';
interface AutoExchangeRateProviderProps {
children: React.ReactNode;
}
interface AutoExchangeRateProviderValue {
autoExRateCurrency: string;
isAutoExchangeRateLoading: boolean;
}
const AutoExchangeRateContext = React.createContext(
{} as AutoExchangeRateProviderValue,
);
function AutoExchangeRateProvider({ children }: AutoExchangeRateProviderProps) {
const [autoExRateCurrency, setAutoExRateCurrency] =
React.useState<string>('');
const currentOrganization = useCurrentOrganization();
// Retrieves the exchange rate.
const { data: autoExchangeRate, isLoading: isAutoExchangeRateLoading } =
useExchangeRate(autoExRateCurrency, currentOrganization.base_currency, {
enabled: Boolean(currentOrganization.base_currency && autoExRateCurrency),
refetchOnWindowFocus: false,
staleTime: 0,
cacheTime: 0,
});
const value = {
autoExRateCurrency,
setAutoExRateCurrency,
isAutoExchangeRateLoading,
autoExchangeRate,
};
return (
<AutoExchangeRateContext.Provider value={value}>
{children}
</AutoExchangeRateContext.Provider>
);
}
const useAutoExRateContext = () => React.useContext(AutoExchangeRateContext);
export {
useAutoExRateContext,
AutoExchangeRateContext,
AutoExchangeRateProvider,
};

View File

@@ -0,0 +1,88 @@
// @ts-nocheck
import React from 'react';
import { useFormikContext } from 'formik';
import { round } from 'lodash';
import * as R from 'ramda';
import { updateItemsEntriesTotal } from './utils';
/**
* Convert the given rate to the local currency.
* @param {number} rate
* @param {number} exchangeRate
* @returns {number}
*/
export const convertToForeignCurrency = (
rate: number,
exchangeRate: number,
) => {
return rate * exchangeRate;
};
/**
* Converts the given rate to the base currency.
* @param {number} rate
* @param {number} exchangeRate
* @returns {number}
*/
export const covertToBaseCurrency = (rate: number, exchangeRate: number) => {
return rate / exchangeRate;
};
/**
* Reverts the given rate from the old exchange rate and covert it to the new
* currency based on the given new exchange rate.
* @param {number} rate -
* @param {number} oldExchangeRate - Old exchange rate.
* @param {number} newExchangeRate - New exchange rate.
* @returns {number}
*/
const revertAndConvertExchangeRate = (
rate: number,
oldExchangeRate: number,
newExchangeRate: number,
) => {
const oldValue = convertToForeignCurrency(rate, oldExchangeRate);
const newValue = covertToBaseCurrency(oldValue, newExchangeRate);
return round(newValue, 3);
};
/**
* Assign the new item entry rate after converting to the new exchange rate.
* @params {number} oldExchangeRate -
* @params {number} newExchangeRate -
* @params {IItemEntry} entries -
*/
const assignRateRevertAndCovertExchangeRate = R.curry(
(oldExchangeRate: number, newExchangeRate: number, entries: IITemEntry[]) => {
return entries.map((entry) => ({
...entry,
rate: revertAndConvertExchangeRate(
entry.rate,
oldExchangeRate,
newExchangeRate,
),
}));
},
);
/**
* Updates items entries on exchange rate change.
* @returns {(oldExchangeRate: number, newExchangeRate: number) => IItemEntry[]}
*/
export const useUpdateEntriesOnExchangeRateChange = () => {
const {
values: { entries },
} = useFormikContext();
return React.useMemo(() => {
return R.curry((oldExchangeRate: number, newExchangeRate: number) => {
return R.compose(
// Updates entries total.
updateItemsEntriesTotal,
// Assign a new rate of the given new exchange rate from the old exchange rate.
assignRateRevertAndCovertExchangeRate(oldExchangeRate, newExchangeRate),
)(entries);
});
}, [entries]);
};

View File

@@ -0,0 +1,121 @@
// @ts-nocheck
import { useFormikContext } from 'formik';
import { useUpdateEntriesOnExchangeRateChange } from './useUpdateEntriesOnExchangeRateChange';
import { useAutoExRateContext } from './AutoExchangeProvider';
import { useCallback, useEffect } from 'react';
import { useCurrentOrganization } from '@/hooks/state';
/**
* Re-calculate the item entries prices based on the old exchange rate.
* @param {InvoiceExchangeRateInputFieldRoot} Component
* @returns {JSX.Element}
*/
export const withExchangeRateItemEntriesPriceRecalc =
(Component) => (props) => {
const { setFieldValue } = useFormikContext();
const updateChangeExRate = useUpdateEntriesOnExchangeRateChange();
return (
<Component
onRecalcConfirm={({ exchangeRate, oldExchangeRate }) => {
setFieldValue(
'entries',
updateChangeExRate(oldExchangeRate, exchangeRate),
);
}}
{...props}
/>
);
};
/**
* Injects the loading props to the exchange rate field.
* @param Component
* @returns {}
*/
export const withExchangeRateFetchingLoading = (Component) => (props) => {
const { isAutoExchangeRateLoading } = useAutoExRateContext();
return (
<Component
isLoading={isAutoExchangeRateLoading}
inputGroupProps={{
disabled: isAutoExchangeRateLoading,
}}
/>
);
};
/**
* Updates the customer currency code and exchange rate once you update the customer
* then change the state to fetch the realtime exchange rate of the new selected currency.
*/
export const useCustomerUpdateExRate = () => {
const { setFieldValue, values } = useFormikContext();
const { setAutoExRateCurrency } = useAutoExRateContext();
const updateEntriesOnExChange = useUpdateEntriesOnExchangeRateChange();
const currentCompany = useCurrentOrganization();
const DEFAULT_EX_RATE = 1;
return useCallback(
(customer) => {
// Reset the auto exchange rate currency cycle.
setAutoExRateCurrency(null);
// If the customer's currency code equals the same base currency.
if (customer.currency_code === currentCompany.base_currency) {
setFieldValue('exchange_rate', DEFAULT_EX_RATE + '');
setFieldValue(
'entries',
updateEntriesOnExChange(values.exchange_rate, DEFAULT_EX_RATE),
);
} else {
// Sets the currency code to fetch exchange rate of the given currency code.
setAutoExRateCurrency(customer?.currency_code);
}
},
[
currentCompany.base_currency,
setAutoExRateCurrency,
setFieldValue,
updateEntriesOnExChange,
values.exchange_rate,
],
);
};
interface UseSyncExRateToFormProps {
onSynced?: () => void;
}
/**
* Syncs the realtime exchange rate to the Formik form and then re-calculates
* the entries rate based on the given new and old ex. rate.
* @param {UseSyncExRateToFormProps} props -
* @returns {React.ReactNode}
*/
export const useSyncExRateToForm = ({ onSynced }: UseSyncExRateToFormProps) => {
const { setFieldValue, values } = useFormikContext();
const { autoExRateCurrency, autoExchangeRate } = useAutoExRateContext();
const updateEntriesOnExChange = useUpdateEntriesOnExchangeRateChange();
// Sync the fetched real-time exchanage rate to the form.
useEffect(() => {
if (autoExchangeRate?.exchange_rate && autoExRateCurrency) {
setFieldValue('exchange_rate', autoExchangeRate?.exchange_rate + '');
setFieldValue(
'entries',
updateEntriesOnExChange(
values.exchange_rate,
autoExchangeRate?.exchange_rate,
),
);
onSynced?.();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [autoExchangeRate?.exchange_rate, autoExRateCurrency]);
return null;
};