Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
9 changes: 4 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import isEqual from '@/is-equal';

export default function isEqualChecker(obj1, obj2) {
return isEqual(obj1, obj2);
}
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';
62 changes: 62 additions & 0 deletions src/models/bank-account.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
122 changes: 122 additions & 0 deletions src/models/bank.ts
Original file line number Diff line number Diff line change
@@ -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<string, BankAccount> = 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');
}
}
51 changes: 51 additions & 0 deletions src/models/user.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
39 changes: 39 additions & 0 deletions src/services/GlobalRegistry.ts
Original file line number Diff line number Diff line change
@@ -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<string, Bank> = new Map();
const users: Map<string, User> = new Map();
const accounts: Map<string, BankAccount> = 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();
}
}
5 changes: 5 additions & 0 deletions src/services/TransactionService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default class TransactionService {
static create() {
return new TransactionService();
}
}
35 changes: 35 additions & 0 deletions src/stores/Registry.ts
Original file line number Diff line number Diff line change
@@ -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<string, Bank>;
type AccountMap = Map<string, BankAccount>;
type UserMap = Map<string, User>;

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);
}
4 changes: 4 additions & 0 deletions src/utils/id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
let counter = 1;
export function genId(prefix = 'id'): string {
return `${prefix}-${counter++}`;
}
Loading