Your task is to implement an in-memory banking system. This problem consists of 4 levels that progressively add complexity:
Level 1: Basic account operations (create, deposit, transfer)
Level 2: Ranking accounts by spending activity
Level 3: Scheduled payments with cashback and payment status tracking
Level 4: Account merging and historical balance queries
Subsequent levels are unlocked when the current level is correctly solved. You always have access to the data for the current and all previous levels.
Initially, the banking system does not contain any accounts. Implement operations to allow account creation, deposits, and transfers between different accounts.
create_account
create_account(self, timestamp: int, account_id: str) -> bool
Creates a new account with the given identifier if it doesn't already exist
Returns True if the account was successfully created
Returns False if an account with that id already exists
deposit
deposit(self, timestamp: int, account_id: str, amount: int) -> int | None
Deposits the given amount of money to the specified account id
Returns the balance of the account after the operation has been processed
Returns None if the specified account doesn't exist
The operation is processed before the result is returned
transfer
transfer(self, timestamp: int, source_account_id: str, target_account_id: str, amount: int) -> int | None
Transfers the given amount of money from source_account_id to target_account_id
Returns the balance of source_account_id if the transfer was successful
Returns None otherwise
Returns None if:
source_account_id or target_account_id doesn't exist
source_account_id and target_account_id are the same
Account source_account_id has insufficient funds to perform the transfer
The bank wants to identify people who are not keeping money in their accounts. Implement operations to support ranking accounts based on outgoing transactions.
top_spenders
top_spenders(self, timestamp: int, n: int) -> list[str]
Returns the identifiers of the top n accounts with the highest outgoing transactions
Outgoing transactions = total amount of money either:
Transferred out, OR
Paid/withdrawn (the pay operation will be introduced in Level 3)
Results are sorted in descending order by total outgoing amount
In case of a tie, sort alphabetically by account_id in ascending order
Format:
["account_id_1(total_outgoing_1)", "account_id_2(total_outgoing_2)", ..., "account_id_n(total_outgoing_n)"]
Notes:
If less than n accounts exist in the system, return all their identifiers in the described format
Cashback (an operation introduced in Level 3) should not be reflected in calculations for total outgoing transactions
The banking system should allow scheduling payments with cashback and checking the status of scheduled payments.
pay
pay(self, timestamp: int, account_id: str, amount: int) -> str | None
Withdraws the given amount of money from the specified account
All withdrawal transactions provide a 2% cashback
The cashback amount (rounded down to the nearest integer) will be refunded to the account 24 hours after the withdrawal
Returns a unique identifier for the payment transaction (e.g., "payment1", "payment2", etc.)
Returns None if:
account_id doesn't exist
account_id has insufficient funds to perform the payment
Additional Conditions:
top_spenders should now also account for the total amount of money withdrawn from accounts
The waiting period for cashback is 24 hours = 24 × 60 × 60 × 1000 = 86400000 milliseconds (the unit for timestamps)
Cashback will be processed at timestamp + 86400000
If the refund is successful (i.e., the account hasn't been removed from the system), the cashback is applied
get_payment_status
get_payment_status(self, timestamp: int, account_id: str, payment: str) -> str | None
Returns the status of the payment transaction for the given payment
Returns None if:
account_id doesn't exist
The given payment doesn't exist for the specified account
The payment transaction was for an account with a different identifier from account_id
Returns:
"IN_PROGRESS" - if cashback has not been received yet
"CASHBACK_RECEIVED" - if cashback has been refunded to the account
The banking system should support merging two accounts while retaining both accounts' balance and transaction histories.
merge_accounts
merge_accounts(self, timestamp: int, account_id_1: str, account_id_2: str) -> bool
Merges account_id_2 into account_id_1
Returns True if accounts were successfully merged
Returns False otherwise
Returns False if:
account_id_1 is equal to account_id_2
account_id_1 or account_id_2 doesn't exist
Merge Behavior:
All pending cashback refunds for account_id_2 should still be processed, but refunded to account_id_1 instead
After the merge, it must be possible to check the status of payment transactions in account_id_2 with payment identifiers by replacing account_id_2 with account_id_1
The balance of account_id_2 should be added to the balance for account_id_1
top_spenders operations should recognize merged accounts — the total outgoing transactions for merged accounts should be the sum of all money transferred and/or withdrawn in both accounts
account_id_2 should be removed from the system after the merge
get_balance
get_balance(self, timestamp: int, account_id: str, time_at: int) -> int | None
Returns the total amount of money in the account account_id at the given timestamp time_at
If the specified account did not exist at a given time time_at, returns None
Important Notes:
If queries have been processed at timestamp time_at, get_balance must reflect the account balance after the query has been processed
If the account was merged into another account, the merged account should inherit its balance history
If account_id_2 is merged into account_id_1, then account_id_2 is removed from the system
After removal, it is possible to create a new account with account_id_2 using create_account
Historical balances and outgoing transaction totals before the merge are preserved in account_id_1
The new account_id_2 account starts with a balance of 0 from the new creation timestamp
If an account was merged at timestamp T, queries with time_at < T should return the account's historical balance
Queries with time_at >= T for a merged account should return None (unless the account was recreated)
All timestamps are given in milliseconds
Timestamps are guaranteed to be unique and in strictly increasing order
Account IDs are valid string identifiers
All amounts are positive integers
✦ AI-Generated Solution · Coding (OA) · Comprehensive Full level-by-level reference implementation, including the hard parts: lazy 24h cashback processing, account merging with redirected refunds, and historical balance queries. Verified with an end-to-end assertion suite covering all four levels.
The defining difficulty is time. Timestamps arrive strictly increasing, but cashback lands in the future (timestamp + 86_400_000). You cannot eagerly schedule a callback, so use the standard trick: keep a list of pending refunds and, at the start of every operation, lazily apply all pending refunds whose execution time is <= current timestamp. Because timestamps are monotonic, this is correct and amortized-cheap.
State to carry across all levels:
accounts: id -> balance, outgoing: id -> total outgoing (transfers out + payments).history: id -> [(timestamp, balance), ...] to answer Level-4 get_balance(time_at) via binary search. A merged/removed account gets a terminal (merge_ts, None) marker so queries at/after the merge return None but earlier history stays queryable.pending: [(exec_time, account, cashback, payment_id)] and a payments registry for status.import bisect
class BankingSystem:
def __init__(self):
self.accounts = {} # id -> balance
self.outgoing = {} # id -> total outgoing
self.history = {} # id -> [(ts, balance)] (balance None = removed)
self.payments = {} # payment_id -> {account, exec_time, status_time}
self.pending = [] # [(exec_time, account, cashback, payment_id)]
self.pay_counter = 0
CASHBACK_DELAY = 86_400_000
# apply every refund whose time has arrived (monotonic timestamps -> safe)
def _process_cashback(self, ts):
self.pending.sort()
keep = []
for exec_time, acc, amount, pid in self.pending:
if exec_time <= ts:
if acc in self.accounts: # skip if account removed
self.accounts[acc] += amount
self._record(acc, exec_time)
self.payments[pid]["status_time"] = exec_time
else:
keep.append((exec_time, acc, amount, pid))
self.pending = keep
def _record(self, acc, ts):
h = self.history.setdefault(acc, [])
if h and h[-1][0] == ts:
h[-1] = (ts, self.accounts[acc])
else:
h.append((ts, self.accounts[acc]))
# ---------- Level 1 ----------
def create_account(self, ts, account_id):
self._process_cashback(ts)
if account_id in self.accounts:
return False
self.accounts[account_id] = 0
self.outgoing[account_id] = 0
self.history[account_id] = [(ts, 0)]
return True
def deposit(self, ts, account_id, amount):
self._process_cashback(ts)
if account_id not in self.accounts:
return None
self.accounts[account_id] += amount
self._record(account_id, ts)
return self.accounts[account_id]
def transfer(self, ts, source, target, amount):
self._process_cashback(ts)
if source not in self.accounts or target not in self.accounts:
return None
if source == target or self.accounts[source] < amount:
return None
self.accounts[source] -= amount
self.accounts[target] += amount
self.outgoing[source] += amount
self._record(source, ts); self._record(target, ts)
return self.accounts[source]
# ---------- Level 2 ----------
def top_spenders(self, ts, n):
self._process_cashback(ts)
ranked = sorted(self.outgoing.items(), key=lambda x: (-x[1], x[0]))
return [f"{acc}({amt})" for acc, amt in ranked[:n]]
# ---------- Level 3 ----------
def pay(self, ts, account_id, amount):
self._process_cashback(ts)
if account_id not in self.accounts or self.accounts[account_id] < amount:
return None
self.accounts[account_id] -= amount
self.outgoing[account_id] += amount
self._record(account_id, ts)
self.pay_counter += 1
pid = f"payment{self.pay_counter}"
cashback = (amount * 2) // 100 # 2%, floored
exec_time = ts + self.CASHBACK_DELAY
self.payments[pid] = {"account": account_id, "exec_time": exec_time, "status_time": None}
self.pending.append((exec_time, account_id, cashback, pid))
return pid
def get_payment_status(self, ts, account_id, payment):
self._process_cashback(ts)
if account_id not in self.accounts:
return None
p = self.payments.get(payment)
if p is None or p["account"] != account_id:
return None
if p["status_time"] is not None and p["status_time"] <= ts:
return "CASHBACK_RECEIVED"
return "IN_PROGRESS"
# ---------- Level 4 ----------
def merge_accounts(self, ts, acc1, acc2):
self._process_cashback(ts)
if acc1 == acc2 or acc1 not in self.accounts or acc2 not in self.accounts:
return False
self.accounts[acc1] += self.accounts[acc2]
self.outgoing[acc1] += self.outgoing[acc2]
# pending cashbacks for acc2 now pay out to acc1
self.pending = [(t, acc1 if a == acc2 else a, amt, pid)
for (t, a, amt, pid) in self.pending]
for p in self.payments.values():
if p["account"] == acc2:
p["account"] = acc1
self._record(acc1, ts)
self.history[acc2].append((ts, None)) # terminal marker
del self.accounts[acc2]; del self.outgoing[acc2]
return True
def get_balance(self, ts, account_id, time_at):
self._process_cashback(ts)
h = self.history.get(account_id)
if not h:
return None
idx = bisect.bisect_right([t for t, _ in h], time_at) - 1
if idx < 0:
return None
_, bal = h[idx]
return bal # None if removed at/before time_at
get_payment_status. A top_spenders or get_balance call at a later timestamp must already reflect refunds that became due.amount * 2 // 100), and it is excluded from outgoing — only money leaving the account (transfers out + payments) counts toward top_spenders.get_balance is historical: binary-search the balance snapshots for the latest record <= time_at. After a merge, the absorbed account returns its pre-merge history for time_at < merge_ts and None afterward (until/unless recreated, which starts fresh at 0).top_spenders tie-break is alphabetical ascending on account_id; format each entry as "id(total_outgoing)".create/deposit/transfer/pay/get_payment_status: O(P log P) worst case from the pending-refund sweep (P = pending refunds), amortized near O(1) since each refund is processed once.top_spenders: O(A log A) in number of accounts.get_balance: O(H) to build the key list (or O(log H) if you keep a parallel sorted key array).An end-to-end assertion suite exercises account creation/duplication, deposits to missing accounts, self-transfer and insufficient-funds guards, top_spenders ranking and ties, a pay with a 24h-delayed 2% cashback, status transition IN_PROGRESS -> CASHBACK_RECEIVED, historical get_balance before vs. after the cashback, an account merge with redirected payment status, and recreation of a merged id. All assertions pass:
Banking: ALL ASSERTIONS PASS