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
3 changes: 3 additions & 0 deletions src/is-equal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function isEqual(a: unknown, b: unknown): boolean {
return JSON.stringify(a) === JSON.stringify(b);
}
1 change: 1 addition & 0 deletions src/models/BankAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@/models/bank-account';
76 changes: 76 additions & 0 deletions src/models/bank-account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { v4 as uuidv4 } from 'uuid';
import type { BankAccountId, BankId } from '@/types/Common';

interface BankAccountOptions {
isNegativeAllowed?: boolean;
}

export default class BankAccount {
private readonly id: BankAccountId;
private readonly bankId: BankId;
private balance: number;
private readonly isNegativeAllowed: boolean;

private constructor(
id: BankAccountId,
bankId: BankId,
initialBalance: number,
options?: BankAccountOptions
) {
this.id = id;
this.bankId = bankId;
this.balance = initialBalance;
this.isNegativeAllowed = options?.isNegativeAllowed ?? false;
}

static create(bankId: BankId, initialBalance = 0, options?: BankAccountOptions): BankAccount {
return new BankAccount(uuidv4(), bankId, initialBalance, options);
}

getId(): BankAccountId {
return this.id;
}

getBankId(): BankId {
return this.bankId;
}

getBalance(): number {
return this.balance;
}

canDebit(amount: number): boolean {
if (amount < 0) {
return false;
}
if (this.isNegativeAllowed) {
return true;
}
return this.balance >= amount;
}

credit(amount: number): void {
if (amount <= 0) {
throw new Error('Amount should be greater than zero');
}
this.balance += amount;
}

debit(amount: number): void {
if (amount <= 0) {
throw new Error('Amount should be greater than zero');
}
if (!this.canDebit(amount)) {
throw new Error('Insufficient funds');
}
this.balance -= amount;
}

deposit(amount: number): void {
this.credit(amount);
}

withdraw(amount: number): void {
this.debit(amount);
}
}
55 changes: 55 additions & 0 deletions src/models/bank.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { v4 as uuidv4 } from 'uuid';
import BankAccount from '@/models/bank-account';
import type { BankAccountId, BankId, UserId } from '@/types/Common';
import GlobalRegistry from '@/services/GlobalRegistry';
import TransactionService from '@/services/TransactionService';

interface BankOptions {
isNegativeAllowed?: boolean;
}

export default class Bank {
private readonly id: BankId;
private readonly isNegativeAllowed: boolean;
private readonly accountIds: Set<BankAccountId>;

private constructor(id: BankId, options?: BankOptions) {
this.id = id;
this.isNegativeAllowed = options?.isNegativeAllowed ?? false;
this.accountIds = new Set<BankAccountId>();
}

static create(options?: BankOptions): Bank {
const bank = new Bank(uuidv4(), options);
GlobalRegistry.registerBank(bank);
return bank;
}

getId(): BankId {
return this.id;
}

createAccount(initialBalance = 0): BankAccount {
const account = BankAccount.create(this.id, initialBalance, {
isNegativeAllowed: this.isNegativeAllowed
});
this.accountIds.add(account.getId());
GlobalRegistry.registerAccount(account);
return account;
}

getAccount(accountId: BankAccountId): BankAccount {
if (!this.accountIds.has(accountId)) {
throw new Error('Bank account not found');
}
return GlobalRegistry.getAccount(accountId);
}

send(fromUserId: UserId, toUserId: UserId, amount: number, toBankId?: BankId): void {
TransactionService.transfer(this.id, fromUserId, toUserId, amount, toBankId);
}

transfer(fromUserId: UserId, toUserId: UserId, amount: number, toBankId?: BankId): void {
this.send(fromUserId, toUserId, amount, toBankId);
}
}
37 changes: 37 additions & 0 deletions src/models/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { v4 as uuidv4 } from 'uuid';
import type { BankAccountId, UserId } from '@/types/Common';
import GlobalRegistry from '@/services/GlobalRegistry';

export default class User {
private readonly id: UserId;
private readonly name: string;
private readonly accountIds: BankAccountId[];

private constructor(id: UserId, name: string, accountIds: BankAccountId[]) {
this.id = id;
this.name = name;
this.accountIds = accountIds;
}

static create(name: string, accountIds: BankAccountId[]): User {
const user = new User(uuidv4(), name, [...accountIds]);
GlobalRegistry.registerUser(user);
return user;
}

getId(): UserId {
return this.id;
}

getName(): string {
return this.name;
}

getAccountIds(): BankAccountId[] {
return [...this.accountIds];
}

getAccounts(): BankAccountId[] {
return this.getAccountIds();
}
}
52 changes: 52 additions & 0 deletions src/services/GlobalRegistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type Bank from '@/models/bank';
import type BankAccount from '@/models/bank-account';
import type User from '@/models/user';
import type { BankAccountId, BankId, UserId } from '@/types/Common';

