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