Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Investments tracking #292

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ module.exports = {

// add your custom rules here
rules: {
"no-console": process.env.NODE_ENV === "production" ? "error" : 1,
"no-console": [
process.env.NODE_ENV === "production" ? "error" : 1,
{ allow: ["warn", "error", "info"] },
],
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "off",
"no-empty": [
"error",
Expand Down Expand Up @@ -107,7 +110,7 @@ module.exports = {
ignoreStrings: true,
ignoreTemplateLiterals: true,
ignoreRegExpLiterals: true,
ignoreHTMLAttributeValues: false,
ignoreHTMLAttributeValues: true,
ignoreHTMLTextContents: false,
},
],
Expand Down
2 changes: 1 addition & 1 deletion backend
Submodule backend updated 44 files
+234 −1 package-lock.json
+4 −1 package.json
+244 −0 shared-types/models/index.ts
+2 −0 src/app.ts
+25 −0 src/controllers/investments/holdings/create-holding.controller.ts
+2 −0 src/controllers/investments/holdings/index.ts
+21 −0 src/controllers/investments/holdings/load-list.controller.ts
+1 −0 src/js/utils/index.ts
+2 −1 src/js/utils/logger.ts
+123 −0 src/js/utils/requests-calling.utils.ts
+220 −0 src/js/utils/requests-calling.utils.unit.ts
+321 −0 src/migrations/1706472509091-investments-models.js
+82 −0 src/migrations/20240128212135-investments-indexes.js
+18 −0 src/migrations/20240408213725-holdings-unique-keys.js
+43 −0 src/migrations/20240412192753-investment-transaction-ref-fee-field.js
+8 −0 src/models/Accounts.model.ts
+1 −0 src/models/Users.model.ts
+10 −0 src/models/associations.ts
+6 −4 src/models/index.ts
+135 −0 src/models/investments/Holdings.model.ts
+134 −0 src/models/investments/InvestmentTransaction.model.ts
+162 −0 src/models/investments/Security.model.ts
+68 −0 src/models/investments/SecurityPricing.model.ts
+128 −0 src/routes/investments.route.ts
+1 −1 src/services/auth.service.ts
+1 −0 src/services/calculate-ref-amount.service.ts
+49 −0 src/services/investments/holdings/create-holding.service.e2e.ts
+72 −0 src/services/investments/holdings/create-holding.service.ts
+6 −0 src/services/investments/holdings/index.ts
+44 −0 src/services/investments/holdings/load-holdings.service.ts
+0 −0 src/services/investments/index.ts
+215 −0 src/services/investments/market-data.service.ts
+1 −0 src/services/investments/mocks/tickers-mock.json
+1 −0 src/services/investments/mocks/tickers-prices-mock.json
+301 −0 src/services/investments/securities.service.ts
+98 −0 src/services/investments/transactions/create.service.e2e.ts
+182 −0 src/services/investments/transactions/create.service.ts
+129 −0 src/services/investments/transactions/delete.service.ts
+45 −0 src/services/investments/transactions/get-list.service.ts
+2 −0 src/services/investments/transactions/index.ts
+1 −0 src/tests/helpers/index.ts
+45 −0 src/tests/helpers/investments/holdings.ts
+18 −3 src/tests/setupIntegrationTests.ts
+1 −0 tsconfig.json
3,020 changes: 1,468 additions & 1,552 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
"tailwindcss-animate": "^1.0.7",
"v-calendar": "^3.1.2",
"vue": "3.4.36",
"vue-router": "^4.4.3"
"vue-router": "^4.4.3",
"vue-virtual-scroller": "^2.0.0-beta.8"
},
"devDependencies": {
"@faker-js/faker": "^8.4.1",
Expand Down Expand Up @@ -80,7 +81,7 @@
"prettier": "3.3.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"sass": "^1.77.8",
"sass": "1.77.6",
"storybook": "^8.2.8",
"storybook-dark-mode": "^4.0.2",
"tailwindcss": "^3.4.8",
Expand Down
6 changes: 2 additions & 4 deletions src/api/_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,8 @@ interface ApiCall {
const API_HTTP = import.meta.env.VITE_APP_API_HTTP;
const API_VER = import.meta.env.VITE_APP_API_VER;

// eslint-disable-next-line no-console
console.log("API_HTTP", API_HTTP);
// eslint-disable-next-line no-console
console.log("API_VER", API_VER);
console.info("API_HTTP", API_HTTP);
console.info("API_VER", API_VER);
class ApiCaller {
authToken: string | null;

Expand Down
10 changes: 3 additions & 7 deletions src/api/accounts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ACCOUNT_CATEGORIES, AccountModel, endpointsTypes } from "shared-types";
import { AccountModel, endpointsTypes } from "shared-types";
import { api } from "@/api/_api";
import { fromSystemAmount, toSystemAmount } from "@/api/helpers";

Expand All @@ -19,18 +19,14 @@ export const loadAccounts = async (): Promise<AccountModel[]> => {
};

export const createAccount = async (
payload: Omit<endpointsTypes.CreateAccountBody, "accountCategory">,
payload: endpointsTypes.CreateAccountBody,
): Promise<AccountModel> => {
const params = payload;

if (params.creditLimit) params.creditLimit = toSystemAmount(Number(params.creditLimit));
if (params.initialBalance) params.initialBalance = toSystemAmount(Number(params.initialBalance));

const result = await api.post("/accounts", {
...params,
// For now we just doesn't allow users to select account category on UI
accountCategory: ACCOUNT_CATEGORIES.general,
});
const result = await api.post("/accounts", params);

return result;
};
Expand Down
1 change: 1 addition & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from "./auth";
export * from "./transactions";
export * from "./stats";
export * from "./currencies";
export * from "./investments";
23 changes: 23 additions & 0 deletions src/api/investments/holdings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { HoldingModel } from "shared-types";
import { api } from "@/api/_api";

export const loadHoldingsList = async () => {
const result: HoldingModel[] = await api.get("/investing/holdings");

return result || [];
};

export const createHolding = async ({
accountId,
securityId,
}: {
accountId: number;
securityId: number;
}) => {
const result = await api.post("/investing/holdings", {
accountId,
securityId,
});

return result;
};
3 changes: 3 additions & 0 deletions src/api/investments/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./securities";
export * from "./holdings";
export * from "./transactions";
21 changes: 21 additions & 0 deletions src/api/investments/securities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { SecurityModel } from "shared-types";
import { api } from "@/api/_api";

export const loadSecuritiesList = async ({
query,
}: { query?: string } = {}): Promise<SecurityModel[]> => {
const result: SecurityModel[] = await api.get(
query ? `/investing/securities?query=${query}` : "/investing/securities",
);

return result || [];
};

export const loadSecuritiesPrices = async () => {
const result = await api.get("/investing/securities/prices");

return result;
};

export const syncSecurities = async (): Promise<void> =>
api.get("/investing/securities/sync");
30 changes: 30 additions & 0 deletions src/api/investments/transactions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { TRANSACTION_TYPES, InvestmentTransactionModel } from "shared-types";
import { api } from "@/api/_api";

interface CreateInvestmentTransactionParams {
accountId: number;
securityId: number;
transactionType: TRANSACTION_TYPES;
date: string;
name?: string;
quantity: string;
price: string;
fees: string;
}

export const createInvestmentTransaction = async (
params: CreateInvestmentTransactionParams,
) => {
const result = await api.post("/investing/transaction", params);

return result;
};

export const getInvestmentTransactionsForAccount = async ({
accountId,
securityId,
}: {
accountId: number;
securityId?: number;
}): Promise<InvestmentTransactionModel[]> =>
api.get("/investing/transactions", { accountId, securityId });
2 changes: 1 addition & 1 deletion src/common/const/account-categories-verbose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ export const ACCOUNT_CATEGORIES_VERBOSE = Object.freeze({
[ACCOUNT_CATEGORIES.mortgage]: "Mortgage",
[ACCOUNT_CATEGORIES.overdraft]: "Overdraft",
[ACCOUNT_CATEGORIES.crypto]: "Crypto",
});
}) as Record<ACCOUNT_CATEGORIES, string>;
114 changes: 88 additions & 26 deletions src/components/common/dropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,53 @@
<slot name="header" />

<div class="ui-dropdown__values" role="listbox">
<template v-for="(item, index) in values" :key="item">
<button
type="button"
role="option"
class="ui-dropdown__item"
:aria-selected="isItemHighlighted(item)"
:class="{
'ui-dropdown__item--highlighed': isItemHighlighted(item),
}"
@click="emit('select', { item, index })"
<template v-if="virtualScroll">
<RecycleScroller
v-slot="{ item, index }: { item: T; index: number }"
:items="values"
:item-size="itemSize"
:style="{ height: virtualScrollHeight }"
:key-field="keyField"
>
{{ getLabelFromValue(item) }}
</button>
<template v-if="$slots.item">
<slot name="item" v-bind="{ item }" />
</template>
<template v-else>
<button
type="button"
role="option"
class="ui-dropdown__item"
:aria-selected="isItemHighlighted(item)"
:class="{
'ui-dropdown__item--highlighed': isItemHighlighted(item),
}"
@click="emit('select', { item, index })"
>
{{ getLabelFromValue(item) }}
</button>
</template>
</RecycleScroller>
</template>
<template v-else>
<template v-for="(item, index) in values" :key="item">
<template v-if="$slots.item">
<slot name="item" v-bind="{ item, index }" />
</template>
<template v-else>
<button
type="button"
role="option"
class="ui-dropdown__item"
:aria-selected="isItemHighlighted(item)"
:class="{
'ui-dropdown__item--highlighed': isItemHighlighted(item),
}"
@click="emit('select', { item, index })"
>
{{ getLabelFromValue(item) }}
</button>
</template>
</template>
</template>
</div>

