diff --git a/apps/client/.babelrc.json b/apps/client/.babelrc.json new file mode 100644 index 00000000..86efa43b --- /dev/null +++ b/apps/client/.babelrc.json @@ -0,0 +1,14 @@ +{ + "presets": [ + "@babel/preset-typescript", + "@babel/preset-env", + [ + "@nrwl/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/apps/client/.storybook/main.js b/apps/client/.storybook/main.js new file mode 100644 index 00000000..2484425c --- /dev/null +++ b/apps/client/.storybook/main.js @@ -0,0 +1,18 @@ +const rootMain = require('../../../.storybook/main') + +module.exports = { + ...rootMain, + core: { ...rootMain.core, builder: 'webpack5' }, + stories: ['../**/*.stories.@(js|jsx|ts|tsx)'], + addons: [...rootMain.addons, '@nrwl/react/plugins/storybook'], + webpackFinal: async (config, { configType }) => { + // apply any global webpack configs that might have been specified in .storybook/main.js + if (rootMain.webpackFinal) { + config = await rootMain.webpackFinal(config, { configType }) + } + + // add your own webpack tweaks if needed + + return config + }, +} diff --git a/apps/client/.storybook/tsconfig.json b/apps/client/.storybook/tsconfig.json new file mode 100644 index 00000000..61b74d70 --- /dev/null +++ b/apps/client/.storybook/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": {}, + "include": ["**/*.ts", "**/*.tsx", "**/**/*.ts", "**/**/*.tsx"] +} diff --git a/apps/client/components/Maintenance.stories.tsx b/apps/client/components/Maintenance.stories.tsx new file mode 100644 index 00000000..468d8d2a --- /dev/null +++ b/apps/client/components/Maintenance.stories.tsx @@ -0,0 +1,18 @@ +import type { Story, Meta } from '@storybook/react' +import Maintenance from './Maintenance.tsx' +import React from 'react' + +export default { + title: 'components/Maintenance.tsx', + component: Maintenance, +} as Meta + +const Template: Story = () => { + return ( + <> + + + ) +} + +export const Base = Template.bind({}) diff --git a/apps/client/tsconfig.json b/apps/client/tsconfig.json index d69a004a..b836c388 100644 --- a/apps/client/tsconfig.json +++ b/apps/client/tsconfig.json @@ -27,5 +27,17 @@ "next-env.d.ts", ".next/types/**/*.ts" ], - "exclude": ["node_modules", "jest.config.ts"] + "exclude": [ + "node_modules", + "jest.config.ts", + "**/*.stories.ts", + "**/*.stories.js", + "**/*.stories.jsx", + "**/*.stories.tsx" + ], + "references": [ + { + "path": "./.storybook/tsconfig.json" + } + ] } diff --git a/apps/server/src/app/__tests__/insights.integration.spec.ts b/apps/server/src/app/__tests__/insights.integration.spec.ts index fe32ffef..029aa73f 100644 --- a/apps/server/src/app/__tests__/insights.integration.spec.ts +++ b/apps/server/src/app/__tests__/insights.integration.spec.ts @@ -1,5 +1,5 @@ import type { User } from '@prisma/client' -import { InvestmentTransactionCategory, Prisma, PrismaClient } from '@prisma/client' +import { AssetClass, InvestmentTransactionCategory, Prisma, PrismaClient } from '@prisma/client' import { createLogger, transports } from 'winston' import { DateTime } from 'luxon' import type { @@ -202,19 +202,25 @@ describe('insight service', () => { holdings: { create: [ { - security: { create: { symbol: 'AAPL', plaidType: 'equity' } }, + security: { + create: { symbol: 'AAPL', assetClass: AssetClass.stocks }, + }, quantity: 1, costBasisUser: 100, value: 200, }, { - security: { create: { symbol: 'NFLX', plaidType: 'equity' } }, + security: { + create: { symbol: 'NFLX', assetClass: AssetClass.stocks }, + }, quantity: 10, costBasisUser: 200, value: 300, }, { - security: { create: { symbol: 'SHOP', plaidType: 'equity' } }, + security: { + create: { symbol: 'SHOP', assetClass: AssetClass.stocks }, + }, quantity: 2, costBasisUser: 100, value: 50, diff --git a/libs/server/features/src/account/insight.service.ts b/libs/server/features/src/account/insight.service.ts index bfe44d9d..116e588e 100644 --- a/libs/server/features/src/account/insight.service.ts +++ b/libs/server/features/src/account/insight.service.ts @@ -641,14 +641,7 @@ export class InsightService implements IInsightService { INNER JOIN ( SELECT id, - CASE - -- plaid - WHEN plaid_type IN ('equity', 'etf', 'mutual fund', 'derivative') THEN 'stocks' - WHEN plaid_type IN ('fixed income') THEN 'fixed_income' - WHEN plaid_type IN ('cash', 'loan') THEN 'cash' - WHEN plaid_type IN ('cryptocurrency') THEN 'crypto' - ELSE 'other' - END AS "asset_class" + asset_class FROM "security" ) s ON s.id = h.security_id @@ -694,14 +687,7 @@ export class InsightService implements IInsightService { INNER JOIN security s ON s.id = h.security_id LEFT JOIN LATERAL ( SELECT - CASE - -- plaid - WHEN s.plaid_type IN ('equity', 'etf', 'mutual fund', 'derivative') THEN 'stocks' - WHEN s.plaid_type IN ('fixed income') THEN 'fixed_income' - WHEN s.plaid_type IN ('cash', 'loan') THEN 'cash' - WHEN s.plaid_type IN ('cryptocurrency') THEN 'crypto' - ELSE 'other' - END AS "category" + asset_class AS "category" ) x ON TRUE WHERE h.account_id IN ${accountIds} @@ -828,28 +814,21 @@ export class InsightService implements IInsightService { UNION ALL -- investment accounts SELECT - s.asset_type, + s.asset_class AS "asset_type", SUM(h.value) AS "amount" FROM holdings_enriched h INNER JOIN ( SELECT id, - CASE - -- plaid - WHEN plaid_type IN ('equity', 'etf', 'mutual fund', 'derivative') THEN 'stocks' - WHEN plaid_type IN ('fixed income') THEN 'bonds' - WHEN plaid_type IN ('cash', 'loan') THEN 'cash' - WHEN plaid_type IN ('cryptocurrency') THEN 'crypto' - ELSE 'other' - END AS "asset_type" + asset_class FROM "security" ) s ON s.id = h.security_id WHERE h.account_id IN ${pAccountIds} GROUP BY - s.asset_type + s.asset_class ) x GROUP BY 1 diff --git a/libs/server/features/src/plan/plan.service.ts b/libs/server/features/src/plan/plan.service.ts index b8fa4aca..9e0ba7a7 100644 --- a/libs/server/features/src/plan/plan.service.ts +++ b/libs/server/features/src/plan/plan.service.ts @@ -33,7 +33,7 @@ const PROJECTION_ASSET_PARAMS: { [type in SharedType.ProjectionAssetType]: [mean: Decimal.Value, stddev: Decimal.Value] } = { stocks: ['0.05', '0.186'], - bonds: ['0.02', '0.052'], + fixed_income: ['0.02', '0.052'], cash: ['-0.02', '0.05'], crypto: ['1.0', '1.0'], property: ['0.1', '0.2'], diff --git a/libs/server/features/src/providers/teller/teller.etl.ts b/libs/server/features/src/providers/teller/teller.etl.ts index 406f70c3..6d295d26 100644 --- a/libs/server/features/src/providers/teller/teller.etl.ts +++ b/libs/server/features/src/providers/teller/teller.etl.ts @@ -100,10 +100,7 @@ export class TellerETL implements IETL { const accounts = await this._extractAccounts(accessToken) - const transactions = await this._extractTransactions( - accessToken, - accounts.map((a) => a.id) - ) + const transactions = await this._extractTransactions(accessToken, accounts) this.logger.info( `Extracted Teller data for customer ${user.tellerUserId} accounts=${accounts.length} transactions=${transactions.length}`, @@ -196,26 +193,26 @@ export class TellerETL implements IETL { ] } - private async _extractTransactions(accessToken: string, accountIds: string[]) { + private async _extractTransactions( + accessToken: string, + tellerAccounts: TellerTypes.GetAccountsResponse + ) { const accountTransactions = await Promise.all( - accountIds.map(async (accountId) => { - const account = await this.prisma.account.findFirst({ - where: { - tellerAccountId: accountId, - }, - }) + tellerAccounts.map(async (tellerAccount) => { + const type = TellerUtil.getType(tellerAccount.type) + const classification = AccountUtil.getClassification(type) const transactions = await SharedUtil.withRetry( () => this.teller.getTransactions({ - accountId, + accountId: tellerAccount.id, accessToken, }), { maxRetries: 3, } ) - if (account!.classification === AccountClassification.asset) { + if (classification === AccountClassification.asset) { transactions.forEach((t) => { t.amount = String(Number(t.amount) * -1) }) @@ -277,7 +274,7 @@ export class TellerETL implements IETL { pending = EXCLUDED.pending, merchant_name = EXCLUDED.merchant_name, teller_type = EXCLUDED.teller_type, - teller_category = EXCLUDED.teller_category; + teller_category = EXCLUDED.teller_category, category = EXCLUDED.category; ` }) diff --git a/libs/shared/src/types/plan-types.ts b/libs/shared/src/types/plan-types.ts index 9b0f8d9c..97b46e5a 100644 --- a/libs/shared/src/types/plan-types.ts +++ b/libs/shared/src/types/plan-types.ts @@ -56,7 +56,13 @@ export type PlanProjectionResponse = { }[] } -export type ProjectionAssetType = 'stocks' | 'bonds' | 'cash' | 'crypto' | 'property' | 'other' +export type ProjectionAssetType = + | 'stocks' + | 'fixed_income' + | 'cash' + | 'crypto' + | 'property' + | 'other' export type ProjectionLiabilityType = 'credit' | 'loan' | 'other' export type PlanInsights = { diff --git a/prisma/migrations/20240121003016_add_asset_class_to_security/migration.sql b/prisma/migrations/20240121003016_add_asset_class_to_security/migration.sql new file mode 100644 index 00000000..23534f6f --- /dev/null +++ b/prisma/migrations/20240121003016_add_asset_class_to_security/migration.sql @@ -0,0 +1,6 @@ +-- CreateEnum +CREATE TYPE "AssetClass" AS ENUM ('cash', 'crypto', 'fixed_income', 'stocks', 'other'); + +-- AlterTable +ALTER TABLE "security" + ADD COLUMN "asset_class" "AssetClass" NOT NULL DEFAULT 'other'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f7bac315..58e8cc40 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -243,18 +243,27 @@ model InvestmentTransaction { @@map("investment_transaction") } +enum AssetClass { + cash + crypto + fixed_income + stocks + other +} + model Security { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) - updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6) + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) + updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6) name String? symbol String? cusip String? isin String? - sharesPerContract Decimal? @map("shares_per_contract") @db.Decimal(36, 18) - currencyCode String @default("USD") @map("currency_code") - pricingLastSyncedAt DateTime? @map("pricing_last_synced_at") @db.Timestamptz(6) - isBrokerageCash Boolean @default(false) @map("is_brokerage_cash") + sharesPerContract Decimal? @map("shares_per_contract") @db.Decimal(36, 18) + currencyCode String @default("USD") @map("currency_code") + pricingLastSyncedAt DateTime? @map("pricing_last_synced_at") @db.Timestamptz(6) + isBrokerageCash Boolean @default(false) @map("is_brokerage_cash") + assetClass AssetClass @default(other) @map("asset_class") // plaid data plaidSecurityId String? @unique @map("plaid_security_id") diff --git a/workspace.json b/workspace.json index ef681dc4..64b2eb15 100644 --- a/workspace.json +++ b/workspace.json @@ -66,6 +66,33 @@ "options": { "command": "node tools/scripts/triggerClientDeploy.js" } + }, + "storybook": { + "executor": "@nrwl/storybook:storybook", + "options": { + "uiFramework": "@storybook/react", + "port": 4400, + "configDir": "apps/client/.storybook" + }, + "configurations": { + "ci": { + "quiet": true + } + } + }, + "build-storybook": { + "executor": "@nrwl/storybook:build", + "outputs": ["{options.outputDir}"], + "options": { + "uiFramework": "@storybook/react", + "outputDir": "dist/storybook/client", + "configDir": "apps/client/.storybook" + }, + "configurations": { + "ci": { + "quiet": true + } + } } }, "tags": ["scope:app"] diff --git a/yarn.lock b/yarn.lock index 06e3d78f..be6dd4ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20432,4 +20432,4 @@ zod@^3.19.1: zwitch@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920" - integrity sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw== + integrity sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw== \ No newline at end of file