Skip to content

Commit 73b278f

Browse files
authored
Banking Simulator Updates and Enhancements (#30)
* Remove unused database switching functionality (#1) - Removed DB_FILE environment variable from .env.sample - Deleted legacy investec.db file - Cleaned up commented DB_FILE reference in app.ts * Add profile and KYC compliant fields to accounts (#2) * Add profile and KYC compliant fields to accounts - Added Profile model with profileId and profileName - Updated Account model with kycCompliant, profileId, and profileName fields - Created profile seeding script with default profile (ID: 10001234567890, Name: Joe Soap) - Updated account seeding to include profile references and KYC compliance - Added database journal files to gitignore - Applied Prisma migration to update existing database This ensures production-like account responses for strict validation compatibility. * Add updated database, edit migration * Add profiles and dashboard summary (#4) * Add profile and KYC compliant fields to accounts - Added Profile model with profileId and profileName - Updated Account model with kycCompliant, profileId, and profileName fields - Created profile seeding script with default profile (ID: 10001234567890, Name: Joe Soap) - Updated account seeding to include profile references and KYC compliance - Added database journal files to gitignore - Applied Prisma migration to update existing database This ensures production-like account responses for strict validation compatibility. * Add updated database, edit migration * Add real-time database summary dashboard (#3) * Add real-time database summary dashboard with Socket.IO - Added Database Summary section showing live counts for profiles, accounts, cards, and transactions - Implemented Socket.IO real-time updates for all database operations (create/delete accounts, transactions, clear/restore) - Added /database-summary API endpoint with real-time emission via emitDatabaseSummary() - Improved server logging to use req.originalUrl for complete path details - Excluded internal database-summary calls from server logs to reduce noise - Added .idea folder to .gitignore * Update dev.db with test accounts and transactions * Improve test coverage and robustness (#5) * Improve test coverage and robustness - Update account.spec.ts to include new profile fields (kycCompliant, profileId, profileName) - Improve card.spec.ts with better timestamp validation instead of exact matching - Enhance environmentalvariables.spec.ts with comprehensive structure validation - Add new profile.spec.ts with comprehensive profile functionality testing - Remove unnecessary comments from test files for cleaner code - Fix TypeScript warnings with proper parameter types * Remove remaining comments from test files - Clean up card.spec.ts by removing unnecessary comments - Clean up environmentalvariables.spec.ts by removing unnecessary comments - Keep code clean and concise without explanatory comments * Fix ESLint configuration and resolve linting issues - Fix eslint.config.js max-len rule configuration with proper severity and options - Fix Array() constructor usage in cards.ts to use array literal notation - Fix line length issues in card.spec.ts by breaking long strings into concatenated lines - Replace 'any' type with proper typing in profile.spec.ts - All 26 tests still passing - No ESLint errors remaining * Add hide/show functionality for sensitive environment variables (#6) - Client Secret and API Key fields now default to hidden state for security - Toggle buttons positioned to the right of input fields with eye icons - Client Secret uses password/text input type switching - API Key uses conditional rendering between textarea and masked dots display - Updated dashboard screenshot to reflect new UI changes * Add basic health endpoint for service monitoring (#7) - Implements GET /health endpoint returning HTTP 200 with { status: 'ok' } - Provides simple service availability check for monitoring tools - Lightweight response with no database dependencies * Fix README typo and add health endpoint documentation (#8) - Fixed typo: "room of the domain" to "root of the domain" - Added /health endpoint to Dashboard section for completeness - Preserved original formatting and structure * Add scrollable logs container with auto-scroll functionality (#9) - Added fixed height (300px) scrollable container for server logs - Implemented auto-scroll to bottom when new log entries arrive - Added custom scrollbar styling for better visibility - Removed overflow-hidden from parent container to show scrollbar - Enhanced log item padding and styling for better readability * Add beneficiary deletion endpoint and fix TypeScript issues (#10) * Update prisma database with latest schema changes (#11)
1 parent 262c510 commit 73b278f

12 files changed

Lines changed: 255 additions & 40 deletions

File tree

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ npm run dev
4545

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

48-
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.
48+
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.
4949

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

@@ -54,6 +54,8 @@ There are helpful links to the Investec docs, Community wiki, GitHub repo and th
5454
### Dashboard
5555
- **GET /**
5656
- Dashboard view of the server
57+
- **GET /health**
58+
- Health check endpoint returning service status
5759
### Auth
5860
- **POST /identity/v2/oauth2/token**
5961
- Get an access token (only required if auth is turned on)
@@ -105,6 +107,10 @@ There are helpful links to the Investec docs, Community wiki, GitHub repo and th
105107
- Create a new account
106108
- **DELETE /za/pb/v1/accounts/:accountId**
107109
- Deletes the account and its transactions
110+
- **POST /za/pb/v1/accounts/beneficiaries**
111+
- Create a new beneficiary
112+
- **DELETE /za/pb/v1/accounts/beneficiaries/:beneficiaryId**
113+
- Delete a beneficiary
108114
- **POST /za/v1/cards/:cardKey/code/execute-live**
109115
- Used for the POS to execute the code on the card
110116

eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export default [
99
'@stylistic/js': stylisticJs,
1010
},
1111
rules: {
12-
'@stylistic/js/max-len': 120,
12+
'@stylistic/js/max-len': ['error', { code: 120 }],
1313
},
1414
},
1515
{ files: ['**/*.{js,mjs,cjs,ts}'] },

images/dashboard.png

186 KB
Loading

prisma/dev.db

0 Bytes
Binary file not shown.

src/app.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,9 @@ app.get('/envs', async (req: Request, res: Response) => {
207207
}
208208
})
209209

210+
app.get('/health', (req: Request, res: Response) => {
211+
res.status(200).json({ status: 'ok' })
212+
})
210213
app.get('/database-summary', async (req: Request, res: Response) => {
211214
try {
212215
const [profileCount, accountCount, cardCount, transactionCount] = await Promise.all([

src/index.html

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,22 @@
44
<meta name="viewport" content="width=device-width,initial-scale=1.0">
55
<title>Programmable Sandbox Sim</title>
66
<link href="/output.css" rel="stylesheet">
7+
<style>
8+
.logs-container::-webkit-scrollbar {
9+
width: 8px;
10+
}
11+
.logs-container::-webkit-scrollbar-track {
12+
background: #f1f5f9;
13+
border-radius: 4px;
14+
}
15+
.logs-container::-webkit-scrollbar-thumb {
16+
background: #9ca3af;
17+
border-radius: 4px;
18+
}
19+
.logs-container::-webkit-scrollbar-thumb:hover {
20+
background: #6b7280;
21+
}
22+
</style>
723
<script async defer src="https://buttons.github.io/buttons.js"></script>
824
</head>
925
<body class="h-full antialiased light bg-gray-100">
@@ -84,17 +100,19 @@ <h2 class="text-lg font-semibold leading-6 text-gray-900">Database Summary</h2>
84100
</div>
85101
</div>
86102
<!-- Server Logs -->
87-
<div class="mt-4 divide-y divide-gray-200 overflow-hidden rounded-lg bg-white shadow">
103+
<div class="mt-4 divide-y divide-gray-200 rounded-lg bg-white shadow">
88104
<div class="px-4 py-5 sm:p-6">
89105
<h2 class="text-lg font-semibold leading-6 text-gray-900">Server Logs</h2>
90106
</div>
91107
<div class="px-4 py-5 sm:p-6">
92-
<ul role="list" id="messages" v-if="messages" class="divide-y divide-gray-200">
93-
<li v-for="item in messages" class="even:bg-gray-50 odd:bg-white">
94-
{{ item }}
95-
</li>
96-
</ul>
97-
<p v-else>No logs available. Send a request to view here.</p>
108+
<div ref="logsContainer" class="logs-container border border-gray-200 rounded bg-gray-50" style="height: 300px !important; overflow-y: scroll !important; overflow-x: hidden !important; scrollbar-width: auto !important; -webkit-overflow-scrolling: touch;">
109+
<ul role="list" id="messages" v-if="messages" class="divide-y divide-gray-200" style="min-height: 350px;">
110+
<li v-for="item in messages" class="even:bg-gray-50 odd:bg-white px-3 py-2">
111+
{{ item }}
112+
</li>
113+
</ul>
114+
<p v-else class="px-3 py-2 text-gray-500" style="min-height: 350px;">No logs available. Send a request to view here.</p>
115+
</div>
98116
</div>
99117
</div>
100118
<!-- Environment Variables -->
@@ -112,14 +130,35 @@ <h2 class="text-base font-semibold leading-7 text-gray-900">Environmental Variab
112130
</div>
113131
<div class="sm:col-span-4">
114132
<label for="client_secret" class="block text-sm font-medium leading-6 text-gray-900">Client Secret</label>
115-
<div class="mt-2">
116-
<input v-model="client_secret" id="client_secret" name="client_secet" type="input" required="" class="block w-full px-3 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-gray-900 sm:text-sm sm:leading-6" />
133+
<div class="mt-2 flex gap-2">
134+
<input v-model="client_secret" id="client_secret" name="client_secet" :type="showClientSecret ? 'text' : 'password'" required="" class="block w-full px-3 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-gray-900 sm:text-sm sm:leading-6" />
135+
<button type="button" @click="toggleClientSecretVisibility" class="flex items-center justify-center px-3 py-1.5 text-gray-400 hover:text-gray-600 focus:outline-none">
136+
<svg v-if="!showClientSecret" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
137+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
138+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
139+
</svg>
140+
<svg v-else class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
141+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21" />
142+
</svg>
143+
</button>
117144
</div>
118145
</div>
119146
<div class="col-span-full">
120147
<label for="api_key" class="block text-sm font-medium leading-6 text-gray-900">API Key</label>
121-
<div class="mt-2">
122-
<textarea v-model="api_key" id="api_key" name="api_key" rows="3" class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-gray-900 sm:text-sm"></textarea>
148+
<div class="mt-2 flex gap-2">
149+
<textarea v-if="showApiKey" v-model="api_key" id="api_key" name="api_key" rows="3" class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-gray-900 sm:text-sm"></textarea>
150+
<div v-else class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 sm:text-sm min-h-[72px] bg-white flex items-start pt-1.5 px-3" style="font-family: inherit; letter-spacing: normal; font-size: 14px; line-height: 1.5;">
151+
<span style="word-break: break-all;">{{ api_key ? '•'.repeat(api_key.length) : '' }}</span>
152+
</div>
153+
<button type="button" @click="toggleApiKeyVisibility" class="flex items-center justify-center px-3 py-1.5 text-gray-400 hover:text-gray-600 focus:outline-none">
154+
<svg v-if="!showApiKey" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
155+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
156+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
157+
</svg>
158+
<svg v-else class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
159+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21" />
160+
</svg>
161+
</button>
123162
</div>
124163
</div>
125164
<div class="col-span-2">
@@ -163,6 +202,8 @@ <h2 class="text-base font-semibold leading-7 text-gray-900">Environmental Variab
163202
token_expiry: 1799,
164203
auth: false,
165204
alertMessage: null,
205+
showClientSecret: false,
206+
showApiKey: false,
166207
summary: {
167208
profiles: 0,
168209
accounts: 0,
@@ -178,7 +219,12 @@ <h2 class="text-base font-semibold leading-7 text-gray-900">Environmental Variab
178219
this.messages = [];
179220
}
180221
this.messages.push(msg);
181-
// window.scrollTo(0, document.body.scrollHeight);
222+
this.$nextTick(() => {
223+
const container = this.$refs.logsContainer;
224+
if (container) {
225+
container.scrollTop = container.scrollHeight;
226+
}
227+
});
182228
});
183229
this.socket.on('envs', (msg) => {
184230
this.client_id = msg.client_id;
@@ -255,6 +301,12 @@ <h2 class="text-base font-semibold leading-7 text-gray-900">Environmental Variab
255301
clearToast() {
256302
this.alertMessage = null;
257303
},
304+
toggleClientSecretVisibility() {
305+
this.showClientSecret = !this.showClientSecret;
306+
},
307+
toggleApiKeyVisibility() {
308+
this.showApiKey = !this.showApiKey;
309+
},
258310
},
259311
}).mount('#app')
260312
</script>

src/routes/accounts.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -362,8 +362,22 @@ router.delete('/:accountId/transactions/:postingDate',
362362
// function to create an account
363363
router.post('/', async (req: Request, res: Response) => {
364364
try {
365-
let account = Investec.account()
366-
account = { ...account, ...req.body }
365+
const baseAccount = Investec.account()
366+
367+
// Get the default profile (or first available profile)
368+
const defaultProfile = await prisma.profile.findFirst()
369+
if (!defaultProfile) {
370+
console.log('No profile found. Please seed the database first.')
371+
return formatErrorResponse(req, res, 500)
372+
}
373+
374+
const account = {
375+
...baseAccount,
376+
profileId: defaultProfile.profileId,
377+
profileName: defaultProfile.profileName,
378+
kycCompliant: true,
379+
...req.body
380+
}
367381
// check that the account exists
368382
const accountcheck = await prisma.account.findFirst({
369383
where: {
@@ -450,4 +464,33 @@ router.post('/beneficiaries', async (req: Request, res: Response) => {
450464
}
451465
})
452466

467+
// Delete a beneficiary
468+
router.delete('/za/pb/v1/accounts/beneficiaries/:beneficiaryId', async (req: Request, res: Response) => {
469+
try {
470+
const { beneficiaryId } = req.params
471+
472+
// Check if beneficiary exists
473+
const existingBeneficiary = await prisma.beneficiary.findUnique({
474+
where: { beneficiaryId },
475+
})
476+
477+
if (!existingBeneficiary) {
478+
return formatErrorResponse(req, res, 404)
479+
}
480+
481+
// Delete the beneficiary
482+
await prisma.beneficiary.delete({
483+
where: { beneficiaryId },
484+
})
485+
486+
// Emit database summary update for real-time dashboard
487+
await emitDatabaseSummary()
488+
489+
return res.status(200).json({})
490+
} catch (error) {
491+
console.log(error)
492+
return formatErrorResponse(req, res, 500)
493+
}
494+
})
495+
453496
export default router

src/routes/cards.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,20 @@ import { v4 as uuidv4 } from 'uuid'
77
import emu from 'programmable-card-code-emulator'
88
import { ExecutionItem } from '../types.js'
99

10+
interface CardResponse {
11+
CardKey: string
12+
CardNumber: string
13+
IsProgrammable: boolean
14+
status: string
15+
CardTypeCode: string
16+
AccountNumber: string
17+
AccountId: string
18+
}
19+
1020
router.get('/', async (req: Request, res: Response) => {
1121
try {
1222
const result = await prisma.card.findMany()
13-
const cards = Array()
23+
const cards: CardResponse[] = []
1424
result.forEach(card => {
1525
cards.push({
1626
CardKey: card.cardKey,

test/account.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ describe('Test the accounts', () => {
1212
accountName: 'Mr J Soap',
1313
referenceName: 'Mr J Soap',
1414
productName: 'Mortgage Loan Account',
15+
kycCompliant: true,
16+
profileId: '10001234567890',
17+
profileName: 'Joe Soap',
1518
})
1619
})
1720

test/card.spec.ts

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,29 +20,66 @@ describe('Test the card helpers', () => {
2020
it('should respond with the cards saved code', async () => {
2121
const response = await request(app).get('/za/v1/cards/700615/code')
2222
assert.equal(response.statusCode, 200)
23-
assert.deepEqual(response.body.data, {
23+
24+
const expectedStructure = {
2425
cardCode: {
2526
codeId: 'DC5A7EE9-DD2A-4327-9305-78B9185890CA',
26-
code: '// This function runs before a transaction.\nconst beforeTransaction = async (authorization) => {\n console.log(authorization);\n};\n// This function runs after a transaction was successful.\nconst afterTransaction = async (transaction) => {\n console.log(transaction);\n};\n// This function runs after a transaction was declined.\nconst afterDecline = async (transaction) => {\n console.log(transaction);\n};',
27-
createdAt: '2024-08-01T13:05:33.685Z',
28-
updatedAt: '2024-08-01T13:05:33.685Z',
29-
publishedAt: '2024-08-01T13:05:33.685Z',
27+
code: '// This function runs before a transaction.\n' +
28+
'const beforeTransaction = async (authorization) => {\n' +
29+
' console.log(authorization);\n' +
30+
'};\n' +
31+
'// This function runs after a transaction was successful.\n' +
32+
'const afterTransaction = async (transaction) => {\n' +
33+
' console.log(transaction);\n' +
34+
'};\n' +
35+
'// This function runs after a transaction was declined.\n' +
36+
'const afterDecline = async (transaction) => {\n' +
37+
' console.log(transaction);\n' +
38+
'};',
3039
},
31-
})
40+
}
41+
42+
assert.equal(response.body.data.cardCode.codeId, expectedStructure.cardCode.codeId)
43+
assert.equal(response.body.data.cardCode.code, expectedStructure.cardCode.code)
44+
45+
assert.exists(response.body.data.cardCode.createdAt)
46+
assert.exists(response.body.data.cardCode.updatedAt)
47+
assert.exists(response.body.data.cardCode.publishedAt)
48+
assert.isTrue(new Date(response.body.data.cardCode.createdAt) instanceof Date)
49+
assert.isTrue(new Date(response.body.data.cardCode.updatedAt) instanceof Date)
50+
assert.isTrue(new Date(response.body.data.cardCode.publishedAt) instanceof Date)
3251
})
3352

3453
it('should respond with the cards published code', async () => {
3554
const response = await request(app).get('/za/v1/cards/700615/publishedcode')
3655
assert.equal(response.statusCode, 200)
37-
assert.deepEqual(response.body.data, {
56+
57+
const expectedStructure = {
3858
cardCode: {
3959
codeId: '3BB77753-R2D2-4U2B-1A2B-4C213E7D0AC3',
40-
code: '// This function runs before a transaction.\nconst beforeTransaction = async (authorization) => {\n console.log(authorization);\n};\n// This function runs after a transaction was successful.\nconst afterTransaction = async (transaction) => {\n console.log(transaction);\n};\n// This function runs after a transaction was declined.\nconst afterDecline = async (transaction) => {\n console.log(transaction);\n};',
41-
createdAt: '2024-08-01T13:05:33.685Z',
42-
updatedAt: '2024-08-01T13:05:33.685Z',
43-
publishedAt: '2024-08-01T13:05:33.685Z',
60+
code: '// This function runs before a transaction.\n' +
61+
'const beforeTransaction = async (authorization) => {\n' +
62+
' console.log(authorization);\n' +
63+
'};\n' +
64+
'// This function runs after a transaction was successful.\n' +
65+
'const afterTransaction = async (transaction) => {\n' +
66+
' console.log(transaction);\n' +
67+
'};\n' +
68+
'// This function runs after a transaction was declined.\n' +
69+
'const afterDecline = async (transaction) => {\n' +
70+
' console.log(transaction);\n' +
71+
'};',
4472
},
45-
})
73+
}
74+
75+
assert.equal(response.body.data.cardCode.codeId, expectedStructure.cardCode.codeId)
76+
assert.equal(response.body.data.cardCode.code, expectedStructure.cardCode.code)
77+
assert.exists(response.body.data.cardCode.createdAt)
78+
assert.exists(response.body.data.cardCode.updatedAt)
79+
assert.exists(response.body.data.cardCode.publishedAt)
80+
assert.isTrue(new Date(response.body.data.cardCode.createdAt) instanceof Date)
81+
assert.isTrue(new Date(response.body.data.cardCode.updatedAt) instanceof Date)
82+
assert.isTrue(new Date(response.body.data.cardCode.publishedAt) instanceof Date)
4683
})
4784

4885
it('should respond with the merchant code list', async () => {

0 commit comments

Comments
 (0)