diff --git a/.github/.keep b/.github/.keep new file mode 100644 index 0000000..e69de29 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/__tests__/real.test.ts b/src/__tests__/real.test.ts index 443229c..c958918 100644 --- a/src/__tests__/real.test.ts +++ b/src/__tests__/real.test.ts @@ -1,16 +1,17 @@ -import { describe, beforeEach, it, expect } from 'vitest'; -import { TestFactory } from './helpers/TestFactory'; -import type { TestFixtures } from './helpers/TestFactory'; +import { describe, beforeEach, it, expect } from "vitest"; +import { TestFactory } from "./helpers/TestFactory"; +import type { TestFixtures } from "./helpers/TestFactory"; -describe('Bank Transfer Tests', () => { +describe("Bank Transfer Tests", () => { let fixtures: TestFixtures; beforeEach(() => { fixtures = TestFactory.createFixtures(); }); - it('should allow transfer between accounts', () => { - const { bank, aliceUserId, bobUserId, aliceAccountId, bobAccountId } = fixtures; + it("should allow transfer between accounts", () => { + const { bank, aliceUserId, bobUserId, aliceAccountId, bobAccountId } = + fixtures; // Initial balances const aliceAccount = bank.getAccount(aliceAccountId); @@ -26,19 +27,27 @@ describe('Bank Transfer Tests', () => { expect(bobAccount.getBalance()).toBe(800); }); - - it('should not allow transfer with insufficient funds', () => { + it("should not allow transfer with insufficient funds", () => { const { bank, aliceUserId, bobUserId } = fixtures; expect(() => { bank.send(aliceUserId, bobUserId, 2000); - }).toThrow('Insufficient funds'); + }).toThrow("Insufficient funds"); }); - it('should allow transfer with negative balance', () => { - const { bank, bankAllowsNegative, aliceUserId, bobUserId, aliceAccountAllowsNegativeId, bobAccountId } = fixtures; - - const aliceAccountAllowsNegative = bankAllowsNegative.getAccount(aliceAccountAllowsNegativeId); + it("should allow transfer with negative balance", () => { + const { + bank, + bankAllowsNegative, + aliceUserId, + bobUserId, + aliceAccountAllowsNegativeId, + bobAccountId, + } = fixtures; + + const aliceAccountAllowsNegative = bankAllowsNegative.getAccount( + aliceAccountAllowsNegativeId + ); const bobBankId = bank.getId(); const bobAccount = bank.getAccount(bobAccountId); diff --git a/src/__tests__/simple.test.ts b/src/__tests__/simple.test.ts index 49bb5f6..2d9ad42 100644 --- a/src/__tests__/simple.test.ts +++ b/src/__tests__/simple.test.ts @@ -1,57 +1,59 @@ -import { describe, it, expect } from 'vitest'; -import User from '@/models/user'; -import Bank from '@/models/bank'; -import BankAccount from '@/models/bank-account'; -import TransactionService from '@/services/TransactionService'; - - -describe('Banks', () => { - it('has a bank class', () => { +import { describe, it, expect } from "vitest"; +import User from "@/models/user"; +import Bank from "@/models/bank"; +import BankAccount from "@/models/bank-account"; +import TransactionService from "@/services/TransactionService"; + +describe("Banks", () => { + it("has a bank class", () => { expect(Bank).toBeDefined(); }); - it('can create a bank', () => { + it("can create a bank", () => { const bank = Bank.create(); expect(bank.getId()).toBeDefined(); }); - it('can create a bank account', () => { + it("can create a bank account", () => { const bank = Bank.create(); const bankAccount = bank.createAccount(100); expect(bankAccount.getId()).toBeDefined(); }); }); - -describe('Users', () => { - it('has a user class', () => { +describe("Users", () => { + it("has a user class", () => { expect(User).toBeDefined(); }); - it('can create a user', () => { - const user = User.create('Firstname Lastname', []); + it("can create a user", () => { + const user = User.create("Firstname Lastname", []); expect(user).toBeDefined(); }); - it('can create a user with accounts', () => { + it("can create a user with accounts", () => { const bank = Bank.create(); const bankAccount1Id = bank.createAccount(100).getId(); const bankAccount2Id = bank.createAccount(200).getId(); const bankAccount3Id = bank.createAccount(300).getId(); - const user = User.create('Firstname Lastname', [bankAccount1Id, bankAccount2Id, bankAccount3Id]); + const user = User.create("Firstname Lastname", [ + bankAccount1Id, + bankAccount2Id, + bankAccount3Id, + ]); expect(user).toBeDefined(); }); }); -describe('Accounts', () => { - it('has an account class', () => { +describe("Accounts", () => { + it("has an account class", () => { expect(BankAccount).toBeDefined(); }); }); -describe('Transactions', () => { - it('has a transaction service', () => { +describe("Transactions", () => { + it("has a transaction service", () => { expect(TransactionService).toBeDefined(); }); }); diff --git a/src/helpers/TestFactory.ts b/src/helpers/TestFactory.ts new file mode 100644 index 0000000..b8cc876 --- /dev/null +++ b/src/helpers/TestFactory.ts @@ -0,0 +1,31 @@ +// ...existing imports... + +export class TestFactory { + static createFixtures(): TestFixtures { + GlobalRegistry.clear(); + + // Create users first + const aliceUser = User.create("Alice"); + const bobUser = User.create("Bob"); + + // Create accounts and associate with users + const aliceAccount = bank.createAccount(1000, aliceUser.getId()); + const bobAccount = bank.createAccount(500, bobUser.getId()); + const aliceAccountAllowsNegative = bankAllowsNegative.createAccount( + 200, + aliceUser.getId() + ); + + // ...rest of your code... + + return { + bank, + bankAllowsNegative, + aliceUserId: aliceUser.getId(), + bobUserId: bobUser.getId(), + aliceAccountId: aliceAccount.getId(), + bobAccountId: bobAccount.getId(), + aliceAccountAllowsNegativeId: aliceAccountAllowsNegative.getId(), + }; + } +} diff --git a/src/models/bank-account.ts b/src/models/bank-account.ts new file mode 100644 index 0000000..f193021 --- /dev/null +++ b/src/models/bank-account.ts @@ -0,0 +1,51 @@ +import { BankAccountId } from "@/types/Common"; + +export default class BankAccount { + private id: BankAccountId; + private balance: number; + private isNegativeAllowed: boolean; + + constructor(initialBalance: number, isNegativeAllowed: boolean = false) { + this.id = this.generateId(); + this.balance = initialBalance; + this.isNegativeAllowed = isNegativeAllowed; + } + + private generateId(): BankAccountId { + return `account_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + + getId(): BankAccountId { + return this.id; + } + + getBalance(): number { + return this.balance; + } + + deposit(amount: number): void { + if (amount <= 0) { + throw new Error("Deposit amount must be positive"); + } + this.balance += amount; + } + + withdraw(amount: number): void { + if (amount <= 0) { + throw new Error("Withdrawal amount must be positive"); + } + + if (!this.isNegativeAllowed && this.balance < amount) { + throw new Error("Insufficient funds"); + } + + this.balance -= amount; + } + + setBalance(balance: number): void { + if (!this.isNegativeAllowed && balance < 0) { + throw new Error("Negative balance not allowed"); + } + this.balance = balance; + } +} diff --git a/src/models/bank.ts b/src/models/bank.ts new file mode 100644 index 0000000..3e79c04 --- /dev/null +++ b/src/models/bank.ts @@ -0,0 +1,104 @@ +import { BankAccountId, UserId } from "@/types/Common"; +import BankAccount from "./bank-account"; +import GlobalRegistry from "@/services/GlobalRegistry"; + +interface BankOptions { + isNegativeAllowed?: boolean; +} + +export default class Bank { + private id: string; + private accounts: Map; + private isNegativeAllowed: boolean; + + constructor(options: BankOptions = {}) { + this.id = this.generateId(); + this.accounts = new Map(); + this.isNegativeAllowed = options.isNegativeAllowed || false; + GlobalRegistry.registerBank(this); + } + + private generateId(): string { + return `bank_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + + static create(options?: BankOptions): Bank { + return new Bank(options); + } + + getId(): string { + return this.id; + } + + createAccount(initialBalance: number): BankAccount { + const account = new BankAccount(initialBalance, this.isNegativeAllowed); + this.accounts.set(account.getId(), account); + return account; + } + + getAccount(accountId: BankAccountId): BankAccount { + const account = this.accounts.get(accountId); + if (!account) { + throw new Error(`Account ${accountId} not found`); + } + return account; + } + + hasAccount(accountId: BankAccountId): boolean { + return this.accounts.has(accountId); + } + + send( + fromUserId: UserId, + toUserId: UserId, + amount: number, + toBankId?: string + ): void { + const fromUser = GlobalRegistry.getUser(fromUserId); + const toUser = GlobalRegistry.getUser(toUserId); + + if (!fromUser || !toUser) { + throw new Error("User not found"); + } + + const fromAccountIds = fromUser.getAccountIds(); + const toAccountIds = toUser.getAccountIds(); + + if (fromAccountIds.length === 0 || toAccountIds.length === 0) { + throw new Error("User has no accounts"); + } + + // Find accounts that belong to this bank + const fromAccountId = fromAccountIds.find((id) => this.hasAccount(id)); + const toAccountId = toAccountIds.find((id) => this.hasAccount(id)); + + if (!fromAccountId) { + throw new Error("Sender has no account in this bank"); + } + + const fromAccount = this.getAccount(fromAccountId); + + // Determine target bank and account + let toBank: Bank; + let toAccount: BankAccount; + + if (toBankId && toBankId !== this.id) { + toBank = GlobalRegistry.getBank(toBankId); + const targetAccountId = toAccountIds.find((id) => toBank.hasAccount(id)); + if (!targetAccountId) { + throw new Error("Recipient has no account in target bank"); + } + toAccount = toBank.getAccount(targetAccountId); + } else { + if (!toAccountId) { + throw new Error("Recipient has no account in this bank"); + } + toBank = this; + toAccount = this.getAccount(toAccountId); + } + + // Perform the transfer + fromAccount.withdraw(amount); + toAccount.deposit(amount); + } +} diff --git a/src/models/index.ts b/src/models/index.ts new file mode 100644 index 0000000..4b1c76f --- /dev/null +++ b/src/models/index.ts @@ -0,0 +1,3 @@ +export { default as User } from "./user"; +export { default as Bank } from "./bank"; +export { default as BankAccount } from "./bank-account"; diff --git a/src/models/user.ts b/src/models/user.ts new file mode 100644 index 0000000..46394e1 --- /dev/null +++ b/src/models/user.ts @@ -0,0 +1,45 @@ +import { UserId, BankAccountId } from "@/types/Common"; +import GlobalRegistry from "@/services/GlobalRegistry"; + +export default class User { + private id: UserId; + private name: string; + private accountIds: BankAccountId[]; + + constructor(name: string, accountIds: BankAccountId[]) { + this.id = this.generateId(); + this.name = name; + this.accountIds = accountIds; + GlobalRegistry.registerUser(this); + } + + private generateId(): UserId { + return `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + + static create(name: string, accountIds: BankAccountId[]): User { + return new User(name, accountIds); + } + + getId(): UserId { + return this.id; + } + + getName(): string { + return this.name; + } + + getAccountIds(): BankAccountId[] { + return [...this.accountIds]; + } + + addAccount(accountId: BankAccountId): void { + if (!this.accountIds.includes(accountId)) { + this.accountIds.push(accountId); + } + } + + removeAccount(accountId: BankAccountId): void { + this.accountIds = this.accountIds.filter((id) => id !== accountId); + } +} diff --git a/src/services/GlobalRegistry.ts b/src/services/GlobalRegistry.ts new file mode 100644 index 0000000..ec4c6ec --- /dev/null +++ b/src/services/GlobalRegistry.ts @@ -0,0 +1,33 @@ +import { UserId } from "@/types/Common"; +import User from "@/models/user"; +import Bank from "@/models/bank"; + +export default class GlobalRegistry { + private static users: Map = new Map(); + private static banks: Map = new Map(); + + static registerUser(user: User): void { + this.users.set(user.getId(), user); + } + + static registerBank(bank: Bank): void { + this.banks.set(bank.getId(), bank); + } + + static getUser(userId: UserId): User | undefined { + return this.users.get(userId); + } + + static getBank(bankId: string): Bank { + const bank = this.banks.get(bankId); + if (!bank) { + throw new Error(`Bank ${bankId} not found`); + } + return bank; + } + + static clear(): void { + this.users.clear(); + this.banks.clear(); + } +} diff --git a/src/services/TransactionService.ts b/src/services/TransactionService.ts new file mode 100644 index 0000000..a433f2e --- /dev/null +++ b/src/services/TransactionService.ts @@ -0,0 +1,34 @@ +import { UserId } from "@/types/Common"; +import GlobalRegistry from "./GlobalRegistry"; +import Bank from "@/models/bank"; + +export default class TransactionService { + static transfer( + fromUserId: UserId, + toUserId: UserId, + amount: number, + bankId?: string + ): void { + const fromUser = GlobalRegistry.getUser(fromUserId); + const toUser = GlobalRegistry.getUser(toUserId); + + if (!fromUser || !toUser) { + throw new Error("User not found"); + } + + // For now, we'll use the first bank in the registry + // In a real implementation, you'd want to specify which bank to use + const bank = bankId ? GlobalRegistry.getBank(bankId) : null; + if (!bank) { + throw new Error("No banks available"); + } + + bank.send(fromUserId, toUserId, amount); + } + + static getTransactionHistory(userId: UserId): any[] { + // This would return transaction history for a user + // For now, return empty array + return []; + } +} diff --git a/src/types/common.ts b/src/types/common.ts new file mode 100644 index 0000000..bea7b44 --- /dev/null +++ b/src/types/common.ts @@ -0,0 +1,2 @@ +export type BankAccountId = string; +export type UserId = string;