export default class GlobalRegistry {
private static banks = new Map<BankId, Bank>();
private static users = new Map<UserId, User>();
private static accounts = new Map<BankAccountId, BankAccount>();

static registerBank(bank: Bank): void {
this.banks.set(bank.getId(), bank);
}

static registerUser(user: User): void {
this.users.set(user.getId(), user);
}

static registerAccount(account: BankAccount): void {
this.accounts.set(account.getId(), account);
}

static getBank(bankId: BankId): Bank {
const bank = this.banks.get(bankId);
if (!bank) {
throw new Error('Bank not found');
}
return bank;
}

static getUser(userId: UserId): User {
const user = this.users.get(userId);
if (!user) {
throw new Error('User not found');
}
return user;
}

static getAccount(accountId: BankAccountId): BankAccount {
const account = this.accounts.get(accountId);
if (!account) {
throw new Error('Bank account not found');
}
return account;
}

static clear(): void {
this.banks.clear();
this.users.clear();
this.accounts.clear();
}
}
119 changes: 119 additions & 0 deletions src/services/TransactionService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import type BankAccount from '@/models/bank-account';
import type { BankId, UserId } from '@/types/Common';
import GlobalRegistry from '@/services/GlobalRegistry';

export default class TransactionService {
static transfer(
fromBankId: BankId,
fromUserId: UserId,
toUserId: UserId,
amount: number,
toBankId?: BankId
): void {
if (amount <= 0) {
throw new Error('Amount should be greater than zero');
}

const sourceBankId = fromBankId;
const targetBankId = toBankId ?? fromBankId;
const isSelfTransfer = fromUserId === toUserId;

const fromUser = GlobalRegistry.getUser(fromUserId);
const toUser = GlobalRegistry.getUser(toUserId);

// Validate banks exist before continuing transfer.
GlobalRegistry.getBank(sourceBankId);
GlobalRegistry.getBank(targetBankId);

const sourceAccounts = this.pickSourceAccounts(fromUser.getAccountIds(), sourceBankId);
const targetAccount = this.pickTargetAccount(
toUser.getAccountIds(),
targetBankId,
isSelfTransfer ? sourceAccounts[0]?.getId() : undefined
);

this.debitAcrossAccounts(sourceAccounts, amount);
targetAccount.credit(amount);
}

static send(
fromBankId: BankId,
fromUserId: UserId,
toUserId: UserId,
amount: number,
toBankId?: BankId
): void {
this.transfer(fromBankId, fromUserId, toUserId, amount, toBankId);
}

private static pickSourceAccounts(accountIds: string[], sourceBankId: BankId): BankAccount[] {
const sourceAccounts = accountIds
.map((accountId) => GlobalRegistry.getAccount(accountId))
.filter((account) => account.getBankId() === sourceBankId);

if (sourceAccounts.length === 0) {
throw new Error('Insufficient funds');
}

return sourceAccounts;
}

private static debitAcrossAccounts(sourceAccounts: BankAccount[], amount: number): void {
let remaining = amount;

for (const account of sourceAccounts) {
if (remaining <= 0) {
break;
}

if (account.canDebit(remaining)) {
remaining = 0;
continue;
}

const available = account.getBalance();
if (available > 0) {
remaining -= Math.min(available, remaining);
}
}

if (remaining > 0) {
throw new Error('Insufficient funds');
}

remaining = amount;
for (const account of sourceAccounts) {
if (remaining <= 0) {
break;
}

if (account.canDebit(remaining)) {
account.debit(remaining);
remaining = 0;
break;
}

const available = account.getBalance();
if (available > 0) {
const debitAmount = Math.min(available, remaining);
account.debit(debitAmount);
remaining -= debitAmount;
}
}
}

private static pickTargetAccount(
accountIds: string[],
targetBankId: BankId,
excludedAccountId?: string
): BankAccount {
for (const accountId of accountIds) {
const account = GlobalRegistry.getAccount(accountId);
if (account.getBankId() === targetBankId && account.getId() !== excludedAccountId) {
return account;
}
}

throw new Error('Target account not found');
}
}
1 change: 1 addition & 0 deletions src/services/global-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@/services/GlobalRegistry';
1 change: 1 addition & 0 deletions src/services/transaction-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@/services/TransactionService';
3 changes: 3 additions & 0 deletions src/types/Common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type BankId = string;
export type BankAccountId = string;
export type UserId = string;
Loading