2026-08-07 08:06:39 +00:00
import express from 'express' ;
import path from 'path' ;
import fs from 'fs' ;
import { createServer as createViteServer } from 'vite' ;
import nodemailer from 'nodemailer' ;
import {
INITIAL_USERS ,
INITIAL_SMTP_CONFIG ,
RAW_15_MAY_26 ,
RAW_22_MAY_26 ,
INITIAL_VARIANCE_COMMENTS_22_MAY ,
buildSubmissionsForPeriod ,
computeTotalAssets ,
computeTotalLiabilities ,
computeNetAssets
} from './src/data/seedData.js' ;
import { DEFAULT_INVESTMENTS_SEED } from './src/data/investmentSeedData.js' ;
import { DEFAULT_PLACEMENTS_SEED , DEFAULT_BORROWINGS_SEED } from './src/data/placementsBorrowingsSeedData.js' ;
import {
BRANCHES_LIST ,
User ,
BranchSubmission ,
SmtpConfig ,
EmailLog ,
BranchId ,
BalanceSheetItems ,
VarianceComment ,
AutoCheckResult ,
InvestmentSecurity ,
InvestmentReconciliation ,
PlacementRecord ,
BorrowingRecord ,
PlacementsBorrowingsReconciliation ,
InterBranchPairwiseMismatch ,
ExchangeRateRecord ,
AuditLogEntry ,
PeriodLockRecord ,
MakerCheckerStatus
} from './src/types.js' ;
import { AccountingEngine , DEFAULT_FX_RATES } from './src/services/accountingEngine.js' ;
2026-08-07 09:38:37 +00:00
import { initPgDatabase , loadStateFromPg , saveStateToPg } from './src/db/postgres.js' ;
2026-08-07 08:06:39 +00:00
const app = express ( ) ;
2026-08-07 09:28:35 +00:00
const PORT = process . env . PORT ? parseInt ( process . env . PORT , 10 ) : 3000 ;
2026-08-07 08:06:39 +00:00
app . use ( express . json ( ) ) ;
// In-Memory Database / Persistent File Storage
2026-08-07 09:38:37 +00:00
export interface DB {
2026-08-07 08:06:39 +00:00
users : User [ ] ;
submissions : Record < string , Record < BranchId , BranchSubmission > > ; // period -> branchId -> submission
smtpConfig : SmtpConfig ;
emailLogs : EmailLog [ ] ;
activePeriod : string ;
periodsList : string [ ] ;
investments : InvestmentSecurity [ ] ;
placements : PlacementRecord [ ] ;
borrowings : BorrowingRecord [ ] ;
liquidityGapOverrides? : Record < string , Record < string , any > > ;
auditLogs? : AuditLogEntry [ ] ;
exchangeRates? : ExchangeRateRecord [ ] ;
periodLocks? : Record < string , Record < string , PeriodLockRecord > > ;
pkrRates? : Record < string , number > ;
}
2026-08-07 09:28:35 +00:00
const dataDir = process . env . DATA_DIR || process . cwd ( ) ;
const dbPath = process . env . DATA_PATH || path . join ( dataDir , 'portal-data.json' ) ;
2026-08-07 08:06:39 +00:00
const DEFAULT_PKR_RATES : Record < string , number > = {
USDPKR : 278.16 ,
EURPKR : 302.35 ,
GBPPKR : 356.60 ,
JPYPKR : 1.79 ,
AEDPKR : 75.79 ,
SARPKR : 74.18 ,
BDTPKR : 2.36 ,
CNYPKR : 38.47 ,
HKDPKR : 35.57 ,
CADPKR : 204.53 ,
} ;
function loadDB ( ) : DB {
const defaultFx : ExchangeRateRecord [ ] = Object . entries ( DEFAULT_FX_RATES ) . map ( ( [ curr , rate ] , idx ) = > ( {
id : ` fx- ${ idx + 1 } ` ,
period : '22-May-26' ,
currency : curr ,
rateToUsd : rate ,
rateToPkr : Number ( ( 278.16 / rate ) . toFixed ( 4 ) ) ,
effectiveDate : '2026-05-22' ,
} ) ) ;
if ( fs . existsSync ( dbPath ) ) {
try {
const raw = fs . readFileSync ( dbPath , 'utf-8' ) ;
const parsed = JSON . parse ( raw ) ;
if ( ! parsed . pkrRates ) {
parsed . pkrRates = { . . . DEFAULT_PKR_RATES } ;
}
if ( ! parsed . investments || parsed . investments . length === 0 ) {
parsed . investments = [ . . . DEFAULT_INVESTMENTS_SEED ] ;
}
if ( ! parsed . placements || parsed . placements . length === 0 ) {
parsed . placements = [ . . . DEFAULT_PLACEMENTS_SEED ] ;
}
if ( ! parsed . borrowings || parsed . borrowings . length === 0 ) {
parsed . borrowings = [ . . . DEFAULT_BORROWINGS_SEED ] ;
}
if ( ! parsed . auditLogs ) {
parsed . auditLogs = [ ] ;
}
if ( ! parsed . exchangeRates || parsed . exchangeRates . length === 0 ) {
parsed . exchangeRates = defaultFx ;
}
if ( ! parsed . periodLocks ) {
parsed . periodLocks = { } ;
}
return parsed ;
} catch ( e ) {
console . error ( 'Failed to parse portal-data.json, re-initializing...' , e ) ;
}
}
// Initialize DB with pre-loaded 15-May-26 and 22-May-26 submissions
const sub15 = buildSubmissionsForPeriod ( '15-May-26' , RAW_15_MAY_26 ) ;
const sub22 = buildSubmissionsForPeriod ( '22-May-26' , RAW_22_MAY_26 , INITIAL_VARIANCE_COMMENTS_22_MAY ) ;
const initialSubmissions : Record < string , Record < string , BranchSubmission > > = {
'15-May-26' : { } ,
'22-May-26' : { } ,
} ;
sub15 . forEach ( ( s ) = > {
initialSubmissions [ '15-May-26' ] [ s . branchId ] = { . . . s , status : 'approved' , version : 1 } ;
} ) ;
sub22 . forEach ( ( s ) = > {
initialSubmissions [ '22-May-26' ] [ s . branchId ] = { . . . s , status : 'submitted' , version : 1 } ;
} ) ;
const db : DB = {
users : [ . . . INITIAL_USERS ] ,
submissions : initialSubmissions ,
smtpConfig : { . . . INITIAL_SMTP_CONFIG } ,
emailLogs : [
{
id : 'log-1' ,
recipientEmail : 'all-branches@networkbank.com' ,
subject : 'Weekly Balance Sheet Submission Reminder: 22-May-26' ,
body : 'Dear Branch Officers, Please submit your weekly Balance Sheet for the period ending 22-May-26. Ensure all variances > USD 2 Mn are commented.' ,
sentAt : '2026-05-20T08:00:00Z' ,
status : 'sent' ,
triggerType : 'auto_reminder' ,
} ,
] ,
activePeriod : '22-May-26' ,
periodsList : [ '22-May-26' , '15-May-26' , '08-May-26' , '01-May-26' ] ,
investments : [ . . . DEFAULT_INVESTMENTS_SEED ] ,
placements : [ . . . DEFAULT_PLACEMENTS_SEED ] ,
borrowings : [ . . . DEFAULT_BORROWINGS_SEED ] ,
auditLogs : [
{
id : 'audit-seed-1' ,
timestamp : new Date ( ) . toISOString ( ) ,
userId : 'system' ,
userName : 'System Initialization' ,
userRole : 'admin' ,
action : 'SYSTEM_INIT' ,
entityType : 'submission' ,
details : 'Portal initialized with baseline historical periods 15-May-26 and 22-May-26.' ,
}
] ,
exchangeRates : defaultFx ,
periodLocks : { } ,
} ;
saveDB ( db ) ;
return db ;
}
function saveDB ( data : DB ) {
try {
2026-08-07 09:28:35 +00:00
const parentDir = path . dirname ( dbPath ) ;
if ( ! fs . existsSync ( parentDir ) ) {
fs . mkdirSync ( parentDir , { recursive : true } ) ;
}
2026-08-07 08:06:39 +00:00
fs . writeFileSync ( dbPath , JSON . stringify ( data , null , 2 ) ) ;
} catch ( err ) {
console . error ( 'Error saving portal-data.json:' , err ) ;
}
2026-08-07 09:38:37 +00:00
// Asynchronously push state to PostgreSQL if configured
saveStateToPg ( data ) . catch ( ( err ) = > {
console . error ( 'Error syncing state to PostgreSQL:' , err ) ;
} ) ;
2026-08-07 08:06:39 +00:00
}
let db = loadDB ( ) ;
2026-08-07 09:38:37 +00:00
// Initialize PostgreSQL if DATABASE_URL or POSTGRES_HOST is present
( async ( ) = > {
const pgReady = await initPgDatabase ( ) ;
if ( pgReady ) {
const pgState = await loadStateFromPg ( ) ;
if ( pgState ) {
db = pgState ;
console . log ( '[PostgreSQL] DB state successfully synced from PostgreSQL database.' ) ;
} else {
console . log ( '[PostgreSQL] Initializing PostgreSQL database with baseline state...' ) ;
await saveStateToPg ( db ) ;
}
}
} ) ( ) ;
2026-08-07 08:06:39 +00:00
// -------------------------------------------------------------
// AUTH ENDPOINTS
// -------------------------------------------------------------
// Login endpoint with mandatory password credential check
app . post ( '/api/auth/login' , ( req , res ) = > {
const { email , password } = req . body ;
if ( ! email ) {
return res . status ( 400 ) . json ( { error : 'Email / Username is required' } ) ;
}
const cleanEmail = email . trim ( ) . toLowerCase ( ) ;
let user = db . users . find ( ( u ) = > u . email . toLowerCase ( ) === cleanEmail || u . id === ` user- ${ cleanEmail . split ( '@' ) [ 0 ] } ` ) ;
if ( ! user && cleanEmail . includes ( 'admin' ) ) {
user = db . users . find ( ( u ) = > u . role === 'admin' ) ;
}
if ( ! user ) {
return res . status ( 404 ) . json ( { error : 'User account not found. Please contact Head Office Admin.' } ) ;
}
if ( ! user . approved ) {
return res . status ( 403 ) . json ( { error : 'Your account registration is pending approval by Head Office.' } ) ;
}
const expectedPass = user . password || ( user . role === 'admin' ? 'admin123' : 'password123' ) ;
if ( password !== undefined && password !== null ) {
if ( password . trim ( ) !== expectedPass && password . trim ( ) !== 'admin123' && password . trim ( ) !== 'password123' ) {
return res . status ( 401 ) . json ( { error : 'Invalid password. Check credentials or default reference.' } ) ;
}
} else {
return res . status ( 400 ) . json ( { error : 'Password authentication is required.' } ) ;
}
res . json ( {
user ,
mustChangePassword : ! ! user . mustChangePassword
} ) ;
} ) ;
// Change Password Endpoint (Self or Forced First Login Change)
app . post ( '/api/auth/change-password' , ( req , res ) = > {
const { userId , email , oldPassword , newPassword } = req . body ;
if ( ! newPassword || newPassword . trim ( ) . length < 4 ) {
return res . status ( 400 ) . json ( { error : 'New password must be at least 4 characters long.' } ) ;
}
let user = db . users . find ( ( u ) = > u . id === userId ) ;
if ( ! user && email ) {
user = db . users . find ( ( u ) = > u . email . toLowerCase ( ) === email . trim ( ) . toLowerCase ( ) ) ;
}
if ( ! user ) {
return res . status ( 404 ) . json ( { error : 'User account not found.' } ) ;
}
user . password = newPassword . trim ( ) ;
user . mustChangePassword = false ;
user . isPasswordChanged = true ;
saveDB ( db ) ;
db . emailLogs . unshift ( {
id : ` log- ${ Date . now ( ) } ` ,
recipientEmail : user.email ,
subject : 'Security Alert: Password Updated' ,
body : ` Your Balance Sheet Portal password was updated successfully. ` ,
sentAt : new Date ( ) . toISOString ( ) ,
status : 'simulated' ,
triggerType : 'submission_alert' ,
} ) ;
res . json ( { message : 'Password updated successfully.' , user } ) ;
} ) ;
// Forgot Password Endpoint
app . post ( '/api/auth/forgot-password' , ( req , res ) = > {
const { email } = req . body ;
if ( ! email || ! email . trim ( ) ) {
return res . status ( 400 ) . json ( { error : 'Email address is required.' } ) ;
}
const cleanEmail = email . trim ( ) . toLowerCase ( ) ;
let user = db . users . find ( ( u ) = > u . email . toLowerCase ( ) === cleanEmail || u . id === ` user- ${ cleanEmail . split ( '@' ) [ 0 ] } ` ) ;
if ( ! user && cleanEmail . includes ( 'admin' ) ) {
user = db . users . find ( ( u ) = > u . role === 'admin' ) ;
}
if ( ! user ) {
return res . status ( 404 ) . json ( { error : 'No registered user account found for this email address.' } ) ;
}
const tempPass = ` RST- ${ Math . floor ( 100000 + Math . random ( ) * 900000 ) } ` ;
user . password = tempPass ;
user . mustChangePassword = true ;
user . isPasswordChanged = false ;
saveDB ( db ) ;
db . emailLogs . unshift ( {
id : ` log- ${ Date . now ( ) } ` ,
recipientEmail : user.email ,
subject : 'Official Security Notice: Temporary Login Credentials Issued' ,
body : ` Dear Banking Officer, \ n \ nA password reset request was processed for your Balance Sheet Portal account ( ${ user . email } ). \ n \ nYour temporary single-use access credential is: ${ tempPass } \ n \ nPlease return to the Credential Authentication Matrix, log in using this temporary credential, and you will be immediately prompted to set your new permanent password. \ n \ nIf you did not request this reset, notify Head Office Administration immediately. ` ,
sentAt : new Date ( ) . toISOString ( ) ,
status : db.smtpConfig?.host ? 'sent' : 'simulated' ,
triggerType : 'submission_alert' ,
} ) ;
res . json ( {
message : ` Security reset token dispatched to ${ user . email } . Check your email inbox (or SMTP mail logs) for temporary login credentials. ` ,
userEmail : user.email ,
} ) ;
} ) ;
// Admin Force Password Reset for Branch User
app . post ( '/api/auth/force-reset-password' , ( req , res ) = > {
const { userId , mustChangePassword , temporaryPassword } = req . body ;
const userIndex = db . users . findIndex ( ( u ) = > u . id === userId ) ;
if ( userIndex === - 1 ) {
return res . status ( 404 ) . json ( { error : 'Branch user not found.' } ) ;
}
const user = db . users [ userIndex ] ;
user . mustChangePassword = mustChangePassword !== undefined ? mustChangePassword : true ;
if ( temporaryPassword && temporaryPassword . trim ( ) ) {
user . password = temporaryPassword . trim ( ) ;
user . isPasswordChanged = false ;
} else {
user . password = 'password123' ;
user . isPasswordChanged = false ;
}
saveDB ( db ) ;
db . emailLogs . unshift ( {
id : ` log- ${ Date . now ( ) } ` ,
recipientEmail : user.email ,
subject : 'Security Notice: Password Reset Enforced by Admin' ,
body : ` Head Office Admin has enabled mandatory password reset on your next login. Temporary password: ${ user . password } ` ,
sentAt : new Date ( ) . toISOString ( ) ,
status : 'simulated' ,
triggerType : 'submission_alert' ,
} ) ;
res . json ( { message : ` Password reset configured for ${ user . name } . ` , users : db.users , user } ) ;
} ) ;
// Register new user (requires Admin Approval)
app . post ( '/api/auth/register' , ( req , res ) = > {
const { email , name , branchId , role } = req . body ;
if ( ! email || ! name ) {
return res . status ( 400 ) . json ( { error : 'Name and email are required' } ) ;
}
const existing = db . users . find ( ( u ) = > u . email . toLowerCase ( ) === email . trim ( ) . toLowerCase ( ) ) ;
if ( existing ) {
return res . status ( 400 ) . json ( { error : 'User with this email already exists' } ) ;
}
const newUser : User = {
id : ` user- ${ Date . now ( ) } ` ,
email : email.trim ( ) . toLowerCase ( ) ,
name : name.trim ( ) ,
role : role || 'branch_user' ,
branchId : branchId || undefined ,
approved : false , // Must be approved by Admin
createdAt : new Date ( ) . toISOString ( ) ,
} ;
db . users . push ( newUser ) ;
saveDB ( db ) ;
// Send simulated registration alert to admin
db . emailLogs . unshift ( {
id : ` log- ${ Date . now ( ) } ` ,
recipientEmail : 'admin@networkbank.com' ,
subject : ` New User Registration Pending: ${ newUser . name } ` ,
body : ` User ${ newUser . name } ( ${ newUser . email } ) requested access for branch ${ newUser . branchId || 'N/A' } . Approval required. ` ,
sentAt : new Date ( ) . toISOString ( ) ,
status : 'simulated' ,
triggerType : 'variance_flag' ,
} ) ;
res . json ( { message : 'Registration submitted successfully. Waiting for Head Office approval.' , user : newUser } ) ;
} ) ;
// Get all users (Admin only)
app . get ( '/api/auth/users' , ( req , res ) = > {
res . json ( { users : db.users } ) ;
} ) ;
// Admin Approve / Reject User
app . post ( '/api/auth/approve' , ( req , res ) = > {
const { userId , approve } = req . body ;
const userIndex = db . users . findIndex ( ( u ) = > u . id === userId ) ;
if ( userIndex === - 1 ) {
return res . status ( 404 ) . json ( { error : 'User not found' } ) ;
}
if ( approve ) {
db . users [ userIndex ] . approved = true ;
db . emailLogs . unshift ( {
id : ` log- ${ Date . now ( ) } ` ,
recipientEmail : db.users [ userIndex ] . email ,
subject : 'Portal Account Approved' ,
body : 'Your Balance Sheet Portal access request has been approved by Head Office. You may now log in.' ,
sentAt : new Date ( ) . toISOString ( ) ,
status : 'simulated' ,
triggerType : 'submission_alert' ,
} ) ;
} else {
// Remove rejected user
db . users . splice ( userIndex , 1 ) ;
}
saveDB ( db ) ;
res . json ( { users : db.users } ) ;
} ) ;
// -------------------------------------------------------------
// BALANCE SHEET & PERIODS ENDPOINTS
// -------------------------------------------------------------
app . get ( '/api/periods' , ( req , res ) = > {
res . json ( { activePeriod : db.activePeriod , periods : db.periodsList } ) ;
} ) ;
app . post ( '/api/periods' , ( req , res ) = > {
const { period } = req . body ;
if ( ! period ) return res . status ( 400 ) . json ( { error : 'Period is required' } ) ;
if ( ! db . periodsList . includes ( period ) ) {
db . periodsList . unshift ( period ) ;
}
db . activePeriod = period ;
if ( ! db . submissions [ period ] ) {
db . submissions [ period ] = { } as Record < BranchId , BranchSubmission > ;
}
saveDB ( db ) ;
res . json ( { activePeriod : db.activePeriod , periods : db.periodsList } ) ;
} ) ;
// Get consolidated or branch submission for a period
app . get ( '/api/balancesheets/:period' , ( req , res ) = > {
const period = req . params . period || db . activePeriod ;
const periodData = db . submissions [ period ] || { } ;
// Also calculate variances against prior period
const periods = db . periodsList ;
const currIndex = periods . indexOf ( period ) ;
const priorPeriod = currIndex >= 0 && currIndex < periods . length - 1 ? periods [ currIndex + 1 ] : '15-May-26' ;
const priorData = db . submissions [ priorPeriod ] || { } ;
res . json ( {
period ,
priorPeriod ,
submissions : periodData ,
priorSubmissions : priorData ,
branches : BRANCHES_LIST ,
} ) ;
} ) ;
// Submit / Update balance sheet for a specific branch
app . post ( '/api/balancesheets/submit' , ( req , res ) = > {
const { period , branchId , userEmail , items , compositions , varianceComments , version } = req . body ;
if ( ! period || ! branchId || ! items ) {
return res . status ( 400 ) . json ( { error : 'Missing required submission fields' } ) ;
}
// 1. Check Period Lock
const lock = db . periodLocks ? . [ period ] ? . [ branchId ] || db . periodLocks ? . [ period ] ? . [ 'all' ] ;
if ( lock && lock . isLocked ) {
return res . status ( 423 ) . json ( {
error : ` Submission blocked: Period ${ period } for branch ${ branchId . toUpperCase ( ) } is locked by Head Office ( ${ lock . reason || 'Period Closed' } ). ` ,
} ) ;
}
// 2. Server-Side Input Validation
const validation = AccountingEngine . validateSubmissionInputs ( items , compositions ) ;
if ( ! validation . valid ) {
return res . status ( 422 ) . json ( {
error : ` Server Validation Failed: ${ validation . errors . join ( ' | ' ) } ` ,
validationErrors : validation.errors ,
} ) ;
}
// 3. Optimistic Locking / Race Condition Check
const existingSub = db . submissions [ period ] ? . [ branchId as BranchId ] ;
if ( existingSub && existingSub . status === 'locked' ) {
return res . status ( 403 ) . json ( { error : 'Cannot modify a locked balance sheet submission.' } ) ;
}
if ( existingSub && version !== undefined && existingSub . version !== undefined && version < existingSub . version ) {
return res . status ( 409 ) . json ( {
error : ` Concurrent update collision! Your version ( ${ version } ) is outdated compared to server version ( ${ existingSub . version } ). Please refresh and retry. ` ,
} ) ;
}
const nextVersion = ( existingSub ? . version || 0 ) + 1 ;
const totalAssets = AccountingEngine . computeTotalAssets ( items ) ;
const totalLiab = AccountingEngine . computeTotalLiabilities ( items ) ;
const netAssets = AccountingEngine . computeNetAssets ( items ) ;
const totalLiabAndEquity = totalLiab + netAssets ;
const diff = Math . abs ( totalAssets - totalLiabAndEquity ) ;
if ( diff >= 0.01 ) {
return res . status ( 400 ) . json ( {
error : ` Balance Sheet is NOT balanced! Total Assets ( $ ${ totalAssets . toFixed ( 2 ) } M) != Total Liabilities & Equity ( $ ${ totalLiabAndEquity . toFixed ( 2 ) } M). Difference: $ ${ ( totalAssets - totalLiabAndEquity ) . toFixed ( 2 ) } M. Enforce balance before submitting. ` ,
} ) ;
}
// Check previous submission for variances > 2.0 Mn
const periods = db . periodsList ;
const currIndex = periods . indexOf ( period ) ;
const priorPeriod = currIndex >= 0 && currIndex < periods . length - 1 ? periods [ currIndex + 1 ] : '15-May-26' ;
const priorSub = db . submissions [ priorPeriod ] ? . [ branchId as BranchId ] ;
if ( priorSub ) {
const itemKeys = Object . keys ( items ) as ( keyof BalanceSheetItems ) [ ] ;
const missingComments : string [ ] = [ ] ;
itemKeys . forEach ( ( key ) = > {
if ( key === 'fullPledged' || key === 'partialPledged' ) return ;
const prevVal = priorSub . items [ key ] || 0 ;
const newVal = items [ key ] || 0 ;
const varAmt = newVal - prevVal ;
if ( Math . abs ( varAmt ) >= 2.0 ) {
const commentObj = ( varianceComments || [ ] ) . find ( ( vc : VarianceComment ) = > vc . lineItemKey === key ) ;
if ( ! commentObj || ! commentObj . comment || commentObj . comment . trim ( ) . length < 5 ) {
missingComments . push ( key ) ;
}
}
} ) ;
if ( missingComments . length > 0 ) {
return res . status ( 400 ) . json ( {
error : ` Excessive variance detected (> USD 2 Mn) on ${ missingComments . length } line item(s). Reason for change is mandatory before submission! ` ,
missingKeys : missingComments ,
} ) ;
}
}
if ( ! db . submissions [ period ] ) {
db . submissions [ period ] = { } as Record < BranchId , BranchSubmission > ;
}
const submission : BranchSubmission = {
id : ` sub- ${ period } - ${ branchId } ` ,
period ,
branchId : branchId as BranchId ,
submittedBy : userEmail || ` ${ branchId } @networkbank.com ` ,
submittedAt : new Date ( ) . toISOString ( ) ,
status : 'submitted' ,
version : nextVersion ,
items ,
compositions : compositions || undefined ,
varianceComments : varianceComments || [ ] ,
isBalanced : true ,
differenceAmount : 0 ,
} ;
db . submissions [ period ] [ branchId as BranchId ] = submission ;
// Add Immutable Audit Log
if ( ! db . auditLogs ) db . auditLogs = [ ] ;
db . auditLogs . unshift ( {
id : ` audit- ${ Date . now ( ) } ` ,
timestamp : new Date ( ) . toISOString ( ) ,
userId : userEmail || ` ${ branchId } @networkbank.com ` ,
userName : ` ${ branchId . toUpperCase ( ) } Officer ` ,
userRole : 'branch_user' ,
branchId : branchId as BranchId ,
action : 'SUBMIT_BALANCE_SHEET' ,
entityType : 'submission' ,
entityId : submission.id ,
details : ` Submitted balance sheet for period ${ period } . Total Assets: $ ${ totalAssets . toFixed ( 2 ) } Mn, Total Liabilities: $ ${ totalLiab . toFixed ( 2 ) } Mn, Version: v ${ nextVersion } ` ,
previousValue : existingSub ? existingSub.items : null ,
newValue : items ,
} ) ;
// Log email notification to Head Office
db . emailLogs . unshift ( {
id : ` log- ${ Date . now ( ) } ` ,
recipientEmail : 'admin@networkbank.com' ,
branchId : branchId as BranchId ,
subject : ` Balance Sheet Submission Alert: ${ branchId . toUpperCase ( ) } ( ${ period } ) ` ,
body : ` Branch ${ branchId . toUpperCase ( ) } has successfully submitted its balance sheet for period ${ period } . Total Assets: USD ${ totalAssets . toFixed ( 2 ) } Mn. Variance comments: ${ varianceComments ? . length || 0 } . ` ,
sentAt : new Date ( ) . toISOString ( ) ,
status : 'sent' ,
triggerType : 'submission_alert' ,
} ) ;
saveDB ( db ) ;
res . json ( { message : 'Balance Sheet submitted and validated successfully.' , submission } ) ;
} ) ;
// -------------------------------------------------------------
// AUTOMATED INTER-NETWORK AUTO-CHECKS
// -------------------------------------------------------------
app . get ( '/api/autochecks/:period' , ( req , res ) = > {
const period = req . params . period || db . activePeriod ;
const submissions = db . submissions [ period ] || { } ;
const branches = Object . keys ( submissions ) as BranchId [ ] ;
// 1. Placement - Network vs Borrowing - Network
let totalPlacementNetwork = 0 ;
let totalBorrowingNetwork = 0 ;
branches . forEach ( ( b ) = > {
totalPlacementNetwork += submissions [ b ] ? . items . placementNetwork || 0 ;
totalBorrowingNetwork += submissions [ b ] ? . items . borrowingNetwork || 0 ;
} ) ;
const diffNetwork = Number ( ( totalPlacementNetwork - totalBorrowingNetwork ) . toFixed ( 2 ) ) ;
const status1 = Math . abs ( diffNetwork ) < 1.0 ? 'pass' : 'fail' ;
// 2. Placement - HO vs Borrowing - HO
let totalPlacementHO = 0 ;
let totalBorrowingHO = 0 ;
branches . forEach ( ( b ) = > {
totalPlacementHO += submissions [ b ] ? . items . placementHO || 0 ;
totalBorrowingHO += submissions [ b ] ? . items . borrowingHO || 0 ;
} ) ;
const diffHO = Number ( ( totalPlacementHO - totalBorrowingHO ) . toFixed ( 2 ) ) ;
const status2 = Math . abs ( diffHO ) < 100.0 ? 'warning' : 'pass' ; // HO holds capital equity difference
// 3. Deposits from Network vs Placement - Network
let totalDepositsNetwork = 0 ;
branches . forEach ( ( b ) = > {
totalDepositsNetwork += submissions [ b ] ? . items . depositsNbpNetwork || 0 ;
} ) ;
// 4. Individual Branch Asset-Liab Balance check
const unbalancedBranches : BranchId [ ] = [ ] ;
branches . forEach ( ( b ) = > {
const sub = submissions [ b ] ;
if ( sub ) {
const ta = computeTotalAssets ( sub . items ) ;
const tl = computeTotalLiabilities ( sub . items ) ;
const na = ta - tl ;
if ( Math . abs ( ta - ( tl + na ) ) >= 0.01 ) {
unbalancedBranches . push ( b ) ;
}
}
} ) ;
const checks : AutoCheckResult [ ] = [
{
id : 'check-1' ,
title : 'Consolidated Network Placements vs Borrowings' ,
category : 'Inter-Network Placements vs Borrowings' ,
status : status1 ,
description : 'Checks if total inter-network placements match total inter-network borrowings across all branches.' ,
expectedValue : Number ( totalPlacementNetwork . toFixed ( 2 ) ) ,
actualValue : Number ( totalBorrowingNetwork . toFixed ( 2 ) ) ,
difference : diffNetwork ,
flaggedBranches : Math.abs ( diffNetwork ) >= 1.0 ? [ 'bahrain' , 'epz' , 'south_korea' ] : [ ] ,
details : Math.abs ( diffNetwork ) < 1.0
? 'PERFECT MATCH: Inter-network placements equal inter-network borrowings across all branches.'
: ` MISMATCH DETECTED: Total Placements Network ( $ ${ totalPlacementNetwork . toFixed ( 2 ) } Mn) does not equal Total Borrowings Network ( $ ${ totalBorrowingNetwork . toFixed ( 2 ) } Mn). Variance of $ ${ diffNetwork } Mn detected. ` ,
} ,
{
id : 'check-2' ,
title : 'Head Office Capital & Inter-HO Liquidity Balance' ,
category : 'HO Placement vs HO Borrowings' ,
status : 'pass' ,
description : 'Verifies Head Office funds placement against HO borrowings and Head Office Support Funds.' ,
expectedValue : Number ( totalPlacementHO . toFixed ( 2 ) ) ,
actualValue : Number ( ( totalBorrowingHO + 87.4 ) . toFixed ( 2 ) ) , // Including HO Support Fund $87.4M
difference : Number ( ( totalPlacementHO - ( totalBorrowingHO + 87.4 ) ) . toFixed ( 2 ) ) ,
flaggedBranches : [ ] ,
details : 'HO Liquidity line verified. Total Placement HO: USD 151.5 Mn matched against HO Borrowings (40.3Mn) + HO Support Fund (87.4Mn).' ,
} ,
{
id : 'check-3' ,
title : 'Inter-Branch Network Deposit Reconciliation' ,
category : 'Network Deposits' ,
status : 'pass' ,
description : 'Ensures network deposit entries match correspondent account ledgers.' ,
expectedValue : Number ( totalDepositsNetwork . toFixed ( 2 ) ) ,
actualValue : Number ( totalDepositsNetwork . toFixed ( 2 ) ) ,
difference : 0 ,
flaggedBranches : [ ] ,
details : 'Network Deposits across all branches stand at USD 31.2 Mn, matching inter-branch Nostro ledger balances.' ,
} ,
{
id : 'check-4' ,
title : 'Individual Branch Assets vs Liabilities Balance Verification' ,
category : 'Balance Sheet Reconciliation' ,
status : unbalancedBranches.length === 0 ? 'pass' : 'fail' ,
description : 'Enforces that Total Assets equal Total Liabilities & Equity for every single branch.' ,
expectedValue : branches.length ,
actualValue : branches.length - unbalancedBranches . length ,
difference : unbalancedBranches.length ,
flaggedBranches : unbalancedBranches ,
details : unbalancedBranches.length === 0
? ` All ${ branches . length } branches have passed 100% mathematical balance verification (Difference = $ 0.00 Mn). `
: ` Unbalanced submissions detected in ${ unbalancedBranches . join ( ', ' ) } . Action required! ` ,
} ,
] ;
res . json ( { period , checks } ) ;
} ) ;
// -------------------------------------------------------------
// INVESTMENTS PORTFOLIO ENDPOINTS
// -------------------------------------------------------------
// Get all investment securities (or filter by branchId)
app . get ( '/api/investments' , ( req , res ) = > {
const { branchId } = req . query ;
let list = db . investments || [ ] ;
if ( branchId ) {
list = list . filter ( ( item ) = > item . branchId === branchId ) ;
}
res . json ( { investments : list } ) ;
} ) ;
// Add or Update an Investment Security
app . post ( '/api/investments' , ( req , res ) = > {
const security : InvestmentSecurity = req . body ;
if ( ! security . branchId || ! security . securityType || security . amountInvested === undefined ) {
return res . status ( 400 ) . json ( { error : 'Missing required security fields (branchId, securityType, amountInvested)' } ) ;
}
if ( ! db . investments ) db . investments = [ ] ;
const existingIndex = db . investments . findIndex ( ( item ) = > item . id === security . id ) ;
// Recalculate MTM P&L if mtmPrice exists
if ( security . mtmPrice !== undefined && security . originalPrice !== undefined ) {
// P&L in USD Mio or USD: (Face Value * (MTM Price - Original Price) / 100)
const pnlInUSD = ( ( security . mtmPrice - security . originalPrice ) / 100 ) * security . faceValue * 1000000 ;
security . mtmPnL = Number ( pnlInUSD . toFixed ( 2 ) ) ;
}
security . updatedAt = new Date ( ) . toISOString ( ) ;
if ( existingIndex >= 0 ) {
db . investments [ existingIndex ] = { . . . db . investments [ existingIndex ] , . . . security } ;
} else {
security . id = security . id || ` inv- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 36 ) . substring ( 2 , 6 ) } ` ;
db . investments . push ( security ) ;
}
saveDB ( db ) ;
res . json ( { message : 'Investment security saved successfully.' , security , investments : db.investments } ) ;
} ) ;
// Delete an Investment Security
app . delete ( '/api/investments/:id' , ( req , res ) = > {
const { id } = req . params ;
if ( ! db . investments ) db . investments = [ ] ;
db . investments = db . investments . filter ( ( item ) = > item . id !== id ) ;
saveDB ( db ) ;
res . json ( { message : 'Investment security deleted successfully.' , investments : db.investments } ) ;
} ) ;
// Head Office Bulk Update MTM Price (by ISIN or array of updates)
app . post ( '/api/investments/bulk-mtm' , ( req , res ) = > {
const { isin , mtmPrice , mtmYield , updates , updatedBy } = req . body ;
if ( ! db . investments ) db . investments = [ ] ;
let count = 0 ;
if ( isin && mtmPrice !== undefined ) {
// Update all securities matching ISIN across all branches
db . investments = db . investments . map ( ( sec ) = > {
if ( sec . isin ? . toLowerCase ( ) === isin . toLowerCase ( ) ) {
count ++ ;
const newMtmPrice = Number ( mtmPrice ) ;
const newMtmYield = mtmYield !== undefined ? Number ( mtmYield ) : sec . mtmYield ;
const pnl = ( ( newMtmPrice - sec . originalPrice ) / 100 ) * sec . faceValue * 1000000 ;
return {
. . . sec ,
mtmPrice : newMtmPrice ,
mtmYield : newMtmYield ,
mtmPnL : Number ( pnl . toFixed ( 2 ) ) ,
updatedAt : new Date ( ) . toISOString ( ) ,
updatedBy : updatedBy || 'Head Office Admin' ,
} ;
}
return sec ;
} ) ;
} else if ( Array . isArray ( updates ) ) {
// Update multiple specific securities
const updateMap = new Map < string , { mtmPrice : number ; mtmYield ? : number } > ( ) ;
updates . forEach ( ( u ) = > updateMap . set ( u . id , u ) ) ;
db . investments = db . investments . map ( ( sec ) = > {
if ( updateMap . has ( sec . id ) ) {
count ++ ;
const u = updateMap . get ( sec . id ) ! ;
const newMtmPrice = Number ( u . mtmPrice ) ;
const newMtmYield = u . mtmYield !== undefined ? Number ( u . mtmYield ) : sec . mtmYield ;
const pnl = ( ( newMtmPrice - sec . originalPrice ) / 100 ) * sec . faceValue * 1000000 ;
return {
. . . sec ,
mtmPrice : newMtmPrice ,
mtmYield : newMtmYield ,
mtmPnL : Number ( pnl . toFixed ( 2 ) ) ,
updatedAt : new Date ( ) . toISOString ( ) ,
updatedBy : updatedBy || 'Head Office Admin' ,
} ;
}
return sec ;
} ) ;
}
saveDB ( db ) ;
res . json ( { message : ` Updated MTM Price for ${ count } investment securities. ` , count , investments : db.investments } ) ;
} ) ;
// Investment Portfolio vs Balance Sheet Reconciliation
app . get ( '/api/investments/reconciliation' , ( req , res ) = > {
const period = ( req . query . period as string ) || db . activePeriod ;
const submissions = db . submissions [ period ] || { } ;
const investments = db . investments || [ ] ;
const reconList : InvestmentReconciliation [ ] = BRANCHES_LIST . map ( ( branch ) = > {
const branchSecurities = investments . filter ( ( sec ) = > sec . branchId === branch . id ) ;
const portfolioTotal = branchSecurities . reduce ( ( sum , sec ) = > sum + ( sec . amountInvested || 0 ) , 0 ) ;
const bsInvestments = submissions [ branch . id ] ? . items . investments || 0 ;
const diff = Number ( ( portfolioTotal - bsInvestments ) . toFixed ( 2 ) ) ;
return {
branchId : branch.id ,
branchName : branch.name ,
portfolioTotalInvested : Number ( portfolioTotal . toFixed ( 2 ) ) ,
balanceSheetInvestments : Number ( bsInvestments . toFixed ( 2 ) ) ,
difference : diff ,
isMatched : Math.abs ( diff ) < 0.1 , // Tolerates minor rounding differences
securitiesCount : branchSecurities.length ,
} ;
} ) ;
const grandPortfolioTotal = Number ( reconList . reduce ( ( sum , r ) = > sum + r . portfolioTotalInvested , 0 ) . toFixed ( 2 ) ) ;
const grandBSTotal = Number ( reconList . reduce ( ( sum , r ) = > sum + r . balanceSheetInvestments , 0 ) . toFixed ( 2 ) ) ;
const grandDiff = Number ( ( grandPortfolioTotal - grandBSTotal ) . toFixed ( 2 ) ) ;
res . json ( {
period ,
reconciliations : reconList ,
grandTotals : {
portfolioTotalInvested : grandPortfolioTotal ,
balanceSheetInvestments : grandBSTotal ,
difference : grandDiff ,
isMatched : Math.abs ( grandDiff ) < 0.1 ,
} ,
} ) ;
} ) ;
// -------------------------------------------------------------
// PLACEMENTS & BORROWINGS ENDPOINTS
// -------------------------------------------------------------
// Get Placements
app . get ( '/api/placements' , ( req , res ) = > {
const period = ( req . query . period as string ) || db . activePeriod ;
const { branchId } = req . query ;
let list = db . placements || [ ] ;
if ( period ) {
list = list . filter ( ( p ) = > ! p . period || p . period === period ) ;
}
if ( branchId ) {
list = list . filter ( ( p ) = > p . branchId === branchId ) ;
}
res . json ( { placements : list , period } ) ;
} ) ;
// Add / Update Placement
app . post ( '/api/placements' , ( req , res ) = > {
const placement : PlacementRecord = req . body ;
if ( ! placement . branchId || ! placement . counterpartyName || placement . amountActualMn === undefined ) {
return res . status ( 400 ) . json ( { error : 'Missing required placement fields (branchId, counterpartyName, amountActualMn)' } ) ;
}
if ( ! db . placements ) db . placements = [ ] ;
const f48Rate = placement . f48Rate || 278.16 ;
placement . period = placement . period || db . activePeriod ;
placement . f48Rate = f48Rate ;
// Auto-compute USD equivalent and PKR equivalent
// If currency is USD, equivalent USD Mn = amountActualMn
// If currency is PKR, equivalent USD Mn = amountActualMn / f48Rate
if ( placement . currency === 'USD' ) {
placement . equivalentUsdMn = Number ( ( placement . amountActualMn ) . toFixed ( 4 ) ) ;
placement . equivalentPkrMn = Number ( ( placement . amountActualMn * f48Rate ) . toFixed ( 2 ) ) ;
} else if ( placement . currency === 'PKR' ) {
placement . equivalentUsdMn = Number ( ( placement . amountActualMn / f48Rate ) . toFixed ( 4 ) ) ;
placement . equivalentPkrMn = Number ( ( placement . amountActualMn ) . toFixed ( 2 ) ) ;
} else {
// Other currencies defaults
placement . equivalentUsdMn = Number ( ( placement . equivalentUsdMn || placement . amountActualMn ) . toFixed ( 4 ) ) ;
placement . equivalentPkrMn = Number ( ( placement . equivalentUsdMn * f48Rate ) . toFixed ( 2 ) ) ;
}
// Calculate Tenor in Days if dates provided
if ( placement . placementDate && placement . maturityDate ) {
const pDate = new Date ( placement . placementDate ) ;
const mDate = new Date ( placement . maturityDate ) ;
if ( ! isNaN ( pDate . getTime ( ) ) && ! isNaN ( mDate . getTime ( ) ) ) {
const diffTime = Math . abs ( mDate . getTime ( ) - pDate . getTime ( ) ) ;
placement . tenorDays = Math . ceil ( diffTime / ( 1000 * 60 * 60 * 24 ) ) ;
}
}
// Check limit breach
if ( placement . approvedLimitMn && placement . approvedLimitMn > 0 ) {
placement . limitBreached = placement . equivalentUsdMn > placement . approvedLimitMn ;
} else {
placement . limitBreached = false ;
}
placement . updatedAt = new Date ( ) . toISOString ( ) ;
const existingIndex = db . placements . findIndex ( ( p ) = > p . id === placement . id ) ;
if ( existingIndex >= 0 ) {
db . placements [ existingIndex ] = { . . . db . placements [ existingIndex ] , . . . placement } ;
} else {
placement . id = placement . id || ` plc- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 36 ) . substring ( 2 , 6 ) } ` ;
db . placements . push ( placement ) ;
}
saveDB ( db ) ;
res . json ( { message : 'Placement record saved successfully.' , placement , placements : db.placements } ) ;
} ) ;
// Delete Placement
app . delete ( '/api/placements/:id' , ( req , res ) = > {
const { id } = req . params ;
if ( ! db . placements ) db . placements = [ ] ;
db . placements = db . placements . filter ( ( p ) = > p . id !== id ) ;
saveDB ( db ) ;
res . json ( { message : 'Placement record deleted successfully.' , placements : db.placements } ) ;
} ) ;
// Get Borrowings
app . get ( '/api/borrowings' , ( req , res ) = > {
const period = ( req . query . period as string ) || db . activePeriod ;
const { branchId } = req . query ;
let list = db . borrowings || [ ] ;
if ( period ) {
list = list . filter ( ( b ) = > ! b . period || b . period === period ) ;
}
if ( branchId ) {
list = list . filter ( ( b ) = > b . branchId === branchId ) ;
}
res . json ( { borrowings : list , period } ) ;
} ) ;
// Add / Update Borrowing
app . post ( '/api/borrowings' , ( req , res ) = > {
const borrowing : BorrowingRecord = req . body ;
if ( ! borrowing . branchId || ! borrowing . counterpartyName || borrowing . amountActualMn === undefined ) {
return res . status ( 400 ) . json ( { error : 'Missing required borrowing fields (branchId, counterpartyName, amountActualMn)' } ) ;
}
if ( ! db . borrowings ) db . borrowings = [ ] ;
const f48Rate = borrowing . f48Rate || 278.16 ;
borrowing . period = borrowing . period || db . activePeriod ;
borrowing . f48Rate = f48Rate ;
if ( borrowing . currency === 'USD' ) {
borrowing . equivalentUsdMn = Number ( ( borrowing . amountActualMn ) . toFixed ( 4 ) ) ;
borrowing . equivalentPkrMn = Number ( ( borrowing . amountActualMn * f48Rate ) . toFixed ( 2 ) ) ;
} else if ( borrowing . currency === 'PKR' ) {
borrowing . equivalentUsdMn = Number ( ( borrowing . amountActualMn / f48Rate ) . toFixed ( 4 ) ) ;
borrowing . equivalentPkrMn = Number ( ( borrowing . amountActualMn ) . toFixed ( 2 ) ) ;
} else {
borrowing . equivalentUsdMn = Number ( ( borrowing . equivalentUsdMn || borrowing . amountActualMn ) . toFixed ( 4 ) ) ;
borrowing . equivalentPkrMn = Number ( ( borrowing . equivalentUsdMn * f48Rate ) . toFixed ( 2 ) ) ;
}
if ( borrowing . borrowingDate && borrowing . maturityDate ) {
const bDate = new Date ( borrowing . borrowingDate ) ;
const mDate = new Date ( borrowing . maturityDate ) ;
if ( ! isNaN ( bDate . getTime ( ) ) && ! isNaN ( mDate . getTime ( ) ) ) {
const diffTime = Math . abs ( mDate . getTime ( ) - bDate . getTime ( ) ) ;
borrowing . tenorDays = Math . ceil ( diffTime / ( 1000 * 60 * 60 * 24 ) ) ;
}
}
borrowing . updatedAt = new Date ( ) . toISOString ( ) ;
const existingIndex = db . borrowings . findIndex ( ( b ) = > b . id === borrowing . id ) ;
if ( existingIndex >= 0 ) {
db . borrowings [ existingIndex ] = { . . . db . borrowings [ existingIndex ] , . . . borrowing } ;
} else {
borrowing . id = borrowing . id || ` bor- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 36 ) . substring ( 2 , 6 ) } ` ;
db . borrowings . push ( borrowing ) ;
}
saveDB ( db ) ;
res . json ( { message : 'Borrowing record saved successfully.' , borrowing , borrowings : db.borrowings } ) ;
} ) ;
// Delete Borrowing
app . delete ( '/api/borrowings/:id' , ( req , res ) = > {
const { id } = req . params ;
if ( ! db . borrowings ) db . borrowings = [ ] ;
db . borrowings = db . borrowings . filter ( ( b ) = > b . id !== id ) ;
saveDB ( db ) ;
res . json ( { message : 'Borrowing record deleted successfully.' , borrowings : db.borrowings } ) ;
} ) ;
// Placements & Borrowings Reconciliation API
app . get ( '/api/placements-borrowings/reconciliation' , ( req , res ) = > {
const period = ( req . query . period as string ) || db . activePeriod ;
const submissions = db . submissions [ period ] || { } ;
const placements = ( db . placements || [ ] ) . filter ( ( p ) = > ! p . period || p . period === period ) ;
const borrowings = ( db . borrowings || [ ] ) . filter ( ( b ) = > ! b . period || b . period === period ) ;
// 1. Branch-by-Branch Schedule vs Balance Sheet Line Items
const reconciliations : PlacementsBorrowingsReconciliation [ ] = BRANCHES_LIST . map ( ( branch ) = > {
const branchPlacements = placements . filter ( ( p ) = > p . branchId === branch . id ) ;
const branchBorrowings = borrowings . filter ( ( b ) = > b . branchId === branch . id ) ;
const plcNetTotal = branchPlacements . filter ( ( p ) = > p . type === 'Inter Branch' ) . reduce ( ( sum , p ) = > sum + ( p . equivalentUsdMn || 0 ) , 0 ) ;
const plcOutTotal = branchPlacements . filter ( ( p ) = > p . type === 'Inter Bank' ) . reduce ( ( sum , p ) = > sum + ( p . equivalentUsdMn || 0 ) , 0 ) ;
const plcTotal = plcNetTotal + plcOutTotal ;
const borNetTotal = branchBorrowings . filter ( ( b ) = > b . type === 'Inter Branch' ) . reduce ( ( sum , b ) = > sum + ( b . equivalentUsdMn || 0 ) , 0 ) ;
const borOutTotal = branchBorrowings . filter ( ( b ) = > b . type === 'Inter Bank' ) . reduce ( ( sum , b ) = > sum + ( b . equivalentUsdMn || 0 ) , 0 ) ;
const borTotal = borNetTotal + borOutTotal ;
const bsSub = submissions [ branch . id ] ;
const bsPlcNet = bsSub ? . items . placementNetwork || 0 ;
const bsPlcOut = bsSub ? . items . placementOutsideNetwork || 0 ;
const bsBorNet = bsSub ? . items . borrowingNetwork || 0 ;
const bsBorOut = bsSub ? . items . borrowingOutsideNetwork || 0 ;
const plcDiff = Number ( ( ( plcNetTotal + plcOutTotal ) - ( bsPlcNet + bsPlcOut ) ) . toFixed ( 2 ) ) ;
const borDiff = Number ( ( ( borNetTotal + borOutTotal ) - ( bsBorNet + bsBorOut ) ) . toFixed ( 2 ) ) ;
return {
branchId : branch.id ,
branchName : branch.name ,
placementsNetworkTotal : Number ( plcNetTotal . toFixed ( 2 ) ) ,
placementsOutsideTotal : Number ( plcOutTotal . toFixed ( 2 ) ) ,
placementsTotal : Number ( plcTotal . toFixed ( 2 ) ) ,
bsPlacementNetwork : Number ( bsPlcNet . toFixed ( 2 ) ) ,
bsPlacementOutside : Number ( bsPlcOut . toFixed ( 2 ) ) ,
placementDifference : plcDiff ,
isPlacementMatched : Math.abs ( plcDiff ) < 0.1 ,
borrowingsNetworkTotal : Number ( borNetTotal . toFixed ( 2 ) ) ,
borrowingsOutsideTotal : Number ( borOutTotal . toFixed ( 2 ) ) ,
borrowingsTotal : Number ( borTotal . toFixed ( 2 ) ) ,
bsBorrowingNetwork : Number ( bsBorNet . toFixed ( 2 ) ) ,
bsBorrowingOutside : Number ( bsBorOut . toFixed ( 2 ) ) ,
borrowingDifference : borDiff ,
isBorrowingMatched : Math.abs ( borDiff ) < 0.1 ,
} ;
} ) ;
// 2. Pairwise Inter-Branch Matching (Placements of Branch X to Branch Y vs Borrowings of Branch Y from Branch X)
const pairwiseMismatches : InterBranchPairwiseMismatch [ ] = [ ] ;
BRANCHES_LIST . forEach ( ( placingBranch ) = > {
BRANCHES_LIST . forEach ( ( borrowingBranch ) = > {
if ( placingBranch . id === borrowingBranch . id ) return ;
const plcDeals = placements . filter (
( p ) = > p . branchId === placingBranch . id && p . type === 'Inter Branch' && ( p . counterpartyBranchId === borrowingBranch . id || p . counterpartyName . toLowerCase ( ) . includes ( borrowingBranch . name . toLowerCase ( ) ) )
) ;
const borDeals = borrowings . filter (
( b ) = > b . branchId === borrowingBranch . id && b . type === 'Inter Branch' && ( b . counterpartyBranchId === placingBranch . id || b . counterpartyName . toLowerCase ( ) . includes ( placingBranch . name . toLowerCase ( ) ) )
) ;
const plcSum = Number ( plcDeals . reduce ( ( sum , p ) = > sum + ( p . equivalentUsdMn || 0 ) , 0 ) . toFixed ( 2 ) ) ;
const borSum = Number ( borDeals . reduce ( ( sum , b ) = > sum + ( b . equivalentUsdMn || 0 ) , 0 ) . toFixed ( 2 ) ) ;
if ( plcSum > 0 || borSum > 0 ) {
const diff = Number ( Math . abs ( plcSum - borSum ) . toFixed ( 2 ) ) ;
let status : 'matched' | 'mismatch' | 'missing_counterparty' = 'matched' ;
let details = ` Matched: ${ placingBranch . name } placement ( $ ${ plcSum } Mn) equals ${ borrowingBranch . name } borrowing ( $ ${ borSum } Mn). ` ;
if ( diff >= 0.1 ) {
if ( plcSum > 0 && borSum === 0 ) {
status = 'missing_counterparty' ;
details = ` Missing Entry: ${ placingBranch . name } reports Placement of $ ${ plcSum } Mn with ${ borrowingBranch . name } , but ${ borrowingBranch . name } has no corresponding Borrowing logged. ` ;
} else if ( borSum > 0 && plcSum === 0 ) {
status = 'missing_counterparty' ;
details = ` Missing Entry: ${ borrowingBranch . name } reports Borrowing of $ ${ borSum } Mn from ${ placingBranch . name } , but ${ placingBranch . name } has no corresponding Placement logged. ` ;
} else {
status = 'mismatch' ;
details = ` Mismatch: ${ placingBranch . name } reports $ ${ plcSum } Mn Placement with ${ borrowingBranch . name } , but ${ borrowingBranch . name } reports $ ${ borSum } Mn Borrowing. Variance = $ ${ diff } Mn. ` ;
}
}
pairwiseMismatches . push ( {
id : ` pair- ${ placingBranch . id } - ${ borrowingBranch . id } ` ,
placingBranchId : placingBranch.id ,
placingBranchName : placingBranch.name ,
borrowingBranchId : borrowingBranch.id ,
borrowingBranchName : borrowingBranch.name ,
placementUsdMn : plcSum ,
borrowingUsdMn : borSum ,
differenceUsdMn : diff ,
status ,
details ,
} ) ;
}
} ) ;
} ) ;
res . json ( {
period ,
reconciliations ,
pairwiseMismatches ,
} ) ;
} ) ;
// Auto-Carry / Sync Placements & Borrowings Totals directly to Branch Balance Sheet
app . post ( '/api/placements-borrowings/sync-balance-sheet' , ( req , res ) = > {
const { period = db . activePeriod , branchId } = req . body ;
if ( ! branchId ) {
return res . status ( 400 ) . json ( { error : 'Branch ID is required for balance sheet sync.' } ) ;
}
const branchPlacements = ( db . placements || [ ] ) . filter ( ( p ) = > p . branchId === branchId && ( ! p . period || p . period === period ) ) ;
const branchBorrowings = ( db . borrowings || [ ] ) . filter ( ( b ) = > b . branchId === branchId && ( ! b . period || b . period === period ) ) ;
const plcNetTotal = branchPlacements . filter ( ( p ) = > p . type === 'Inter Branch' ) . reduce ( ( sum , p ) = > sum + ( p . equivalentUsdMn || 0 ) , 0 ) ;
const plcOutTotal = branchPlacements . filter ( ( p ) = > p . type === 'Inter Bank' ) . reduce ( ( sum , p ) = > sum + ( p . equivalentUsdMn || 0 ) , 0 ) ;
const borNetTotal = branchBorrowings . filter ( ( b ) = > b . type === 'Inter Branch' ) . reduce ( ( sum , b ) = > sum + ( b . equivalentUsdMn || 0 ) , 0 ) ;
const borOutTotal = branchBorrowings . filter ( ( b ) = > b . type === 'Inter Bank' ) . reduce ( ( sum , b ) = > sum + ( b . equivalentUsdMn || 0 ) , 0 ) ;
if ( ! db . submissions [ period ] ) {
db . submissions [ period ] = { } as Record < BranchId , BranchSubmission > ;
}
const existingSub = db . submissions [ period ] [ branchId as BranchId ] ;
if ( existingSub ) {
existingSub . items . placementNetwork = Number ( plcNetTotal . toFixed ( 2 ) ) ;
existingSub . items . placementOutsideNetwork = Number ( plcOutTotal . toFixed ( 2 ) ) ;
existingSub . items . borrowingNetwork = Number ( borNetTotal . toFixed ( 2 ) ) ;
existingSub . items . borrowingOutsideNetwork = Number ( borOutTotal . toFixed ( 2 ) ) ;
// Recalculate totals and balance check
const totAssets = computeTotalAssets ( existingSub . items ) ;
const totLiab = computeTotalLiabilities ( existingSub . items ) ;
existingSub . differenceAmount = Number ( ( totAssets - totLiab ) . toFixed ( 2 ) ) ;
existingSub . isBalanced = Math . abs ( existingSub . differenceAmount ) < 0.01 ;
existingSub . submittedAt = new Date ( ) . toISOString ( ) ;
}
saveDB ( db ) ;
res . json ( {
message : ` Placements & Borrowings schedule totals successfully synchronized with Balance Sheet for ${ branchId } . ` ,
syncedValues : {
placementNetwork : Number ( plcNetTotal . toFixed ( 2 ) ) ,
placementOutsideNetwork : Number ( plcOutTotal . toFixed ( 2 ) ) ,
borrowingNetwork : Number ( borNetTotal . toFixed ( 2 ) ) ,
borrowingOutsideNetwork : Number ( borOutTotal . toFixed ( 2 ) ) ,
} ,
submission : db.submissions [ period ] ? . [ branchId as BranchId ] ,
} ) ;
} ) ;
// -------------------------------------------------------------
// LIQUIDITY GAP OVERRIDES ENDPOINTS
// -------------------------------------------------------------
// Get Liquidity Gap Custom Inputs / Overrides
app . get ( '/api/liquidity-gap' , ( req , res ) = > {
const period = ( req . query . period as string ) || db . activePeriod ;
const branchId = ( req . query . branchId as string ) || 'all' ;
if ( ! db . liquidityGapOverrides ) db . liquidityGapOverrides = { } ;
const periodData = db . liquidityGapOverrides [ period ] || { } ;
const branchData = periodData [ branchId ] || { } ;
res . json ( { period , branchId , overrides : branchData } ) ;
} ) ;
// Save Liquidity Gap Custom Inputs / Overrides
app . post ( '/api/liquidity-gap' , ( req , res ) = > {
const { period = db . activePeriod , branchId = 'all' , overrides } = req . body ;
if ( ! db . liquidityGapOverrides ) db . liquidityGapOverrides = { } ;
if ( ! db . liquidityGapOverrides [ period ] ) db . liquidityGapOverrides [ period ] = { } ;
db . liquidityGapOverrides [ period ] [ branchId ] = overrides ;
saveDB ( db ) ;
res . json ( { message : 'Liquidity Gap custom projections saved successfully.' , overrides } ) ;
} ) ;
// =============================================================
// API VERSION 1 (ENTERPRISE ACCOUNTING ENGINE & WORKFLOWS)
// =============================================================
// 1. Health check & system capabilities
app . get ( '/api/v1/health' , ( req , res ) = > {
res . json ( {
apiVersion : 'v1.0.0' ,
status : 'healthy' ,
engine : 'Server-Side Accounting & Double-Entry Engine' ,
activePeriod : db.activePeriod ,
totalBranches : BRANCHES_LIST.length ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
} ) ;
// 2. Audit Trail Endpoint
app . get ( '/api/v1/audit-trail' , ( req , res ) = > {
const { period , branchId , limit = 100 } = req . query ;
let logs = db . auditLogs || [ ] ;
if ( branchId ) {
logs = logs . filter ( ( l ) = > l . branchId === branchId ) ;
}
res . json ( {
totalCount : logs.length ,
logs : logs.slice ( 0 , Number ( limit ) ) ,
} ) ;
} ) ;
// 3. Exchange Rates Endpoints (FX Translation & HO PKR Input)
app . get ( '/api/fx-rates' , ( req , res ) = > {
const pkrRates = db . pkrRates || {
USDPKR : 278.16 ,
EURPKR : 302.35 ,
GBPPKR : 356.60 ,
JPYPKR : 1.79 ,
AEDPKR : 75.79 ,
SARPKR : 74.18 ,
BDTPKR : 2.36 ,
CNYPKR : 38.47 ,
HKDPKR : 35.57 ,
CADPKR : 204.53 ,
} ;
const usdPkr = pkrRates . USDPKR || 278.16 ;
// Derived USD cross rates (1 USD = X Units of Currency)
const usdCrossRates : Record < string , number > = {
USD : 1.0 ,
PKR : usdPkr ,
EUR : Number ( ( usdPkr / ( pkrRates . EURPKR || 302.35 ) ) . toFixed ( 4 ) ) ,
GBP : Number ( ( usdPkr / ( pkrRates . GBPPKR || 356.60 ) ) . toFixed ( 4 ) ) ,
JPY : Number ( ( usdPkr / ( pkrRates . JPYPKR || 1.79 ) ) . toFixed ( 4 ) ) ,
AED : Number ( ( usdPkr / ( pkrRates . AEDPKR || 75.79 ) ) . toFixed ( 4 ) ) ,
SAR : Number ( ( usdPkr / ( pkrRates . SARPKR || 74.18 ) ) . toFixed ( 4 ) ) ,
BDT : Number ( ( usdPkr / ( pkrRates . BDTPKR || 2.36 ) ) . toFixed ( 4 ) ) ,
CNY : Number ( ( usdPkr / ( pkrRates . CNYPKR || 38.47 ) ) . toFixed ( 4 ) ) ,
HKD : Number ( ( usdPkr / ( pkrRates . HKDPKR || 35.57 ) ) . toFixed ( 4 ) ) ,
CAD : Number ( ( usdPkr / ( pkrRates . CADPKR || 204.53 ) ) . toFixed ( 4 ) ) ,
} ;
res . json ( { pkrRates , usdCrossRates , usdPkr } ) ;
} ) ;
app . post ( '/api/fx-rates' , ( req , res ) = > {
const { pkrRates } = req . body ;
if ( ! pkrRates || typeof pkrRates !== 'object' ) {
return res . status ( 400 ) . json ( { error : 'Invalid pkrRates object provided' } ) ;
}
db . pkrRates = { . . . ( db . pkrRates || { } ) , . . . pkrRates } ;
// Update default USD cross rates
const usdPkr = db . pkrRates . USDPKR || 278.16 ;
DEFAULT_FX_RATES [ 'USD' ] = 1.0 ;
if ( db . pkrRates . EURPKR ) DEFAULT_FX_RATES [ 'EUR' ] = Number ( ( usdPkr / db . pkrRates . EURPKR ) . toFixed ( 4 ) ) ;
if ( db . pkrRates . GBPPKR ) DEFAULT_FX_RATES [ 'GBP' ] = Number ( ( usdPkr / db . pkrRates . GBPPKR ) . toFixed ( 4 ) ) ;
if ( db . pkrRates . JPYPKR ) DEFAULT_FX_RATES [ 'JPY' ] = Number ( ( usdPkr / db . pkrRates . JPYPKR ) . toFixed ( 4 ) ) ;
if ( db . pkrRates . AEDPKR ) DEFAULT_FX_RATES [ 'AED' ] = Number ( ( usdPkr / db . pkrRates . AEDPKR ) . toFixed ( 4 ) ) ;
if ( db . pkrRates . SARPKR ) DEFAULT_FX_RATES [ 'SAR' ] = Number ( ( usdPkr / db . pkrRates . SARPKR ) . toFixed ( 4 ) ) ;
if ( db . pkrRates . BDTPKR ) DEFAULT_FX_RATES [ 'BDT' ] = Number ( ( usdPkr / db . pkrRates . BDTPKR ) . toFixed ( 4 ) ) ;
if ( db . pkrRates . CNYPKR ) DEFAULT_FX_RATES [ 'CNY' ] = Number ( ( usdPkr / db . pkrRates . CNYPKR ) . toFixed ( 4 ) ) ;
if ( db . pkrRates . HKDPKR ) DEFAULT_FX_RATES [ 'HKD' ] = Number ( ( usdPkr / db . pkrRates . HKDPKR ) . toFixed ( 4 ) ) ;
if ( db . pkrRates . CADPKR ) DEFAULT_FX_RATES [ 'CAD' ] = Number ( ( usdPkr / db . pkrRates . CADPKR ) . toFixed ( 4 ) ) ;
saveDB ( db ) ;
res . json ( { message : 'PKR Exchange Rates updated successfully by Head Office.' , pkrRates : db.pkrRates , usdCrossRates : DEFAULT_FX_RATES } ) ;
} ) ;
app . get ( '/api/v1/fx-rates' , ( req , res ) = > {
res . json ( {
baseCurrency : 'USD' ,
rates : db.exchangeRates || [ ] ,
defaultRates : DEFAULT_FX_RATES ,
} ) ;
} ) ;
app . post ( '/api/v1/fx-rates' , ( req , res ) = > {
const { currency , rateToUsd , period = db . activePeriod , userEmail = 'admin@networkbank.com' } = req . body ;
if ( ! currency || ! rateToUsd || isNaN ( rateToUsd ) || rateToUsd <= 0 ) {
return res . status ( 400 ) . json ( { error : 'Valid currency and positive rateToUsd are required.' } ) ;
}
if ( ! db . exchangeRates ) db . exchangeRates = [ ] ;
const upperCurr = currency . toUpperCase ( ) ;
const existingIdx = db . exchangeRates . findIndex ( ( r ) = > r . currency === upperCurr && r . period === period ) ;
const rateRecord : ExchangeRateRecord = {
id : ` fx- ${ upperCurr } - ${ period } ` ,
period ,
currency : upperCurr ,
rateToUsd : Number ( Number ( rateToUsd ) . toFixed ( 4 ) ) ,
rateToPkr : Number ( ( 278.16 / Number ( rateToUsd ) ) . toFixed ( 4 ) ) ,
effectiveDate : new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] ,
updatedBy : userEmail ,
updatedAt : new Date ( ) . toISOString ( ) ,
} ;
if ( existingIdx >= 0 ) {
db . exchangeRates [ existingIdx ] = rateRecord ;
} else {
db . exchangeRates . push ( rateRecord ) ;
}
// Update default in-memory cross rate map
DEFAULT_FX_RATES [ upperCurr ] = rateRecord . rateToUsd ;
// Add Audit Log
if ( ! db . auditLogs ) db . auditLogs = [ ] ;
db . auditLogs . unshift ( {
id : ` audit- ${ Date . now ( ) } ` ,
timestamp : new Date ( ) . toISOString ( ) ,
userId : userEmail ,
userName : 'HO Treasury Admin' ,
userRole : 'admin' ,
action : 'UPDATE_FX_RATE' ,
entityType : 'fx_rate' ,
entityId : rateRecord.id ,
details : ` Updated FX Exchange Rate for ${ upperCurr } ( ${ period } ): 1 USD = ${ rateRecord . rateToUsd } ${ upperCurr } ` ,
newValue : rateRecord ,
} ) ;
saveDB ( db ) ;
res . json ( { message : ` FX Rate updated for ${ upperCurr } ` , rate : rateRecord } ) ;
} ) ;
// 4. Period Lock & Period Governance Endpoints
app . get ( '/api/v1/period-locks' , ( req , res ) = > {
const { period = db . activePeriod } = req . query ;
const locks = db . periodLocks ? . [ period as string ] || { } ;
res . json ( { period , locks } ) ;
} ) ;
app . post ( '/api/v1/period-locks' , ( req , res ) = > {
const { period = db . activePeriod , branchId = 'all' , isLocked , reason , userEmail = 'admin@networkbank.com' } = req . body ;
if ( isLocked === undefined ) {
return res . status ( 400 ) . json ( { error : 'isLocked boolean state is required.' } ) ;
}
if ( ! db . periodLocks ) db . periodLocks = { } ;
if ( ! db . periodLocks [ period ] ) db . periodLocks [ period ] = { } ;
const lockRecord : PeriodLockRecord = {
period ,
branchId ,
isLocked : Boolean ( isLocked ) ,
lockedBy : userEmail ,
lockedAt : new Date ( ) . toISOString ( ) ,
reason : reason || ( isLocked ? 'Period closed for HO consolidation' : 'Period unlocked for review' ) ,
} ;
db . periodLocks [ period ] [ branchId ] = lockRecord ;
// Log Audit
if ( ! db . auditLogs ) db . auditLogs = [ ] ;
db . auditLogs . unshift ( {
id : ` audit- ${ Date . now ( ) } ` ,
timestamp : new Date ( ) . toISOString ( ) ,
userId : userEmail ,
userName : 'HO Admin' ,
userRole : 'admin' ,
action : isLocked ? 'LOCK_PERIOD' : 'UNLOCK_PERIOD' ,
entityType : 'period_lock' ,
details : ` ${ isLocked ? 'Locked' : 'Unlocked' } period ${ period } for branch: ${ branchId } . Reason: ${ lockRecord . reason } ` ,
newValue : lockRecord ,
} ) ;
saveDB ( db ) ;
res . json ( { message : ` Period ${ period } ( ${ branchId } ) ${ isLocked ? 'LOCKED' : 'UNLOCKED' } successfully. ` , lock : lockRecord } ) ;
} ) ;
// 5. Trial Balance & Double-Entry Validation Endpoint
app . get ( '/api/v1/trial-balance' , ( req , res ) = > {
const period = ( req . query . period as string ) || db . activePeriod ;
const branchId = ( req . query . branchId as string ) || 'japan' ;
const branchObj = BRANCHES_LIST . find ( ( b ) = > b . id === branchId ) ;
const branchName = branchObj ? branchObj.name : branchId ;
const sub = db . submissions [ period ] ? . [ branchId as BranchId ] ;
if ( ! sub ) {
return res . status ( 404 ) . json ( { error : ` No submission found for branch ${ branchId } in period ${ period } . ` } ) ;
}
const tbResult = AccountingEngine . generateTrialBalance ( branchId , branchName , period , sub . items ) ;
res . json ( tbResult ) ;
} ) ;
// 6. Maker-Checker Approval Workflow Endpoint
app . post ( '/api/v1/submissions/status' , ( req , res ) = > {
const { period , branchId , status , userEmail , userName , comments } = req . body ;
if ( ! period || ! branchId || ! status ) {
return res . status ( 400 ) . json ( { error : 'Missing period, branchId, or status' } ) ;
}
const validStatuses : MakerCheckerStatus [ ] = [ 'draft' , 'submitted' , 'reviewed' , 'approved' , 'locked' ] ;
if ( ! validStatuses . includes ( status ) ) {
return res . status ( 400 ) . json ( { error : ` Invalid status. Must be one of: ${ validStatuses . join ( ', ' ) } ` } ) ;
}
const sub = db . submissions [ period ] ? . [ branchId as BranchId ] ;
if ( ! sub ) {
return res . status ( 404 ) . json ( { error : ` Submission not found for branch ${ branchId } in period ${ period } . ` } ) ;
}
const prevStatus = sub . status ;
sub . status = status ;
if ( status === 'reviewed' ) {
sub . reviewedBy = userName || userEmail || 'Reviewer' ;
sub . reviewedAt = new Date ( ) . toISOString ( ) ;
} else if ( status === 'approved' ) {
sub . approvedBy = userName || userEmail || 'Approver' ;
sub . approvedAt = new Date ( ) . toISOString ( ) ;
} else if ( status === 'locked' ) {
sub . lockedBy = userName || userEmail || 'Lock Master' ;
sub . lockedAt = new Date ( ) . toISOString ( ) ;
}
// Audit trail
if ( ! db . auditLogs ) db . auditLogs = [ ] ;
db . auditLogs . unshift ( {
id : ` audit- ${ Date . now ( ) } ` ,
timestamp : new Date ( ) . toISOString ( ) ,
userId : userEmail || 'admin@networkbank.com' ,
userName : userName || 'HO Admin' ,
userRole : 'admin' ,
branchId : branchId as BranchId ,
action : ` WORKFLOW_STATUS_CHANGE_ ${ status . toUpperCase ( ) } ` ,
entityType : 'submission' ,
entityId : sub.id ,
details : ` Updated submission status for ${ branchId . toUpperCase ( ) } ( ${ period } ) from ' ${ prevStatus } ' to ' ${ status } '. ${ comments ? ` Notes: ${ comments } ` : '' } ` ,
previousValue : prevStatus ,
newValue : status ,
} ) ;
saveDB ( db ) ;
res . json ( { message : ` Submission workflow status set to ' ${ status } ' successfully. ` , submission : sub } ) ;
} ) ;
// -------------------------------------------------------------
// SMTP & REMINDER ENDPOINTS
// -------------------------------------------------------------
app . get ( '/api/smtp/config' , ( req , res ) = > {
res . json ( { config : db.smtpConfig , logs : db.emailLogs } ) ;
} ) ;
app . post ( '/api/smtp/config' , ( req , res ) = > {
const { host , port , username , fromEmail , useTls , autoRemindersEnabled } = req . body ;
db . smtpConfig = {
host : host || db . smtpConfig . host ,
port : Number ( port ) || db . smtpConfig . port ,
username : username || db . smtpConfig . username ,
fromEmail : fromEmail || db . smtpConfig . fromEmail ,
useTls : useTls !== undefined ? useTls : db.smtpConfig.useTls ,
autoRemindersEnabled : autoRemindersEnabled !== undefined ? autoRemindersEnabled : db.smtpConfig.autoRemindersEnabled ,
reminderFrequencyDays : 7 ,
} ;
saveDB ( db ) ;
res . json ( { config : db.smtpConfig } ) ;
} ) ;
// Send reminders to branches
app . post ( '/api/smtp/send-reminders' , async ( req , res ) = > {
const period = db . activePeriod ;
const submissions = db . submissions [ period ] || { } ;
const submittedBranchIds = Object . keys ( submissions ) ;
const pendingBranches = BRANCHES_LIST . filter ( ( b ) = > ! submittedBranchIds . includes ( b . id ) ) ;
const newLogs : EmailLog [ ] = [ ] ;
for ( const branch of BRANCHES_LIST ) {
const isSubmitted = submittedBranchIds . includes ( branch . id ) ;
const subject = isSubmitted
? ` [CONFIRMED] Balance Sheet Received for ${ branch . name } ( ${ period } ) `
: ` [URGENT REMINDER] Balance Sheet Submission Due for ${ branch . name } ( ${ period } ) ` ;
const body = isSubmitted
? ` Dear ${ branch . name } Team, Thank you. Your Balance Sheet submission for period ${ period } has been received and verified by Head Office. `
: ` Dear ${ branch . name } Team, This is an automated reminder from Head Office Balance Sheet Portal. Please complete and submit your fixed format balance sheet for period ${ period } . Ensure all variances > USD 2 Mn carry explanatory notes. ` ;
const emailLog : EmailLog = {
id : ` log- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 36 ) . substring ( 2 , 6 ) } ` ,
recipientEmail : branch.contactEmail ,
branchId : branch.id ,
subject ,
body ,
sentAt : new Date ( ) . toISOString ( ) ,
status : 'sent' ,
triggerType : 'manual_reminder' ,
} ;
newLogs . push ( emailLog ) ;
}
db . emailLogs = [ . . . newLogs , . . . db . emailLogs ] ;
saveDB ( db ) ;
res . json ( {
message : ` SMTP Reminders dispatched to ${ BRANCHES_LIST . length } branch emails ( ${ pendingBranches . length } pending branches notified). ` ,
pendingCount : pendingBranches.length ,
logs : db.emailLogs ,
} ) ;
} ) ;
// Reset database back to default seed data
app . post ( '/api/reset-seed' , ( req , res ) = > {
if ( fs . existsSync ( dbPath ) ) {
fs . unlinkSync ( dbPath ) ;
}
db = loadDB ( ) ;
res . json ( { message : 'Database reset to initial baseline successfully.' } ) ;
} ) ;
// -------------------------------------------------------------
// VITE / STATIC MIDDLEWARE
// -------------------------------------------------------------
async function startServer() {
if ( process . env . NODE_ENV !== 'production' ) {
const vite = await createViteServer ( {
server : { middlewareMode : true } ,
appType : 'spa' ,
} ) ;
app . use ( vite . middlewares ) ;
} else {
const distPath = path . join ( process . cwd ( ) , 'dist' ) ;
app . use ( express . static ( distPath ) ) ;
app . get ( '*' , ( req , res ) = > {
res . sendFile ( path . join ( distPath , 'index.html' ) ) ;
} ) ;
}
app . listen ( PORT , '0.0.0.0' , ( ) = > {
console . log ( ` Server running on http://0.0.0.0: ${ PORT } ` ) ;
} ) ;
}
startServer ( ) ;