Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
1 change: 0 additions & 1 deletion .env.sample
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
PORT=3000
DB_FILE='investec.db'
AUTH=false
CLIENT_ID=
CLIENT_SECRET=
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@
**/dist
.env
**.db-shm
**.db-wal
**.db-wal
**.db-journal
CLAUDE.md
.idea
2 changes: 1 addition & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default [
'@stylistic/js': stylisticJs,
},
rules: {
'@stylistic/js/max-len': 120,
'@stylistic/js/max-len': ['error', { code: 120 }],
},
},
{ files: ['**/*.{js,mjs,cjs,ts}'] },
Expand Down
Binary file removed investec.db
Binary file not shown.
15 changes: 15 additions & 0 deletions prisma/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,49 @@ const accountData: Prisma.AccountCreateInput[] = [
accountName: 'Mr J Soap',
referenceName: 'Mr J Soap',
productName: 'Private Bank Account',
kycCompliant: true,
profileId: '10001234567890',
profileName: 'Joe Soap',
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider normalization and data consistency improvements.

All accounts are using the same profile data, which may be appropriate for testing but raises some concerns:

  1. Data duplication: Storing both profileId and profileName in the Account model duplicates data from the Profile table, violating normalization principles.
  2. Data consistency risk: Without foreign key constraints, there's no guarantee the profileId exists in the Profile table.

Consider removing profileName from the Account model and using joins to retrieve profile information when needed, or adding proper foreign key relationships.

-    kycCompliant: true,
-    profileId: '10001234567890',
-    profileName: 'Joe Soap',
+    kycCompliant: true,
+    profileId: '10001234567890',

Also applies to: 22-24, 32-34, 42-44, 52-54

🤖 Prompt for AI Agents
In prisma/account.ts around lines 12-14, 22-24, 32-34, and 42-44, the Account
model includes both profileId and profileName, causing data duplication and
risking inconsistency. To fix this, remove the profileName field from the
Account model and ensure profileId is a foreign key referencing the Profile
table. Adjust queries to join with the Profile table to retrieve profileName
when needed, maintaining normalization and data integrity.

},
{
accountId: '4675778129910189600000004',
accountNumber: '10012420004',
accountName: 'Mr J Soap',
referenceName: 'Mr J Soap',
productName: 'PrimeSaver',
kycCompliant: true,
profileId: '10001234567890',
profileName: 'Joe Soap',
},
{
accountId: '4675778129910189600000005',
accountNumber: '10012420005',
accountName: 'Mr J Soap',
referenceName: 'Mr J Soap',
productName: 'Cash Management Account',
kycCompliant: true,
profileId: '10001234567890',
profileName: 'Joe Soap',
},
{
accountId: '4675778129910189600000006',
accountNumber: '10012420006',
accountName: 'Mr J Soap',
referenceName: 'Mr J Soap',
productName: 'Mortgage Loan Account',
kycCompliant: true,
profileId: '10001234567890',
profileName: 'Joe Soap',
},
{
accountId: '4675778129910189600000007',
accountNumber: '10012420007',
accountName: 'Mr J Soap',
referenceName: 'Mr J Soap',
productName: 'Instalment Sale Loan Account',
kycCompliant: true,
profileId: '10001234567890',
profileName: 'Joe Soap',
},
]

Expand Down
Binary file modified prisma/dev.db
Binary file not shown.
28 changes: 28 additions & 0 deletions prisma/migrations/20250708211014_add_profile_and_kyc/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-- CreateTable
CREATE TABLE "Profile" (
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like the migrations could be compressed, given that the DB is shipped with the simulator.

"profileId" TEXT NOT NULL PRIMARY KEY,
"profileName" TEXT NOT NULL
);

-- Insert default profile
INSERT INTO "Profile" ("profileId", "profileName") VALUES ('10001234567890', 'Joe Soap');

-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Account" (
"accountId" TEXT NOT NULL PRIMARY KEY,
"accountNumber" TEXT NOT NULL,
"accountName" TEXT NOT NULL,
"referenceName" TEXT NOT NULL,
"productName" TEXT NOT NULL,
"kycCompliant" BOOLEAN NOT NULL DEFAULT true,
"profileId" TEXT NOT NULL,
"profileName" TEXT NOT NULL
);
INSERT INTO "new_Account" ("accountId", "accountName", "accountNumber", "productName", "referenceName", "kycCompliant", "profileId", "profileName")
SELECT "accountId", "accountName", "accountNumber", "productName", "referenceName", true, '10001234567890', 'Joe Soap' FROM "Account";
DROP TABLE "Account";
ALTER TABLE "new_Account" RENAME TO "Account";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;
19 changes: 19 additions & 0 deletions prisma/profile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { PrismaClient, Prisma } from '@prisma/client'

const prisma = new PrismaClient()

