nanopy-bank 1.0.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nanopy_bank/__init__.py +20 -0
- nanopy_bank/api/__init__.py +10 -0
- nanopy_bank/api/server.py +242 -0
- nanopy_bank/app.py +282 -0
- nanopy_bank/cli.py +152 -0
- nanopy_bank/core/__init__.py +85 -0
- nanopy_bank/core/audit.py +404 -0
- nanopy_bank/core/auth.py +306 -0
- nanopy_bank/core/bank.py +407 -0
- nanopy_bank/core/beneficiary.py +258 -0
- nanopy_bank/core/branch.py +319 -0
- nanopy_bank/core/fees.py +243 -0
- nanopy_bank/core/holding.py +416 -0
- nanopy_bank/core/models.py +308 -0
- nanopy_bank/core/products.py +300 -0
- nanopy_bank/data/__init__.py +31 -0
- nanopy_bank/data/demo.py +846 -0
- nanopy_bank/documents/__init__.py +11 -0
- nanopy_bank/documents/statement.py +304 -0
- nanopy_bank/sepa/__init__.py +10 -0
- nanopy_bank/sepa/sepa.py +452 -0
- nanopy_bank/storage/__init__.py +11 -0
- nanopy_bank/storage/json_storage.py +127 -0
- nanopy_bank/storage/sqlite_storage.py +326 -0
- nanopy_bank/ui/__init__.py +14 -0
- nanopy_bank/ui/pages/__init__.py +33 -0
- nanopy_bank/ui/pages/accounts.py +85 -0
- nanopy_bank/ui/pages/advisor.py +140 -0
- nanopy_bank/ui/pages/audit.py +73 -0
- nanopy_bank/ui/pages/beneficiaries.py +115 -0
- nanopy_bank/ui/pages/branches.py +64 -0
- nanopy_bank/ui/pages/cards.py +36 -0
- nanopy_bank/ui/pages/common.py +18 -0
- nanopy_bank/ui/pages/dashboard.py +100 -0
- nanopy_bank/ui/pages/fees.py +60 -0
- nanopy_bank/ui/pages/holding.py +943 -0
- nanopy_bank/ui/pages/loans.py +105 -0
- nanopy_bank/ui/pages/login.py +174 -0
- nanopy_bank/ui/pages/sepa.py +118 -0
- nanopy_bank/ui/pages/settings.py +48 -0
- nanopy_bank/ui/pages/transfers.py +94 -0
- nanopy_bank/ui/pages.py +16 -0
- nanopy_bank-1.0.8.dist-info/METADATA +72 -0
- nanopy_bank-1.0.8.dist-info/RECORD +47 -0
- nanopy_bank-1.0.8.dist-info/WHEEL +5 -0
- nanopy_bank-1.0.8.dist-info/entry_points.txt +2 -0
- nanopy_bank-1.0.8.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Beneficiaries page
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import streamlit as st
|
|
6
|
+
|
|
7
|
+
from .common import page_header
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def render_beneficiaries():
|
|
11
|
+
"""Render beneficiaries page"""
|
|
12
|
+
page_header("Beneficiaries")
|
|
13
|
+
|
|
14
|
+
bank = st.session_state.bank
|
|
15
|
+
|
|
16
|
+
tab1, tab2, tab3 = st.tabs(["Saved Beneficiaries", "Standing Orders", "SEPA Mandates"])
|
|
17
|
+
|
|
18
|
+
with tab1:
|
|
19
|
+
st.markdown("### Saved Beneficiaries")
|
|
20
|
+
|
|
21
|
+
with st.expander("Add New Beneficiary", expanded=False):
|
|
22
|
+
with st.form("add_beneficiary"):
|
|
23
|
+
col1, col2 = st.columns(2)
|
|
24
|
+
with col1:
|
|
25
|
+
ben_name = st.text_input("Beneficiary Name")
|
|
26
|
+
ben_iban = st.text_input("IBAN")
|
|
27
|
+
ben_bic = st.text_input("BIC (optional)")
|
|
28
|
+
with col2:
|
|
29
|
+
ben_alias = st.text_input("Alias (e.g., 'Mom', 'Landlord')")
|
|
30
|
+
ben_category = st.selectbox("Category", ["family", "bills", "business", "friends", "other"])
|
|
31
|
+
|
|
32
|
+
if st.form_submit_button("Add Beneficiary"):
|
|
33
|
+
st.success(f"Beneficiary '{ben_name}' added!")
|
|
34
|
+
|
|
35
|
+
demo_beneficiaries = [
|
|
36
|
+
{"name": "Marie Dupont", "iban": "FR76 1441 0000 0112 3456 7890 123", "alias": "Maman", "category": "family"},
|
|
37
|
+
{"name": "SCI Les Lilas", "iban": "FR76 3000 4000 0312 3456 7890 143", "alias": "Proprietaire", "category": "bills"},
|
|
38
|
+
{"name": "EDF", "iban": "FR76 3000 1007 9412 3456 7890 185", "alias": "Electricite", "category": "bills"},
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
for ben in demo_beneficiaries:
|
|
42
|
+
col1, col2, col3, col4 = st.columns([3, 3, 2, 1])
|
|
43
|
+
with col1:
|
|
44
|
+
st.markdown(f"**{ben['name']}**")
|
|
45
|
+
st.caption(ben['alias'])
|
|
46
|
+
with col2:
|
|
47
|
+
st.code(ben['iban'], language=None)
|
|
48
|
+
with col3:
|
|
49
|
+
st.caption(ben['category'])
|
|
50
|
+
with col4:
|
|
51
|
+
st.button("Use", key=f"use_{ben['iban']}")
|
|
52
|
+
st.divider()
|
|
53
|
+
|
|
54
|
+
with tab2:
|
|
55
|
+
st.markdown("### Standing Orders (Virements Permanents)")
|
|
56
|
+
|
|
57
|
+
with st.expander("Create Standing Order", expanded=False):
|
|
58
|
+
with st.form("add_standing_order"):
|
|
59
|
+
col1, col2 = st.columns(2)
|
|
60
|
+
with col1:
|
|
61
|
+
if bank.accounts:
|
|
62
|
+
from_options = {acc.account_name: acc.iban for acc in bank.accounts.values()}
|
|
63
|
+
st.selectbox("From Account", list(from_options.keys()))
|
|
64
|
+
st.text_input("To IBAN")
|
|
65
|
+
st.text_input("Beneficiary Name")
|
|
66
|
+
with col2:
|
|
67
|
+
st.number_input("Amount (EUR)", min_value=0.01, value=100.0)
|
|
68
|
+
st.selectbox("Frequency", ["Monthly", "Weekly", "Quarterly", "Yearly"])
|
|
69
|
+
st.number_input("Execution Day", min_value=1, max_value=28, value=1)
|
|
70
|
+
st.text_input("Label")
|
|
71
|
+
|
|
72
|
+
if st.form_submit_button("Create Standing Order"):
|
|
73
|
+
st.success("Standing order created!")
|
|
74
|
+
|
|
75
|
+
demo_orders = [
|
|
76
|
+
{"label": "Loyer mensuel", "to": "SCI Les Lilas", "amount": "850.00", "frequency": "Monthly", "next": "05/02/2026"},
|
|
77
|
+
{"label": "Epargne mensuelle", "to": "Mon Livret A", "amount": "200.00", "frequency": "Monthly", "next": "01/02/2026"},
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
for order in demo_orders:
|
|
81
|
+
col1, col2, col3, col4 = st.columns([3, 2, 2, 1])
|
|
82
|
+
with col1:
|
|
83
|
+
st.markdown(f"**{order['label']}**")
|
|
84
|
+
st.caption(f"To: {order['to']}")
|
|
85
|
+
with col2:
|
|
86
|
+
st.markdown(f":blue[{order['amount']} EUR]")
|
|
87
|
+
st.caption(order['frequency'])
|
|
88
|
+
with col3:
|
|
89
|
+
st.caption(f"Next: {order['next']}")
|
|
90
|
+
with col4:
|
|
91
|
+
st.button("Edit", key=f"edit_order_{order['label']}")
|
|
92
|
+
st.divider()
|
|
93
|
+
|
|
94
|
+
with tab3:
|
|
95
|
+
st.markdown("### SEPA Mandates (Mandats de Prelevement)")
|
|
96
|
+
st.info("Manage your direct debit authorizations here.")
|
|
97
|
+
|
|
98
|
+
demo_mandates = [
|
|
99
|
+
{"creditor": "Netflix", "reference": "MNDT-NF-001234", "max": "19.99", "status": "Active"},
|
|
100
|
+
{"creditor": "EDF", "reference": "MNDT-EDF-005678", "max": "200.00", "status": "Active"},
|
|
101
|
+
{"creditor": "Orange", "reference": "MNDT-OR-009012", "max": "50.00", "status": "Active"},
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
for mandate in demo_mandates:
|
|
105
|
+
col1, col2, col3, col4 = st.columns([3, 2, 2, 1])
|
|
106
|
+
with col1:
|
|
107
|
+
st.markdown(f"**{mandate['creditor']}**")
|
|
108
|
+
st.caption(mandate['reference'])
|
|
109
|
+
with col2:
|
|
110
|
+
st.markdown(f"Max: {mandate['max']} EUR")
|
|
111
|
+
with col3:
|
|
112
|
+
st.success(mandate['status'])
|
|
113
|
+
with col4:
|
|
114
|
+
st.button("Revoke", key=f"revoke_{mandate['reference']}")
|
|
115
|
+
st.divider()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Branches page
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import streamlit as st
|
|
6
|
+
|
|
7
|
+
from .common import page_header
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def render_branches():
|
|
11
|
+
"""Render branches and employees page"""
|
|
12
|
+
page_header("Branches & Contact")
|
|
13
|
+
|
|
14
|
+
tab1, tab2 = st.tabs(["Our Branches", "Contact Advisor"])
|
|
15
|
+
|
|
16
|
+
with tab1:
|
|
17
|
+
st.markdown("### Find a Branch")
|
|
18
|
+
|
|
19
|
+
st.text_input("Search by city or postal code", placeholder="Paris, 75001...")
|
|
20
|
+
|
|
21
|
+
branches = [
|
|
22
|
+
{"name": "Paris Opera", "address": "1 Place de l'Opera, 75009 Paris", "phone": "+33 1 42 68 00 00", "open": True},
|
|
23
|
+
{"name": "Lyon Part-Dieu", "address": "17 Rue de la Part-Dieu, 69003 Lyon", "phone": "+33 4 72 00 00 00", "open": True},
|
|
24
|
+
{"name": "Marseille Vieux-Port", "address": "42 Quai du Port, 13002 Marseille", "phone": "+33 4 91 00 00 00", "open": False},
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
for branch in branches:
|
|
28
|
+
col1, col2, col3 = st.columns([3, 2, 1])
|
|
29
|
+
with col1:
|
|
30
|
+
st.markdown(f"**{branch['name']}**")
|
|
31
|
+
st.caption(branch['address'])
|
|
32
|
+
with col2:
|
|
33
|
+
st.caption(branch['phone'])
|
|
34
|
+
with col3:
|
|
35
|
+
if branch['open']:
|
|
36
|
+
st.success("Open")
|
|
37
|
+
else:
|
|
38
|
+
st.error("Closed")
|
|
39
|
+
st.divider()
|
|
40
|
+
|
|
41
|
+
with tab2:
|
|
42
|
+
st.markdown("### Your Advisor")
|
|
43
|
+
|
|
44
|
+
col1, col2 = st.columns([1, 2])
|
|
45
|
+
with col1:
|
|
46
|
+
st.markdown("""
|
|
47
|
+
<div style="background: #1e1e2f; border-radius: 50%; width: 100px; height: 100px; display: flex; align-items: center; justify-content: center; font-size: 40px;">
|
|
48
|
+
👤
|
|
49
|
+
</div>
|
|
50
|
+
""", unsafe_allow_html=True)
|
|
51
|
+
with col2:
|
|
52
|
+
st.markdown("**Thomas Moreau**")
|
|
53
|
+
st.caption("Personal Banking Advisor")
|
|
54
|
+
st.markdown("Email: thomas.moreau@nanopybank.fr")
|
|
55
|
+
st.markdown("Phone: +33 1 42 68 00 01")
|
|
56
|
+
|
|
57
|
+
st.divider()
|
|
58
|
+
|
|
59
|
+
st.markdown("### Send a Message")
|
|
60
|
+
with st.form("contact_form"):
|
|
61
|
+
st.text_input("Subject")
|
|
62
|
+
st.text_area("Message")
|
|
63
|
+
if st.form_submit_button("Send"):
|
|
64
|
+
st.success("Message sent to your advisor!")
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cards page
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import streamlit as st
|
|
6
|
+
|
|
7
|
+
from .common import page_header
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def render_cards():
|
|
11
|
+
"""Render cards page"""
|
|
12
|
+
page_header("Cards")
|
|
13
|
+
|
|
14
|
+
st.info("Card management coming soon!")
|
|
15
|
+
|
|
16
|
+
st.markdown("### Your Cards")
|
|
17
|
+
|
|
18
|
+
col1, col2 = st.columns(2)
|
|
19
|
+
|
|
20
|
+
with col1:
|
|
21
|
+
st.markdown("""
|
|
22
|
+
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 16px; padding: 24px; color: white; height: 200px;">
|
|
23
|
+
<div style="font-size: 12px; opacity: 0.8;">VISA</div>
|
|
24
|
+
<div style="font-size: 24px; letter-spacing: 4px; margin: 24px 0;">**** **** **** 4242</div>
|
|
25
|
+
<div style="display: flex; justify-content: space-between;">
|
|
26
|
+
<div>
|
|
27
|
+
<div style="font-size: 10px; opacity: 0.8;">CARD HOLDER</div>
|
|
28
|
+
<div>JOHN DOE</div>
|
|
29
|
+
</div>
|
|
30
|
+
<div>
|
|
31
|
+
<div style="font-size: 10px; opacity: 0.8;">EXPIRES</div>
|
|
32
|
+
<div>12/27</div>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
""", unsafe_allow_html=True)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Common utilities for UI pages
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import streamlit as st
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def format_currency(amount: Decimal, currency: str = "EUR") -> str:
|
|
10
|
+
"""Format amount with currency symbol"""
|
|
11
|
+
symbols = {"EUR": "€", "USD": "$", "GBP": "£", "CHF": "CHF"}
|
|
12
|
+
symbol = symbols.get(currency, currency)
|
|
13
|
+
return f"{amount:,.2f} {symbol}"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def page_header(title: str):
|
|
17
|
+
"""Render page header"""
|
|
18
|
+
st.markdown(f'<h1 class="main-header">{title}</h1>', unsafe_allow_html=True)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dashboard page
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import streamlit as st
|
|
6
|
+
import streamlit_shadcn_ui as ui
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
|
|
9
|
+
from .common import format_currency, page_header
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def render_dashboard():
|
|
13
|
+
"""Render dashboard page"""
|
|
14
|
+
page_header("Dashboard")
|
|
15
|
+
|
|
16
|
+
bank = st.session_state.bank
|
|
17
|
+
|
|
18
|
+
if bank.accounts:
|
|
19
|
+
accounts = list(bank.accounts.values())
|
|
20
|
+
account_options = {f"{acc.account_name} ({acc.format_iban()})": acc.iban for acc in accounts}
|
|
21
|
+
|
|
22
|
+
selected = st.selectbox("Select Account", options=list(account_options.keys()), key="account_selector")
|
|
23
|
+
|
|
24
|
+
if selected:
|
|
25
|
+
iban = account_options[selected]
|
|
26
|
+
account = bank.get_account(iban)
|
|
27
|
+
st.session_state.current_account = account
|
|
28
|
+
|
|
29
|
+
col1, col2, col3 = st.columns(3)
|
|
30
|
+
|
|
31
|
+
with col1:
|
|
32
|
+
ui.metric_card(
|
|
33
|
+
title="Current Balance",
|
|
34
|
+
content=format_currency(account.balance, account.currency.value),
|
|
35
|
+
description=f"Available: {format_currency(account.available_balance, account.currency.value)}",
|
|
36
|
+
key="balance_card"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
with col2:
|
|
40
|
+
ui.metric_card(
|
|
41
|
+
title="Account Type",
|
|
42
|
+
content=account.account_type.value.title(),
|
|
43
|
+
description=f"Status: {account.status.value.title()}",
|
|
44
|
+
key="type_card"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
with col3:
|
|
48
|
+
ui.metric_card(
|
|
49
|
+
title="IBAN",
|
|
50
|
+
content=account.format_iban()[:19] + "...",
|
|
51
|
+
description=f"BIC: {account.bic}",
|
|
52
|
+
key="iban_card"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
st.divider()
|
|
56
|
+
|
|
57
|
+
st.markdown("### Recent Transactions")
|
|
58
|
+
transactions = bank.get_account_transactions(iban, limit=10)
|
|
59
|
+
|
|
60
|
+
if transactions:
|
|
61
|
+
for tx in transactions:
|
|
62
|
+
col1, col2, col3, col4 = st.columns([3, 2, 2, 1])
|
|
63
|
+
with col1:
|
|
64
|
+
st.markdown(f"**{tx.label}**")
|
|
65
|
+
st.caption(tx.counterparty_name or tx.description)
|
|
66
|
+
with col2:
|
|
67
|
+
st.caption(tx.created_at.strftime("%d/%m/%Y %H:%M"))
|
|
68
|
+
with col3:
|
|
69
|
+
color = "green" if tx.is_credit else "red"
|
|
70
|
+
sign = "+" if tx.is_credit else "-"
|
|
71
|
+
st.markdown(f":{color}[{sign}{format_currency(tx.amount, tx.currency.value)}]")
|
|
72
|
+
with col4:
|
|
73
|
+
st.caption(tx.transaction_type.value)
|
|
74
|
+
st.divider()
|
|
75
|
+
else:
|
|
76
|
+
st.info("No transactions yet")
|
|
77
|
+
|
|
78
|
+
else:
|
|
79
|
+
st.warning("No accounts found. Create one in the Accounts section.")
|
|
80
|
+
|
|
81
|
+
if ui.button("Create Demo Account", key="create_demo"):
|
|
82
|
+
from nanopy_bank.core import AccountType, TransactionType
|
|
83
|
+
|
|
84
|
+
customer = bank.create_customer(
|
|
85
|
+
first_name="John", last_name="Doe",
|
|
86
|
+
email="john.doe@example.com", phone="+33612345678",
|
|
87
|
+
address="123 Rue de la Paix", city="Paris",
|
|
88
|
+
postal_code="75001", country="FR"
|
|
89
|
+
)
|
|
90
|
+
account = bank.create_account(
|
|
91
|
+
customer_id=customer.customer_id,
|
|
92
|
+
account_type=AccountType.CHECKING,
|
|
93
|
+
initial_balance=Decimal("1500.00"),
|
|
94
|
+
account_name="Compte Principal"
|
|
95
|
+
)
|
|
96
|
+
bank.credit(account.iban, Decimal("2500.00"), "Salaire Janvier", "ACME Corp", "", TransactionType.SEPA_CREDIT)
|
|
97
|
+
bank.debit(account.iban, Decimal("45.90"), "Carrefour", "CARREFOUR", "", TransactionType.CARD_PAYMENT, category="Courses")
|
|
98
|
+
|
|
99
|
+
st.success(f"Demo account created: {account.format_iban()}")
|
|
100
|
+
st.rerun()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Fees page
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import streamlit as st
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
from .common import page_header
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def render_fees():
|
|
12
|
+
"""Render fees and rates page"""
|
|
13
|
+
page_header("Fees & Rates")
|
|
14
|
+
|
|
15
|
+
tab1, tab2 = st.tabs(["Banking Fees", "Interest Rates"])
|
|
16
|
+
|
|
17
|
+
with tab1:
|
|
18
|
+
st.markdown("### Banking Fees Schedule")
|
|
19
|
+
|
|
20
|
+
fees = [
|
|
21
|
+
{"name": "Account Maintenance", "amount": "2.00 EUR", "frequency": "Monthly"},
|
|
22
|
+
{"name": "Visa Card Annual Fee", "amount": "45.00 EUR", "frequency": "Yearly"},
|
|
23
|
+
{"name": "SEPA Transfer", "amount": "Free", "frequency": "Per transaction"},
|
|
24
|
+
{"name": "International Transfer", "amount": "15.00 EUR", "frequency": "Per transaction"},
|
|
25
|
+
{"name": "Overdraft Fee", "amount": "8.00 EUR", "frequency": "Per occurrence"},
|
|
26
|
+
{"name": "Rejected Payment", "amount": "20.00 EUR", "frequency": "Per occurrence"},
|
|
27
|
+
{"name": "ATM (Other Banks)", "amount": "1.00 EUR", "frequency": "After 3/month"},
|
|
28
|
+
{"name": "Currency Conversion", "amount": "2.00%", "frequency": "Per transaction"},
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
df = pd.DataFrame(fees)
|
|
32
|
+
st.dataframe(df, use_container_width=True, hide_index=True)
|
|
33
|
+
|
|
34
|
+
with tab2:
|
|
35
|
+
st.markdown("### Current Interest Rates")
|
|
36
|
+
|
|
37
|
+
col1, col2 = st.columns(2)
|
|
38
|
+
|
|
39
|
+
with col1:
|
|
40
|
+
st.markdown("#### Savings Rates")
|
|
41
|
+
savings_rates = [
|
|
42
|
+
{"product": "Livret A", "rate": "3.00%"},
|
|
43
|
+
{"product": "LDDS", "rate": "3.00%"},
|
|
44
|
+
{"product": "LEP", "rate": "5.00%"},
|
|
45
|
+
{"product": "PEL", "rate": "2.00%"},
|
|
46
|
+
]
|
|
47
|
+
for r in savings_rates:
|
|
48
|
+
st.markdown(f"**{r['product']}**: :green[{r['rate']}]")
|
|
49
|
+
|
|
50
|
+
with col2:
|
|
51
|
+
st.markdown("#### Loan Rates")
|
|
52
|
+
loan_rates = [
|
|
53
|
+
{"product": "Personal Loan", "rate": "5.50%"},
|
|
54
|
+
{"product": "Auto Loan", "rate": "4.90%"},
|
|
55
|
+
{"product": "Mortgage (20yr)", "rate": "3.80%"},
|
|
56
|
+
{"product": "Overdraft (Authorized)", "rate": "7.00%"},
|
|
57
|
+
{"product": "Overdraft (Unauthorized)", "rate": "16.00%"},
|
|
58
|
+
]
|
|
59
|
+
for r in loan_rates:
|
|
60
|
+
st.markdown(f"**{r['product']}**: :red[{r['rate']}]")
|