Expand All @@ -24,24 +58,52 @@
</template>

<script lang="ts" setup generic="T extends Record<string, any>">
import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
import { RecycleScroller } from "vue-virtual-scroller";
import { computed } from "vue";

const emit = defineEmits<{
select: [item: { item: T; index: number }];
}>();

const props = withDefaults(
defineProps<{
isVisible?: boolean;
values: T[];
selectedValue: T | null;
labelKey?: keyof T | ((value: T) => string) | "label";
position?: "top" | "bottom";
}>(),
{
isVisible: false,
position: "bottom",
labelKey: "label",
},
);
type BaseProps = {
isVisible?: boolean;
values: T[];
selectedValue?: T | null;
labelKey?: keyof T | ((value: T) => string) | "label";
position?: "top" | "bottom";
};
type VirtualScrollProps = BaseProps & {
virtualScroll: true;
itemSize: number;
maxHeight: number | string;
keyField?: string;
};

type NonVirtualScrollProps = BaseProps & {
virtualScroll?: false;
itemSize?: never;
maxHeight?: never;
keyField?: never;
};

type ComponentProps = VirtualScrollProps | NonVirtualScrollProps;

const props = withDefaults(defineProps<ComponentProps>(), {
isVisible: false,
selectedValue: null,
labelKey: "label",
position: "bottom",

maxHeight: 250,
itemSize: 50,
keyField: "id",
});

const virtualScrollHeight = computed(() => {
const { maxHeight } = props;
return typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight;
});

const getLabelFromValue = (value: T) => {
const { labelKey } = props;
Expand Down
1 change: 0 additions & 1 deletion src/components/common/progress.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ export default defineComponent({
methods: {
validateProgressValue(value: string | number) {
if (value > this.maxProgress) {
// eslint-disable-next-line no-console
console.error(
`["Progress" component]: current progress value is ${value}, but maximum allowed is ${this.maxProgress}. Override default "maxProgress" value using props to have right visualization!`,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,12 @@
</template>
<template v-else>
<form-row>
<input-field model-value="No account exists" label="Account" readonly :disabled="disabled">
<input-field
model-value="No account exists"
label="Account"
readonly
:disabled="disabled"
>
<template #label-right>
<div
class="text-primary cursor-pointer hover:underline"
Expand Down Expand Up @@ -102,7 +107,11 @@ withDefaults(
},
);

const emit = defineEmits(["close-modal", "update:account", "update:to-account"]);
const emit = defineEmits([
"close-modal",
"update:account",
"update:to-account",
]);

const router = useRouter();

Expand Down
Loading
Loading