const profileData: Prisma.ProfileCreateInput[] = [
{
profileId: '10001234567890',
profileName: 'Joe Soap',
},
]

export async function seedProfiles() {
for (const p of profileData) {
const profile = await prisma.profile.create({
data: p,
})
console.log(`Created profile with id: ${profile.profileId}`)
}
}
8 changes: 8 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,20 @@ datasource db {
url = "file:./dev.db"
}

model Profile {
profileId String @id
profileName String
}

model Account {
accountId String @id
accountNumber String
accountName String
referenceName String
productName String
kycCompliant Boolean @default(true)
profileId String
profileName String
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add foreign key relationship and consider normalization.

The Account model extensions introduce profile functionality but lack proper relational constraints:

  1. Missing foreign key: No relationship defined between Account.profileId and Profile.profileId
  2. Data duplication: profileName is stored in both Profile and Account tables

Consider this improved schema design:

    kycCompliant Boolean @default(true)
    profileId String
-    profileName String
+    profile Profile @relation(fields: [profileId], references: [profileId])

This approach:

  • Establishes proper foreign key constraints
  • Eliminates data duplication
  • Ensures referential integrity
  • Requires joining to get profile name when needed
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
kycCompliant Boolean @default(true)
profileId String
profileName String
kycCompliant Boolean @default(true)
profileId String
profile Profile @relation(fields: [profileId], references: [profileId])
🤖 Prompt for AI Agents
In prisma/schema.prisma around lines 21 to 23, the Account model has profileId
and profileName fields but lacks a foreign key relationship to the Profile
model, causing data duplication and missing referential integrity. To fix this,
remove the profileName field from Account, define a relation field linking
Account.profileId to Profile.id (or the appropriate primary key), and add the
@relation attribute to enforce the foreign key constraint. This will normalize
the schema, eliminate duplicated profileName data, and ensure proper relational
integrity.

}

