Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ npm run dev

This will start the simulator on http://localhost:3000

Accessing the room of the domain will show the dashboard view of the server. The dashboard allows you to set the environment variables for the server and view the logs of the server.
Accessing the root of the domain will show the dashboard view of the server. The dashboard allows you to set the environment variables for the server and view the logs of the server.

There are helpful links to the Investec docs, Community wiki, GitHub repo and the Postman collection.

Expand All @@ -54,6 +54,8 @@ There are helpful links to the Investec docs, Community wiki, GitHub repo and th
### Dashboard
- **GET /**
- Dashboard view of the server
- **GET /health**
- Health check endpoint returning service status
### Auth
- **POST /identity/v2/oauth2/token**
- Get an access token (only required if auth is turned on)
Expand Down Expand Up @@ -105,6 +107,10 @@ There are helpful links to the Investec docs, Community wiki, GitHub repo and th
- Create a new account
- **DELETE /za/pb/v1/accounts/:accountId**
- Deletes the account and its transactions
- **POST /za/pb/v1/accounts/beneficiaries**
- Create a new beneficiary
- **DELETE /za/pb/v1/accounts/beneficiaries/:beneficiaryId**
- Delete a beneficiary
- **POST /za/v1/cards/:cardKey/code/execute-live**
- Used for the POS to execute the code on the card

Expand Down
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 modified images/dashboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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
85 changes: 71 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,33 @@ app.get('/envs', async (req: Request, res: Response) => {
}
})

app.get('/health', (req: Request, res: Response) => {
res.status(200).json({ status: 'ok' })
})

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 +255,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 +312,7 @@ export function formatErrorResponse(req: Request, res: Response, code: number) {
' ' +
req.method +
' ' +
req.url +
req.originalUrl +
' HTTP/' +
req.httpVersion +
' ' +
Expand Down
Loading