1from abc import ABC, abstractmethod
2from datetime import datetime
3
4
5class Account(ABC):
6 """Abstract Base Class representing a generic bank account."""
7
8 def __init__(self, account_no: str, holder_name: str, initial_deposit: float = 0.0):
9 if initial_deposit < 0:
10 raise ValueError("Initial deposit cannot be negative.")
11 self.account_no = account_no
12 self.holder_name = holder_name
13 self._balance = float(initial_deposit)
14 self.transactions = []
15 self._record_transaction("Account Opened", initial_deposit)
16
17 @property
18 def balance(self) -> float:
19 """Encapsulated getter for account balance."""
20 return self._balance
21
22 def deposit(self, amount: float) -> bool:
23 """Deposits a positive amount into the account."""
24 if amount <= 0:
25 print(f"[{self.account_no}] Deposit amount must be positive.")
26 return False
27 self._balance += amount
28 self._record_transaction("Deposit", amount)
29 print(f"[{self.account_no}] Deposited ₹{amount:,.2f}. New Balance: ₹{self._balance:,.2f}")
30 return True
31
32 @abstractmethod
33 def withdraw(self, amount: float) -> bool:
34 """Abstract method enforced across all account types."""
35 pass
36
37 @abstractmethod
38 def monthly_maintenance(self):
39 """Executes account-specific end-of-month routines."""
40 pass
41
42 def _record_transaction(self, tx_type: str, amount: float):
43 """Internal helper for logging transaction ledger records."""
44 entry = {
45 "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
46 "type": tx_type,
47 "amount": amount,
48 "resulting_balance": self._balance
49 }
50 self.transactions.append(entry)
51
52 def print_statement(self):
53 """Prints a formatted ledger of all transactions."""
54 print(f"\n===== Account Statement: {self.account_no} ({self.holder_name}) =====")
55 print(f"Account Type: {self.__class__.__name__}")
56 print(f"{'Timestamp':<20} | {'Type':<18} | {'Amount':<12} | {'Balance'}")
57 print("-" * 65)
58 for tx in self.transactions:
59 print(f"{tx['timestamp']:<20} | {tx['type']:<18} | ₹{tx['amount']:<11.2f} | ₹{tx['resulting_balance']:,.2f}")
60 print(f"Current Available Balance: ₹{self._balance:,.2f}\n")
61
62
63class SavingsAccount(Account):
64 """Savings account featuring interest accumulation and minimum balance rules."""
65
66 def __init__(self, account_no: str, holder_name: str, initial_deposit: float,
67 interest_rate: float = 0.04, min_balance: float = 1000.0):
68 super().__init__(account_no, holder_name, initial_deposit)
69 self.interest_rate = interest_rate
70 self.min_balance = min_balance
71
72 def withdraw(self, amount: float) -> bool:
73 if amount <= 0:
74 print(f"[{self.account_no}] Invalid withdrawal amount.")
75 return False
76 if (self._balance - amount) < self.min_balance:
77 print(f"[{self.account_no}] Withdrawal declined: Must maintain minimum balance of ₹{self.min_balance:,.2f}.")
78 return False
79
80 self._balance -= amount
81 self._record_transaction("Withdrawal", amount)
82 print(f"[{self.account_no}] Withdrew ₹{amount:,.2f}. Balance: ₹{self._balance:,.2f}")
83 return True
84
85 def monthly_maintenance(self):
86 """Calculates and deposits monthly accrued interest."""
87 monthly_interest = (self._balance * self.interest_rate) / 12
88 self._balance += monthly_interest
89 self._record_transaction("Interest Credited", monthly_interest)
90 print(f"[{self.account_no}] Interest Credited: ₹{monthly_interest:,.2f}. New Balance: ₹{self._balance:,.2f}")
91
92
93class CheckingAccount(Account):
94 """Checking account with overdraft protection and transaction fees."""
95
96 def __init__(self, account_no: str, holder_name: str, initial_deposit: float,
97 overdraft_limit: float = 5000.0, fee: float = 20.0):
98 super().__init__(account_no, holder_name, initial_deposit)
99 self.overdraft_limit = overdraft_limit
100 self.transaction_fee = fee
101
102 def withdraw(self, amount: float) -> bool:
103 if amount <= 0:
104 print(f"[{self.account_no}] Invalid withdrawal amount.")
105 return False
106
107 total_deduction = amount
108 # Apply fee if overdraft is utilized
109 if (self._balance - amount) < 0:
110 total_deduction += self.transaction_fee
111 print(f"[{self.account_no}] Overdraft used. A fee of ₹{self.transaction_fee:.2f} applies.")
112
113 if (self._balance - total_deduction) < -self.overdraft_limit:
114 print(f"[{self.account_no}] Withdrawal declined: Exceeds overdraft limit of ₹{self.overdraft_limit:,.2f}.")
115 return False
116
117 self._balance -= total_deduction
118 self._record_transaction("Withdrawal (Checking)", total_deduction)
119 print(f"[{self.account_no}] Withdrew ₹{amount:,.2f} (Total debited: ₹{total_deduction:,.2f}). Balance: ₹{self._balance:,.2f}")
120 return True
121
122 def monthly_maintenance(self):
123 print(f"[{self.account_no}] Checking account verified. No maintenance charges this cycle.")
124
125
126class Bank:
127 """Manages an aggregate collection of diverse account types polymorphically."""
128
129 def __init__(self, name: str):
130 self.name = name
131 self.accounts = {}
132
133 def add_account(self, account: Account):
134 self.accounts[account.account_no] = account
135 print(f"Account {account.account_no} registered with {self.name}.")
136
137 def run_monthly_audit(self):
138 """Polymorphic dispatch: Calls monthly_maintenance on each account."""
139 print(f"\n--- Running Monthly Bank Audit for {self.name} ---")
140 for acc in self.accounts.values():
141 acc.monthly_maintenance()