model Transaction {
Expand Down
2 changes: 2 additions & 0 deletions prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { PrismaClient } from '@prisma/client'
import { seedCurrencies } from './currency'
import { seedCountries } from './country'
import { seedMerchants } from './merchant'
import { seedProfiles } from './profile'
import { seedAccounts } from './account'
import { seedTransactions } from './transaction'
import { seedSettings } from './settings'
Expand All @@ -16,6 +17,7 @@ async function main() {
await seedCurrencies()
await seedCountries()
await seedMerchants()
await seedProfiles()
await seedAccounts()
await seedTransactions()
await seedCards()
Expand Down
81 changes: 67 additions & 14 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ import { seedTransactions } from '../prisma/transaction.js'
import { seedBeneficiaries } from '../prisma/beneficiary.js'
import { seedCards } from '../prisma/card.js'
import { seedCardCodes } from '../prisma/card-code.js'
import { seedProfiles } from '../prisma/profile.js'

dotenv.config()

export const port = process.env.PORT || 3000
// const dbFile = process.env.DB_FILE || 'investec.db'
// const overdraft = process.env.OVERDRAFT || 5000
const prisma = new PrismaClient()
export const app = express()
Expand Down Expand Up @@ -67,18 +67,23 @@ io.on('connection', socket => {
await prisma.beneficiary.deleteMany()
await prisma.card.deleteMany()
await prisma.cardCode.deleteMany()
await prisma.profile.deleteMany()
await emitDatabaseSummary()
break
case 'restore':
await prisma.account.deleteMany()
await prisma.transaction.deleteMany()
await prisma.beneficiary.deleteMany()
await prisma.card.deleteMany()
await prisma.cardCode.deleteMany()
await prisma.profile.deleteMany()
seedProfiles()
seedAccounts()
seedTransactions()
seedBeneficiaries()
seedCards()
seedCardCodes()
await emitDatabaseSummary()
break
}
//io.emit('control', settings);
Expand Down Expand Up @@ -202,6 +207,29 @@ app.get('/envs', async (req: Request, res: Response) => {
}
})

app.get('/database-summary', async (req: Request, res: Response) => {
try {
const [profileCount, accountCount, cardCount, transactionCount] = await Promise.all([
prisma.profile.count(),
prisma.account.count(),
prisma.card.count(),
prisma.transaction.count()
])

const summary = {
profiles: profileCount,
accounts: accountCount,
cards: cardCount,
transactions: transactionCount
}

return formatResponse(summary, req, res)
} catch (error) {
console.log(error)
return formatErrorResponse(req, res, 500)
}
})

function isValidToken(req: Request) {
if (settings.auth !== true) {
return true
Expand All @@ -223,20 +251,45 @@ function isValidToken(req: Request) {
return false
}

export async function emitDatabaseSummary() {
try {
const [profileCount, accountCount, cardCount, transactionCount] = await Promise.all([
prisma.profile.count(),
prisma.account.count(),
prisma.card.count(),
prisma.transaction.count()
])

const summary = {
profiles: profileCount,
accounts: accountCount,
cards: cardCount,
transactions: transactionCount
}

io.sockets.emit('database-summary', summary)
} catch (error) {
console.log('Error emitting database summary:', error)
}
}

export function formatResponse (data: unknown, req: Request, res: Response) {
const date = new Date()
io.sockets.emit(
messageQueue,
date.toUTCString() +
' ' +
req.method +
' ' +
req.url +
' HTTP/' +
req.httpVersion +
' ' +
res.statusCode,
)
// Don't log database-summary calls as they're internal dashboard calls
if (req.originalUrl !== '/database-summary') {
io.sockets.emit(
messageQueue,
date.toUTCString() +
' ' +
req.method +
' ' +
req.originalUrl +
' HTTP/' +
req.httpVersion +
' ' +
res.statusCode,
)
}
return res.json({
data,
links: {
Expand All @@ -255,7 +308,7 @@ export function formatErrorResponse(req: Request, res: Response, code: number) {
' ' +
req.method +
' ' +
req.url +
req.originalUrl +
' HTTP/' +
req.httpVersion +
' ' +
Expand Down
40 changes: 38 additions & 2 deletions src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,23 @@
<button type="button" @click="restoreMethod" class="block rounded-md bg-gray-900 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-gray-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-600">Reset DB</button>
</div>
</div>
<!-- Database Summary -->
<div class="mt-4 divide-y divide-gray-200 overflow-hidden rounded-lg bg-white shadow">
<div class="px-4 py-5 sm:p-6">
<h2 class="text-lg font-semibold leading-6 text-gray-900">Database Summary</h2>
</div>
<div class="px-4 py-4 sm:px-6">
<div class="text-sm text-gray-600">
<span class="font-medium">Profiles:</span> <span class="font-semibold text-gray-900">{{ summary.profiles || 0 }}</span>
<span style="margin: 0 2rem;"></span>
<span class="font-medium">Accounts:</span> <span class="font-semibold text-gray-900">{{ summary.accounts || 0 }}</span>
<span style="margin: 0 2rem;"></span>
<span class="font-medium">Cards:</span> <span class="font-semibold text-gray-900">{{ summary.cards || 0 }}</span>
<span style="margin: 0 2rem;"></span>
<span class="font-medium">Transactions:</span> <span class="font-semibold text-gray-900">{{ summary.transactions || 0 }}</span>
</div>
</div>
</div>
<!-- Server Logs -->
<div class="mt-4 divide-y divide-gray-200 overflow-hidden rounded-lg bg-white shadow">
<div class="px-4 py-5 sm:p-6">
Expand Down Expand Up @@ -146,6 +163,12 @@ <h2 class="text-base font-semibold leading-7 text-gray-900">Environmental Variab
token_expiry: 1799,
auth: false,
alertMessage: null,
summary: {
profiles: 0,
accounts: 0,
cards: 0,
transactions: 0
}
}
},
mounted() {
Expand All @@ -168,7 +191,11 @@ <h2 class="text-base font-semibold leading-7 text-gray-900">Environmental Variab
this.auth = false;
}
});
this.socket.on('database-summary', (msg) => {
this.summary = msg;
});
this.fetchEnv();
this.fetchSummary();
},
methods: {
fetchEnv() {
Expand All @@ -186,6 +213,13 @@ <h2 class="text-base font-semibold leading-7 text-gray-900">Environmental Variab
}
});
},
fetchSummary() {
fetch('/database-summary')
.then(response => response.json())
.then(data => {
this.summary = data.data;
});
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add basic error handling to fetchSummary

A failing /database-summary GET (network error or 5xx) currently throws an unhandled promise rejection. Wrap in try/catch or append .catch() to surface an error toast.

 fetch('/database-summary')
   .then(r => r.json())
   .then(d => { this.summary = d.data })
+  .catch(() => this.toast('Unable to load DB summary'));

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/index.html around lines 262 to 268, the fetchSummary method lacks error
handling for the fetch call to '/database-summary', which can cause unhandled
promise rejections on network or server errors. Add a .catch() block after the
existing .then() chain to handle errors by showing an error toast or logging the
error, ensuring any fetch failures are gracefully handled and surfaced to the
user.

mySubmitMethod() {
if (this.client_id && this.client_secret && this.api_key) {
this.socket.emit('envs', {
Expand All @@ -203,14 +237,16 @@ <h2 class="text-base font-semibold leading-7 text-gray-900">Environmental Variab
action: 'clear',
message: '',
});
this.toast('Database cleared')
this.toast('Database cleared');
setTimeout(() => this.fetchSummary(), 500);
},
restoreMethod() {
this.socket.emit('control', {
action: 'restore',
message: '',
});
this.toast('Database Reseeded')
this.toast('Database Reseeded');
setTimeout(() => this.fetchSummary(), 500);
},
toast(msg) {
this.alertMessage = msg;
Expand Down
Loading