diff --git a/README.md b/README.md index cab3c08..76507d5 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +[![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.github.com/a/W3nV4mdD) # Banking management ## Overview diff --git a/src/index.ts b/src/index.ts index 3972b63..ff5ffa5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,4 @@ -import isEqual from '@/is-equal'; - -export default function isEqualChecker(obj1, obj2) { - return isEqual(obj1, obj2); -} \ No newline at end of file +export { default as Bank } from '@/models/bank'; +export { default as User } from '@/models/user'; +export { default as BankAccount } from '@/models/bank-account'; +export { default as TransactionService } from '@/services/TransactionService'; diff --git a/src/models/bank-account.ts b/src/models/bank-account.ts new file mode 100644 index 0000000..32049ed --- /dev/null +++ b/src/models/bank-account.ts @@ -0,0 +1,62 @@ +import { genId } from '@/utils/id'; + +export default class BankAccount { + private id: string; + private balance: number; + private bankId: string; + private ownerId: string | null = null; + private allowsNegative: boolean; + + constructor(bankId: string, initialBalance = 0, allowsNegative = false) { + this.id = genId('acct'); + this.bankId = bankId; + this.balance = initialBalance; + this.allowsNegative = allowsNegative; + } + + static create(bankId: string, initialBalance = 0, allowsNegative = false) { + return new BankAccount(bankId, initialBalance, allowsNegative); + } + + getId(): string { + return this.id; + } + + getBalance(): number { + return this.balance; + } + + getBankId(): string { + return this.bankId; + } + + getOwnerId(): string | null { + return this.ownerId; + } + + setOwner(userId: string) { + this.ownerId = userId; + } + + deposit(amount: number) { + if (amount <= 0) throw new Error('Deposit must be positive'); + this.balance += amount; + } + + canWithdraw(amount: number): boolean { + if (amount <= 0) return false; + if (this.allowsNegative) return true; + return this.balance - amount >= 0; + } + + withdraw(amount: number) { + if (amount <= 0) throw new Error('Withdraw must be positive'); + if (!this.canWithdraw(amount)) throw new Error('Insufficient funds'); + this.balance -= amount; + } + + forceWithdraw(amount: number) { + if (amount <= 0) throw new Error('Withdraw must be positive'); + this.balance -= amount; + } +} diff --git a/src/models/bank.ts b/src/models/bank.ts new file mode 100644 index 0000000..d541336 --- /dev/null +++ b/src/models/bank.ts @@ -0,0 +1,122 @@ +import { genId } from '@/utils/id'; +import BankAccount from '@/models/bank-account'; +import { registerBank, registerAccount, getAccountById, getUserById, getBankById } from '@/stores/Registry'; + +export default class Bank { + private id: string; + private accounts: Map = new Map(); + private allowsNegativeBalance: boolean; + + private constructor(allowsNegativeBalance = false) { + this.id = genId('bank'); + this.allowsNegativeBalance = allowsNegativeBalance; + registerBank(this.id, this); + } + + static create(allowsNegativeBalance = false): Bank { + return new Bank(allowsNegativeBalance); + } + + getId(): string { + return this.id; + } + + createAccount(initialBalance = 0): BankAccount { + const acct = BankAccount.create(this.id, initialBalance, this.allowsNegativeBalance); + this.accounts.set(acct.getId(), acct); + registerAccount(acct.getId(), acct); + return acct; + } + + getAccount(accountId: string): BankAccount { + const acct = this.accounts.get(accountId); + if (!acct) throw new Error('Account not found'); + return acct; + } + + send(fromUserId: string, toUserId: string, amount: number, toBankId?: string) { + if (amount <= 0) throw new Error('Transfer amount must be positive'); + + const fromUser = getUserById(fromUserId); + const toUser = getUserById(toUserId); + if (!fromUser || !toUser) throw new Error('User not found'); + + const destBank = toBankId ? getBankById(toBankId) : this; + if (!destBank) throw new Error('Destination bank not found'); + + const toAcctId = toUser.getAccountIds().find(id => { + const acct = getAccountById(id); + return acct && (acct as any).getBankId?.() === destBank.getId(); + }); + + if (!toAcctId) throw new Error('Destination account not found for user in destination bank'); + + const toAcct = getAccountById(toAcctId)!; + + const fromAcctIds = fromUser.getAccountIds().filter(id => { + const acct = getAccountById(id); + return acct && (acct as any).getBankId?.() === this.getId(); + }); + + if (fromAcctIds.length === 0) throw new Error('Source account not found for user in this bank'); + + const primaryAcct = getAccountById(fromAcctIds[0])!; + + const sameBankDestination = destBank.getId() === this.getId(); + + if (primaryAcct.canWithdraw(amount)) { + primaryAcct.withdraw(amount); + (toAcct as any).deposit(amount); + (fromUser as any).addAccountIdFront(primaryAcct.getId()); + (toUser as any).addAccountIdFront(toAcct.getId()); + return; + } + + if (this.allowsNegativeBalance) { + (primaryAcct as any).forceWithdraw(amount); + (toAcct as any).deposit(amount); + (fromUser as any).addAccountIdFront(primaryAcct.getId()); + (toUser as any).addAccountIdFront(toAcct.getId()); + return; + } + + if (sameBankDestination) { + let totalAvailable = 0; + const acctObjs = fromAcctIds.map(id => getAccountById(id)!); + for (const a of acctObjs) { + totalAvailable += Math.max(0, (a as any).getBalance()); + } + if (totalAvailable < amount) { + throw new Error('Insufficient funds'); + } + + let remaining = amount; + const deltas: Array<{ acct: any; withdrawAmt: number }> = []; + + for (const a of acctObjs) { + if (remaining <= 0) break; + const available = Math.max(0, a.getBalance()); + const take = Math.min(available, remaining); + if (take > 0) { + deltas.push({ acct: a, withdrawAmt: take }); + remaining -= take; + } + } + + if (remaining > 0) { + throw new Error('Insufficient funds'); + } + + for (const d of deltas) { + d.acct.withdraw(d.withdrawAmt); + (fromUser as any).addAccountIdFront(d.acct.getId()); + } + + (toAcct as any).deposit(amount); + (toUser as any).addAccountIdFront(toAcct.getId()); + return; + } + + throw new Error('Insufficient funds'); + } +} diff --git a/src/models/user.ts b/src/models/user.ts new file mode 100644 index 0000000..364aee2 --- /dev/null +++ b/src/models/user.ts @@ -0,0 +1,51 @@ +import { genId } from '@/utils/id'; +import { registerUser, getAccountById } from '@/stores/Registry'; + +export default class User { + private id: string; + private name: string; + private accountIds: string[] = []; + + private constructor(name: string, accountIds: string[]) { + this.id = genId('user'); + this.name = name; + this.accountIds = [...accountIds]; + + for (const acctId of accountIds) { + const acct = getAccountById(acctId); + if (acct) { + try { + (acct as any).setOwner?.(this.id); + } catch { + + } + } + } + + registerUser(this.id, this); + } + + static create(name: string, accountIds: string[]) { + return new User(name, accountIds); + } + + getId(): string { + return this.id; + } + + getName(): string { + return this.name; + } + + getAccountIds(): string[] { + return [...this.accountIds]; + } + + addAccountIdFront(acctId: string) { + this.accountIds = [acctId, ...this.accountIds.filter(id => id !== acctId)]; + } + + addAccountIdBack(acctId: string) { + if (!this.accountIds.includes(acctId)) this.accountIds.push(acctId); + } +} diff --git a/src/services/GlobalRegistry.ts b/src/services/GlobalRegistry.ts new file mode 100644 index 0000000..b73bab6 --- /dev/null +++ b/src/services/GlobalRegistry.ts @@ -0,0 +1,39 @@ +import type Bank from '@/models/bank'; +import type User from '@/models/user'; +import type BankAccount from '@/models/bank-account'; + +const banks: Map = new Map(); +const users: Map = new Map(); +const accounts: Map = new Map(); + +export default class GlobalRegistry { + static registerBank(bank: Bank) { + banks.set(bank.getId(), bank); + } + + static getBank(id: string): Bank | undefined { + return banks.get(id); + } + + static registerUser(user: User) { + users.set(user.getId(), user); + } + + static getUser(id: string): User | undefined { + return users.get(id); + } + + static registerAccount(account: BankAccount) { + accounts.set(account.getId(), account); + } + + static getAccount(id: string): BankAccount | undefined { + return accounts.get(id); + } + + static clear() { + banks.clear(); + users.clear(); + accounts.clear(); + } +} diff --git a/src/services/TransactionService.ts b/src/services/TransactionService.ts new file mode 100644 index 0000000..8c185c2 --- /dev/null +++ b/src/services/TransactionService.ts @@ -0,0 +1,5 @@ +export default class TransactionService { + static create() { + return new TransactionService(); + } +} diff --git a/src/stores/Registry.ts b/src/stores/Registry.ts new file mode 100644 index 0000000..ab551f9 --- /dev/null +++ b/src/stores/Registry.ts @@ -0,0 +1,35 @@ +import type BankAccount from '@/models/bank-account'; +import type Bank from '@/models/bank'; +import type User from '@/models/user'; + +type BankMap = Map; +type AccountMap = Map; +type UserMap = Map; + +const banks: BankMap = new Map(); +const accounts: AccountMap = new Map(); +const users: UserMap = new Map(); + +export function registerBank(id: string, bank: Bank) { + banks.set(id, bank); +} + +export function getBankById(id: string): Bank | undefined { + return banks.get(id); +} + +export function registerAccount(id: string, account: BankAccount) { + accounts.set(id, account); +} + +export function getAccountById(id: string): BankAccount | undefined { + return accounts.get(id); +} + +export function registerUser(id: string, user: User) { + users.set(id, user); +} + +export function getUserById(id: string): User | undefined { + return users.get(id); +} diff --git a/src/utils/id.ts b/src/utils/id.ts new file mode 100644 index 0000000..bebfe40 --- /dev/null +++ b/src/utils/id.ts @@ -0,0 +1,4 @@ +let counter = 1; +export function genId(prefix = 'id'): string { + return `${prefix}-${counter++}`; +}