Загрузка данных
import datetime as _dt
import itertools as _it
from dataclasses import dataclass, field
from enum import Enum
# =============================== КОНСТАНТЫ ===============================
DEFAULT_CARD_INFO_FIELDS = [
"card_id",
"user_id",
"phone",
"bank_name",
"bank_bic",
"acc_id",
"pan",
"payment_system",
"currency",
"status",
"issue_date",
"expiry_date",
"balance",
"cashback_balance",
"user_cards",
]
DEFAULT_ACCOUNT_BALANCE = 0.00
DEFAULT_CASHBACK_BALANCE = 0.00
CARD_CURRENCY = "RUB"
DEFAULT_PAYMENT_SYSTEM = "MIR"
ACCOUNT_TYPE_CODE = "40817" # тип счета для физлиц
ACCOUNT_BRANCH = "0000" # отсутствие филиалов у банка
ACCOUNT_CURRENCY = "810" # идентификатор для рублёвых операций
EMPTY_PAN = "0000000000000000"
BIN_BY_SYSTEM = {
"MIR": "220400",
"VISA": "400000",
"MASTERCARD": "510000",
}
DEFAULT_CASHBACK_TRANSACTION = 0.00
TRANSACTION_HISTORY_HEADER = [
"timestamp,type,from_card,to_card,amount,mcc,cashback,description"
]
DEPOSIT_DESCRIPTION = "{amount:.2f}₽ → карта #{card_id}"
TRANSFER_DESCRIPTION = "{amount:.2f}₽: карта #{from_card} → карта #{to_card}"
PAY_DESCRIPTION = "{amount:.2f}₽ (MCC: {mcc}) с карты #{card_id}"
BALANCE_DESCRIPTION = "Баланс: {balance:.2f}₽"
# =============================== ГЕНЕРАТОРЫ ДАННЫХ ===============================
ISSUE_DATE_START = _dt.date(2022, 1, 1)
ISSUE_DATE_GENERATOR = (ISSUE_DATE_START + _dt.timedelta(days=i) for i in _it.count())
EXPIRY_YEARS = 4
TIMESTAMP_START = _dt.datetime(2022, 1, 1, 9, 0, 0)
def timestamp_generator():
for i in _it.count():
base_date = TIMESTAMP_START + _dt.timedelta(days=i)
hour = 9 + (i * 3) % 10 # цикличное смещение часа
minute = (i * 7) % 60 # цикличное смещение минут
second = (i * 11) % 60 # цикличное смещение секунд
yield base_date.replace(hour=hour % 24, minute=minute, second=second)
TIMESTAMP_GENERATOR = timestamp_generator()
def next_timestamp_after(issue_date: _dt.date) -> _dt.datetime:
"""
Возвращает ближайший timestamp из генератора, который позже issue_date.
"""
while True:
ts = next(TIMESTAMP_GENERATOR)
if ts.date() > issue_date:
return ts
# =============================== ENUM'Ы ===============================
class CardStatus(Enum):
ACTIVE = "Active"
CLOSED = "Closed"
BLOCKED = "Blocked"
CARD_STATUS = CardStatus.ACTIVE
class TransactionType(Enum):
DEPOSIT = "deposit"
TRANSFER = "transfer"
PAY = "pay"
INTEREST = "interest"
# ============================== ОСНОВНЫЕ КЛАССЫ ===============================
@dataclass
class Transaction:
from_card: int | None
to_card: int | None
amount: float
type: TransactionType
mcc: str | None
description: str
timestamp: _dt.datetime
cashback: float = DEFAULT_CASHBACK_TRANSACTION
@dataclass
class User:
last_name: str
first_name: str
pin: str
phone: str
user_id: int
accounts: list = field(default_factory=list)
cards: list = field(default_factory=list)
def change_pin(self, old_pin: str, new_pin: str):
if self.pin == old_pin:
self.pin = new_pin
@dataclass
class Account:
owner: "User"
acc_id: str
balance: float = DEFAULT_ACCOUNT_BALANCE
cashback_balance: float = DEFAULT_CASHBACK_BALANCE
class Card:
def __init__(
self,
account,
card_id,
payment_system=DEFAULT_PAYMENT_SYSTEM,
pan=EMPTY_PAN,
issue_date=None,
expiry_date=None,
currency=CARD_CURRENCY,
status=CARD_STATUS,
bank=None,
):
self.account = account
self.card_id = card_id
self.payment_system = payment_system
self.pan = pan
self.issue_date = issue_date
self.expiry_date = expiry_date
self.currency = currency
self.status = status
self.bank = bank
# Обновляем дату заведения и срок окончания карты
if self.issue_date is None:
self.issue_date = next(ISSUE_DATE_GENERATOR)
if self.expiry_date is None and self.issue_date is not None:
self.expiry_date = _dt.date(
self.issue_date.year + EXPIRY_YEARS,
self.issue_date.month,
self.issue_date.day,
)
def get_card_info(self, fields: list = None):
user = self.account.owner
data = {
"bank_name": f"Банк: {self.bank.name}",
"bank_bic": f"БИК банка: {self.bank.bic}",
"card_id": f"Карта #{self.card_id}",
"user_id": f"Пользователь: {user.user_id} — {user.last_name} {user.first_name}",
"phone": f"Телефон: {user.phone}",
"pan": f"PAN: {self.pan}",
"acc_id": f"Счёт: {self.account.acc_id}",
"payment_system": f"Плат. система: {self.payment_system}",
"currency": f"Валюта: {self.currency}",
"status": f"Статус: {self.status.value}",
"issue_date": f"Выпуск: {self.issue_date}",
"expiry_date": f"Срок: {self.expiry_date}",
"user_cards": f"Карты пользователя: {[c.pan for c in user.cards]}",
"cashback_balance": f"Кешбэк: {self.account.cashback_balance:.2f}₽",
"balance": f"Баланс: {self.account.balance:.2f}₽",
}
if fields is None:
fields = DEFAULT_CARD_INFO_FIELDS
return (
"\n".join([data[field] for field in fields if field in data])
+ "\n"
+ "-" * 50
)
def __repr__(self):
return (
f"Card(card_id={self.card_id}, pan={self.pan}, account={self.account}, "
f"status={self.status}, issue_date={self.issue_date}, expiry_date={self.expiry_date})"
)
def get_balance(self):
balance = self.account.balance
return BALANCE_DESCRIPTION.format(balance=balance)
def deposit(self, amount: float):
self.account.balance += amount
self.bank.transaction_log.append(
Transaction(
from_card=None,
to_card=self.card_id,
amount=amount,
type=TransactionType.DEPOSIT,
mcc=None,
cashback=DEFAULT_CASHBACK_TRANSACTION,
description=DEPOSIT_DESCRIPTION.format(
amount=amount, card_id=self.card_id
),
timestamp=next_timestamp_after(self.issue_date),
)
)
def transfer(self, to_card, amount: float):
self.account.balance -= amount
to_card.account.balance += amount
latest_issue = max(self.issue_date, to_card.issue_date)
self.bank.transaction_log.append(
Transaction(
from_card=self.card_id,
to_card=to_card.card_id,
amount=amount,
type=TransactionType.TRANSFER,
mcc=None,
cashback=DEFAULT_CASHBACK_TRANSACTION,
description=TRANSFER_DESCRIPTION.format(
amount=amount, from_card=self.card_id, to_card=to_card.card_id
),
timestamp=next_timestamp_after(latest_issue),
)
)
def pay(self, amount: float, mcc: str):
self.account.balance -= amount
self.bank.transaction_log.append(
Transaction(
from_card=self.card_id,
to_card=None,
amount=amount,
type=TransactionType.PAY,
mcc=mcc,
cashback=DEFAULT_CASHBACK_TRANSACTION,
description=PAY_DESCRIPTION.format(
amount=amount, mcc=mcc, card_id=self.card_id
),
timestamp=next_timestamp_after(self.issue_date),
)
)
def get_transaction_history(self):
rows = list(TRANSACTION_HISTORY_HEADER)
sorted_transactions = sorted(
self.bank.transaction_log, key=lambda x: x.timestamp
)
for tx in sorted_transactions:
if tx.from_card == self.card_id or tx.to_card == self.card_id:
dt_str = tx.timestamp.strftime("%Y-%m-%d %H:%M:%S")
from_card = tx.from_card if tx.from_card is not None else ""
to_card = tx.to_card if tx.to_card is not None else ""
if tx.from_card == self.card_id:
sign = "-"
elif tx.to_card == self.card_id:
sign = "+"
else:
sign = ""
amount_str = f"{sign}{tx.amount:.2f}₽"
mcc_str = tx.mcc if tx.mcc else ""
tx_type = tx.type.value
cashback_str = f"{getattr(tx, 'cashback', 0):.2f}₽"
rows.append(
f"{dt_str},{tx_type},{from_card},{to_card},{amount_str},{mcc_str},{cashback_str},{tx.description}"
)
return rows
def close(self):
self.status = CardStatus.CLOSED
@dataclass
class Bank:
name: str
bic: str
_user_seq: int = field(default_factory=lambda: _it.count(1), init=False)
_account_seq: int = field(default_factory=lambda: _it.count(1), init=False)
_card_seq: int = field(default_factory=lambda: _it.count(1), init=False)
_pan_seq: int = field(default_factory=lambda: _it.count(1), init=False)
customers: dict = field(default_factory=dict)
accounts: dict = field(default_factory=dict)
cards: dict = field(default_factory=dict)
transaction_log: list = field(default_factory=list)
def _next_account_number(self):
prefix_left = ACCOUNT_TYPE_CODE + ACCOUNT_CURRENCY
prefix_right = ACCOUNT_BRANCH
bic_tail = self.bic[-3:]
serial = f"{next(self._account_seq):07d}"
for control_digit in range(10):
candidate_account_number = prefix_left + str(control_digit) + prefix_right + serial
digits = [int(d) for d in bic_tail + candidate_account_number]
weights = [7, 1, 3] * 8
weighted = [a * b for a, b in zip(digits, weights[:23])]
control_sum = sum(x % 10 for x in weighted)
if control_sum % 10 == 0:
return candidate_account_number
def _generate_pan(self, system):
bin_code = BIN_BY_SYSTEM.get(
system.upper(), BIN_BY_SYSTEM[DEFAULT_PAYMENT_SYSTEM]
)
seq = f"{next(self._pan_seq):09d}"
partial = bin_code + seq
check = self._luhn(partial)
return partial + str(check)
def _luhn(self, number15):
digits = [int(d) for d in number15[::-1]]
for i in range(1, len(digits), 2):
doubled = digits[i] * 2
digits[i] = doubled - 9 if doubled > 9 else doubled
return (10 - sum(digits) % 10) % 10
def apply_for_card(
self,
last_name,
first_name,
pin,
phone,
payment_system=DEFAULT_PAYMENT_SYSTEM,
card_class: type = Card,
**kwargs,
):
# Поиск существующего пользователя по телефону
user = next(
(
user
for user in self.customers.values()
if user.last_name == last_name
and user.first_name == first_name
and user.phone == phone
),
None,
)
if not user:
user_id = next(self._user_seq)
user = User(last_name, first_name, pin, phone, user_id)
self.customers[user_id] = user
acc_id = self._next_account_number()
acc = Account(owner=user, acc_id=acc_id)
self.accounts[acc_id] = acc
user.accounts.append(acc)
card_id = next(self._card_seq)
pan = self._generate_pan(payment_system)
issue_date = next(ISSUE_DATE_GENERATOR)
card = card_class(
account=acc,
card_id=card_id,
payment_system=payment_system,
pan=pan,
issue_date=issue_date,
currency=CARD_CURRENCY,
status=CARD_STATUS,
bank=self,
**kwargs,
)
self.cards[card_id] = card
user.cards.append(card)
return card
def get_global_history(self) -> list:
rows = list(TRANSACTION_HISTORY_HEADER)
for tx in sorted(self.transaction_log, key=lambda x: x.timestamp):
dt_str = tx.timestamp.strftime("%Y-%m-%d %H:%M:%S")
tx_type = tx.type.value
from_card = tx.from_card if tx.from_card is not None else ""
to_card = tx.to_card if tx.to_card is not None else ""
amount_str = f"{tx.amount:.2f}₽"
cashback_str = f"{getattr(tx, 'cashback', 0):.2f}₽"
mcc_str = tx.mcc if tx.mcc else ""
rows.append(
f"{dt_str},{tx_type},{from_card},{to_card},{amount_str},{mcc_str},{cashback_str},{tx.description}"
)
return rows
# Заводим банк
bank = Bank("Demo Bank", "044452345")
# Заводим карты
cards = []
card_data = [
("Иванов", "Иван", "1234", "+79161234501", "MIR"),
("Петров", "Пётр", "5678", "+79161234502", "VISA"),
("Сидоров", "Сидор", "0000", "+79161234503", "MASTERCARD"),
("Кузнецов", "Кузьма", "9999", "+79161234504", "VISA"),
("Смирнов", "Сергей", "1111", "+79161234505", "MIR"),
("Смирнов", "Сергей", "1111", "+79161234505", "VISA"),
("Захаров", "Кирилл", "1212", "+79161234507", "MASTERCARD")
]
for last_name, first_name, pin, phone, system in card_data:
card = bank.apply_for_card(last_name, first_name, pin, phone, system)
cards.append(card)
# Проверяем смену PIN-Code
print("Проверка pin-code")
user = cards[0].account.owner
print("Старый PIN:", user.pin)
user.change_pin("1234", "5678")
print("Новый PIN (после правильной смены):", user.pin)
user.change_pin("0000", "9999")
print("PIN после попытки с неверным старым:", user.pin)
# Проверяем работу метода get_balance
print("\nБаланс выпущенных карт")
for i, card in enumerate(cards):
print(card.get_balance())
# Проверяем работу метода deposit
print("\nПополняем депозиты карт")
for i, card in enumerate(cards):
amount = 100 * (i + 1)
card.deposit(amount)
print(card.get_balance())
# Проверяем работу метода pay
print("\nПокупаем продукты в магазине")
print(cards[0].get_balance())
cards[0].pay(45, "5814")
print(cards[0].get_balance())
# Проверяем работу метода transfer
print("\nПереводим деньги с одной карты на другую")
print(cards[0].get_balance())
print(cards[1].get_balance())
cards[0].transfer(cards[1], 50)
print(cards[0].get_balance())
print(cards[1].get_balance())
for row in cards[1].get_transaction_history():
print(row)
# Проверяем работу метода close
print("\nЗакрываем карту")
print(cards[0].status.value)
cards[0].close()
print(cards[0].status.value)
# Проверяем работу метода get_transaction_history
print("\nИстория транзакций банковской карты")
for row in cards[0].get_transaction_history():
print(row)
print(cards[0].get_balance())
# Проверяем работу метода get_global_history
print("\nИстория транзакций банка")
for row in bank.get_global_history():
print(row)