Skip to content

Commit

Permalink
chore: intermediate progress
Browse files Browse the repository at this point in the history
  • Loading branch information
letehaha committed Nov 7, 2024
1 parent c4cd517 commit bc091a1
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 40 deletions.
2 changes: 1 addition & 1 deletion docker/dev/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ services:
- POSTGRES_USER=${APPLICATION_DB_USERNAME}
- POSTGRES_PASSWORD=${APPLICATION_DB_PASSWORD}
- POSTGRES_DB=${APPLICATION_DB_DATABASE}
ports: ['${APPLICATION_DB_PORT}:5432']
ports: ['${APPLICATION_DB_PORT}:${APPLICATION_DB_PORT}']

redis:
image: redis:6
Expand Down
2 changes: 2 additions & 0 deletions src/services/calculate-ref-amount.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ async function calculateRefAmountImpl(params: Params): Promise<number> {
});
const rate = result.rate;

console.log('rate', rate);

const isNegative = amount < 0;
const refAmount = amount === 0 ? 0 : Math.floor(Math.abs(amount) * rate);

Expand Down
174 changes: 168 additions & 6 deletions src/services/investments/transactions/create.service.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ describe('Create investment transaction service', () => {
await helpers.syncSecuritiesData();
const securities = await helpers.getSecuritiesList({ raw: true });
const mockedSecurity = securities[0]!;
const account = await helpers.createAccount({ raw: true });
const account = await helpers.createInvestmentAccount({
raw: true,
});

const balances = await helpers.makeRequest({
method: 'get',
Expand Down Expand Up @@ -70,6 +72,8 @@ describe('Create investment transaction service', () => {
raw: true,
});

console.log('accountUpdated', accountUpdated);

expect(holdings.length).toBe(1);
expect(transactions.length).toBe(1);

Expand Down Expand Up @@ -106,11 +110,169 @@ describe('Create investment transaction service', () => {
{ date: format(new Date(), 'yyyy-MM-dd'), amount: expectedAccountBalance },
]);
});
it.todo(
'correctly works for non-base currency (ref values are correct for tx, holdings, and account balance)',
);
it.todo('after creation, the balances table is updated correctly');
it.todo('after creation, statistics are updated correctly');

it.skip('correctly works for non-base currency with multiple securities', async () => {
const currencyUAH = global.MODELS_CURRENCIES.find((item) => item.code === 'UAH');
// Change base currency to UAH so that we can calculate securities to ref currency
await helpers.makeRequest({
method: 'post',
url: '/user/currencies/base',
payload: { currencyId: currencyUAH.id },
});

await helpers.syncSecuritiesData();
// Set up EUR currency and exchange rates
const allCurrencies = await helpers.getAllCurrencies();
const eur = allCurrencies.find((i) => i.code === 'EUR')!;
const securities = await helpers.getSecuritiesList({ raw: true });
const security1 = securities[0]!;
const security2 = securities[1]!;
const account = await helpers.createAccount({
raw: true,
payload: helpers.buildAccountPayload({ currencyId: eur.id }),
});

await helpers.makeRequest({
method: 'post',
url: '/user/currencies',
payload: { currencies: [{ currencyId: eur.id }] },
raw: false,
});
await helpers.editCurrencyExchangeRate({
pairs: [
{ baseCode: 'USD', quoteCode: 'EUR', rate: 0.85 },
{ baseCode: 'EUR', quoteCode: 'USD', rate: 1.18 },
],
});

// Create holdings for both securities
for (const security of [security1, security2]) {
await helpers.createHolding({
payload: { accountId: account.id, securityId: security.id },
});
}

const transactions = [
{
securityId: security1.id,
quantity: 10,
price: 25.1,
fees: 0.25,
date: '2024-05-25',
transactionType: TRANSACTION_TYPES.income,
},
{
securityId: security2.id,
quantity: 5,
price: 50.0,
fees: 0.5,
date: '2024-05-26',
transactionType: TRANSACTION_TYPES.income,
},
{
securityId: security1.id,
quantity: 3,
price: 27.5,
fees: 0.2,
date: '2024-05-27',
transactionType: TRANSACTION_TYPES.expense,
},
{
securityId: security2.id,
quantity: 2,
price: 52.0,
fees: 0.3,
date: '2024-05-28',
transactionType: TRANSACTION_TYPES.income,
},
];

for (const tx of transactions) {
await helpers.createInvestmentTransaction({
payload: {
...tx,
accountId: account.id,
date: new Date(tx.date).toISOString(),
},
});
}

const holdings = await helpers.getHoldings({ raw: true });
const createdTransactions = await helpers.makeRequest({
method: 'get',
url: '/investing/transactions',
payload: { accountId: account.id },
raw: true,
});
// const accountUpdated = await helpers.getAccount({ id: account.id, raw: true });

expect(holdings.length).toBe(2);
expect(createdTransactions.length).toBe(transactions.length);

// Calculate expected values for each security
// const expectedValues = securities.slice(0, 2).map((security) => {
// const securityTxs = transactions.filter((tx) => tx.securityId === security.id);
// const quantity = securityTxs.reduce(
// (sum, tx) =>
// tx.transactionType === TRANSACTION_TYPES.income ? sum + tx.quantity : sum - tx.quantity,
// 0,
// );
// const eurValue = securityTxs.reduce((sum, tx) => {
// const txAmount = tx.quantity * tx.price;
// return tx.transactionType === TRANSACTION_TYPES.income ? sum + txAmount : sum - txAmount;
// }, 0);
// const eurFees = securityTxs.reduce((sum, tx) => sum + tx.fees, 0);
// const eurTotalValue = eurValue - eurFees;
// const usdTotalValue = Math.round(eurTotalValue * 1.18 * 100) / 100;
// return { securityId: security.id, quantity, eurTotalValue, usdTotalValue };
// });

// console.log('expectedValues', expectedValues);
// console.log('holdings', holdings);

const holdingA = holdings.find((h) => h.securityId === security1.id)!;
console.log('holdingA', holdingA);
expect({
quantity: Number(holdingA.quantity),
costBasis: Number(holdingA.costBasis),
refCostBasis: Number(holdingA.refCostBasis),
}).toStrictEqual({
quantity: 7,
costBasis: 10 * 25.1 + 0.25 - 3 * 27.5 + 0.2,
refCostBasis: (10 * 25.1 + 0.25) * 1.18 - (3 * 27.5 + 0.2) * 1.18,
});
// const holdingB = holdings.find((h) => h.securityId === security1.id)!;

// holdings.forEach((holding) => {
// const expected = expectedValues.filter((v) => v.securityId === holding.securityId)!;
// expect(Number(holding.quantity)).toBe(expected.reduce((a, b) => a + b.quantity, 0));
// expect(Number(holding.value)).toBeCloseTo(
// expected.reduce((a, b) => a + b.eurTotalValue, 0),
// 2,
// );
// expect(Number(holding.refValue)).toBeCloseTo(
// expected.reduce((a, b) => a + b.usdTotalValue, 0),
// 2,
// );
// });

// createdTransactions.forEach((tx, index) => {
// const originalTx = transactions[index]!;
// expect(tx.securityId).toBe(originalTx.securityId);
// expect(Number(tx.quantity)).toBe(originalTx.quantity);
// expect(Number(tx.price)).toBe(originalTx.price);
// expect(Number(tx.fees)).toBe(originalTx.fees);
// expect(tx.transactionType).toBe(originalTx.transactionType);

// const txEurAmount = originalTx.quantity * originalTx.price;
// expect(Number(tx.amount)).toBeCloseTo(txEurAmount, 2);
// expect(Number(tx.refAmount)).toBeCloseTo(txEurAmount * 1.18, 2);
// });

// const totalUsdValue = expectedValues.reduce((sum, v) => sum + v.usdTotalValue, 0);
// const expectedAccountBalance = Math.round(totalUsdValue * 100);
// expect(accountUpdated.currentBalance).toBe(expectedAccountBalance);
});

describe('failure cases', () => {
it(`throws when trying to create transaction when:
Expand Down
70 changes: 54 additions & 16 deletions src/services/investments/transactions/create.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ export const createInvestmentTransaction = withTransaction(
},
});

const initialAccount = (await Accounts.findOne({
where: { id: params.accountId, userId },
}))!;

console.log('initialAccount_balance', initialAccount.currentBalance);

if (!security) {
throw new ValidationError({
message: `Security with id ${params.securityId} does not exist.`,
Expand Down Expand Up @@ -75,6 +81,10 @@ export const createInvestmentTransaction = withTransaction(
? INVESTMENT_TRANSACTION_CATEGORY.buy
: INVESTMENT_TRANSACTION_CATEGORY.sell,
};

// NFLX-USD – security, but base currency is UAH, means that transaction will have
// amount in USD, and refAmount in {baseCurrency} (UAH)
// Investment account strictly has currencyCode as baseCurrency
const amount = parseFloat(params.quantity) * parseFloat(params.price);
const refAmount = await calculateRefAmount({
amount,
Expand Down Expand Up @@ -102,23 +112,40 @@ export const createInvestmentTransaction = withTransaction(
refFees: String(refFees),
});

const newQuantity = parseFloat(currentHolding.quantity) + parseFloat(params.quantity);
const value = newQuantity * parseFloat(params.price);
console.log('result', result);

const newQuantity =
result.category === INVESTMENT_TRANSACTION_CATEGORY.buy
? parseFloat(currentHolding.quantity) + parseFloat(params.quantity)
: parseFloat(currentHolding.quantity) - parseFloat(params.quantity);

const refValue = await calculateRefAmount({
amount: value,
const newHoldingValue = newQuantity * parseFloat(params.price);

console.log('newHoldingValue', newHoldingValue);

const newHoldingRefValue = await calculateRefAmount({
amount: newHoldingValue,
userId,
baseCode: security.currencyCode,
});

const newCostBasis: number =
parseFloat(currentHolding.costBasis) + amount + parseFloat(params.fees);
const newRefCostBasis: number = parseFloat(currentHolding.refCostBasis) + refAmount + refFees;
console.log('newHoldingRefValue', newHoldingRefValue);

const [newCostBasis, newRefCostBasis] =
params.transactionType === TRANSACTION_TYPES.income
? [
parseFloat(currentHolding.costBasis) + amount + parseFloat(params.fees),
parseFloat(currentHolding.refCostBasis) + refAmount + refFees,
]
: [
parseFloat(currentHolding.costBasis) - amount + parseFloat(params.fees),
parseFloat(currentHolding.refCostBasis) - refAmount + refFees,
];

await Holding.update(
const [, updatedHoldings] = await Holding.update(
{
value: String(value),
refValue: String(refValue),
value: String(newHoldingValue),
refValue: String(newHoldingRefValue),
quantity: String(newQuantity),
costBasis: String(newCostBasis),
refCostBasis: String(newRefCostBasis),
Expand All @@ -128,27 +155,38 @@ export const createInvestmentTransaction = withTransaction(
accountId: params.accountId,
securityId: params.securityId,
},
returning: true,
},
);
const updatedHolding = updatedHoldings[0]!;

const currency = await Currencies.findOne({
where: { code: security.currencyCode },
});

const account = (await Accounts.findOne({
where: {
id: params.accountId,
userId,
},
where: { id: params.accountId, userId },
}))!;

console.log({
userId,
accountId: params.accountId,
transactionType: params.transactionType,
// We store amounts in Account as integer, so need to mutiply that by 100
amount: Math.floor((parseFloat(updatedHolding.costBasis) + amount) * 100),
refAmount: Math.floor((parseFloat(updatedHolding.refCostBasis) + refAmount) * 100),
currencyId: currency!.id,
accountType: account.type,
time: new Date(params.date).toISOString(),
});

await updateAccountBalanceForChangedTx({
userId,
accountId: params.accountId,
transactionType: params.transactionType,
// We store amounts in Account as integer, so need to mutiply that by 100
amount: Math.floor((parseFloat(currentHolding.costBasis) + amount) * 100),
refAmount: Math.floor((parseFloat(currentHolding.refCostBasis) + refAmount) * 100),
amount: Math.floor((parseFloat(updatedHolding.costBasis) + amount) * 100),
refAmount: Math.floor((parseFloat(updatedHolding.refCostBasis) + refAmount) * 100),
currencyId: currency!.id,
accountType: account.type,
time: new Date(params.date).toISOString(),
Expand Down
6 changes: 4 additions & 2 deletions src/services/investments/transactions/delete.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export const deleteInvestmentTransaction = withTransaction(
parseFloat(currentTx.refAmount) +
parseFloat(currentTx.refFees);

const [, [updatedHolding]] = await Holding.update(
const [, updatedHoldings] = await Holding.update(
{
value: String(newValue),
refValue: String(newRefValue),
Expand All @@ -83,11 +83,13 @@ export const deleteInvestmentTransaction = withTransaction(
returning: true,
},
);
// TODO: when holding quantity turns to 0, make 0 all other fields too.
// `costBasis` cannot be positive or negative when `quantity` is 0
const updatedHolding = updatedHoldings[0]!;

// Recalculate account balance

// TODO: maybe not "old costBasis - new costBasis", but "old value - new value"?
if (!updatedHolding) return undefined;

const account = (await Accounts.findOne({
where: {
Expand Down
Loading

0 comments on commit bc091a1

Please sign in to comment.