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,319 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Branch and Employee models - Bank organization structure
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime, date, time
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Optional, List, Dict
|
|
10
|
+
import uuid
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BranchType(Enum):
|
|
14
|
+
"""Types of branches"""
|
|
15
|
+
HEADQUARTERS = "headquarters" # Siège social
|
|
16
|
+
REGIONAL = "regional" # Direction régionale
|
|
17
|
+
BRANCH = "branch" # Agence
|
|
18
|
+
ONLINE = "online" # Banque en ligne
|
|
19
|
+
ATM_ONLY = "atm_only" # Point automatique
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class EmployeeRole(Enum):
|
|
23
|
+
"""Employee roles"""
|
|
24
|
+
DIRECTOR = "director" # Directeur
|
|
25
|
+
MANAGER = "manager" # Responsable
|
|
26
|
+
ADVISOR = "advisor" # Conseiller
|
|
27
|
+
TELLER = "teller" # Guichetier
|
|
28
|
+
ANALYST = "analyst" # Analyste
|
|
29
|
+
SUPPORT = "support" # Support client
|
|
30
|
+
IT = "it" # Informatique
|
|
31
|
+
COMPLIANCE = "compliance" # Conformité
|
|
32
|
+
RISK = "risk" # Risques
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class EmployeeStatus(Enum):
|
|
36
|
+
"""Employee status"""
|
|
37
|
+
ACTIVE = "active"
|
|
38
|
+
ON_LEAVE = "on_leave"
|
|
39
|
+
SUSPENDED = "suspended"
|
|
40
|
+
TERMINATED = "terminated"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class Branch:
|
|
45
|
+
"""
|
|
46
|
+
Bank branch / Agency
|
|
47
|
+
"""
|
|
48
|
+
branch_id: str = field(default_factory=lambda: f"BR{uuid.uuid4().hex[:6].upper()}")
|
|
49
|
+
branch_code: str = "" # Code guichet (5 digits in France)
|
|
50
|
+
|
|
51
|
+
# Branch info
|
|
52
|
+
name: str = ""
|
|
53
|
+
branch_type: BranchType = BranchType.BRANCH
|
|
54
|
+
|
|
55
|
+
# Location
|
|
56
|
+
address: str = ""
|
|
57
|
+
city: str = ""
|
|
58
|
+
postal_code: str = ""
|
|
59
|
+
country: str = "FR"
|
|
60
|
+
latitude: Optional[float] = None
|
|
61
|
+
longitude: Optional[float] = None
|
|
62
|
+
|
|
63
|
+
# Contact
|
|
64
|
+
phone: str = ""
|
|
65
|
+
fax: str = ""
|
|
66
|
+
email: str = ""
|
|
67
|
+
|
|
68
|
+
# Opening hours
|
|
69
|
+
opening_hours: Dict[str, Dict[str, str]] = field(default_factory=lambda: {
|
|
70
|
+
"monday": {"open": "09:00", "close": "17:00"},
|
|
71
|
+
"tuesday": {"open": "09:00", "close": "17:00"},
|
|
72
|
+
"wednesday": {"open": "09:00", "close": "17:00"},
|
|
73
|
+
"thursday": {"open": "09:00", "close": "17:00"},
|
|
74
|
+
"friday": {"open": "09:00", "close": "17:00"},
|
|
75
|
+
"saturday": {"open": "09:00", "close": "12:00"},
|
|
76
|
+
"sunday": {"open": "", "close": ""}, # Closed
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
# Services
|
|
80
|
+
services: List[str] = field(default_factory=lambda: [
|
|
81
|
+
"accounts", "cards", "loans", "insurance", "savings"
|
|
82
|
+
])
|
|
83
|
+
has_atm: bool = True
|
|
84
|
+
has_safe_deposit: bool = False # Coffres
|
|
85
|
+
wheelchair_accessible: bool = True
|
|
86
|
+
|
|
87
|
+
# Hierarchy
|
|
88
|
+
parent_branch_id: Optional[str] = None # For regional structure
|
|
89
|
+
manager_employee_id: Optional[str] = None
|
|
90
|
+
|
|
91
|
+
# Statistics
|
|
92
|
+
customer_count: int = 0
|
|
93
|
+
account_count: int = 0
|
|
94
|
+
total_deposits: Decimal = Decimal("0.00")
|
|
95
|
+
total_loans: Decimal = Decimal("0.00")
|
|
96
|
+
|
|
97
|
+
# Status
|
|
98
|
+
is_active: bool = True
|
|
99
|
+
opened_date: date = field(default_factory=date.today)
|
|
100
|
+
closed_date: Optional[date] = None
|
|
101
|
+
|
|
102
|
+
# Metadata
|
|
103
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
104
|
+
|
|
105
|
+
def is_open_now(self) -> bool:
|
|
106
|
+
"""Check if branch is currently open"""
|
|
107
|
+
now = datetime.now()
|
|
108
|
+
day = now.strftime("%A").lower()
|
|
109
|
+
hours = self.opening_hours.get(day, {})
|
|
110
|
+
|
|
111
|
+
if not hours.get("open") or not hours.get("close"):
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
open_time = datetime.strptime(hours["open"], "%H:%M").time()
|
|
115
|
+
close_time = datetime.strptime(hours["close"], "%H:%M").time()
|
|
116
|
+
current_time = now.time()
|
|
117
|
+
|
|
118
|
+
return open_time <= current_time <= close_time
|
|
119
|
+
|
|
120
|
+
def to_dict(self) -> dict:
|
|
121
|
+
return {
|
|
122
|
+
"branch_id": self.branch_id,
|
|
123
|
+
"branch_code": self.branch_code,
|
|
124
|
+
"name": self.name,
|
|
125
|
+
"branch_type": self.branch_type.value,
|
|
126
|
+
"address": self.address,
|
|
127
|
+
"city": self.city,
|
|
128
|
+
"postal_code": self.postal_code,
|
|
129
|
+
"country": self.country,
|
|
130
|
+
"latitude": self.latitude,
|
|
131
|
+
"longitude": self.longitude,
|
|
132
|
+
"phone": self.phone,
|
|
133
|
+
"fax": self.fax,
|
|
134
|
+
"email": self.email,
|
|
135
|
+
"opening_hours": self.opening_hours,
|
|
136
|
+
"services": self.services,
|
|
137
|
+
"has_atm": self.has_atm,
|
|
138
|
+
"has_safe_deposit": self.has_safe_deposit,
|
|
139
|
+
"wheelchair_accessible": self.wheelchair_accessible,
|
|
140
|
+
"parent_branch_id": self.parent_branch_id,
|
|
141
|
+
"manager_employee_id": self.manager_employee_id,
|
|
142
|
+
"customer_count": self.customer_count,
|
|
143
|
+
"account_count": self.account_count,
|
|
144
|
+
"total_deposits": str(self.total_deposits),
|
|
145
|
+
"total_loans": str(self.total_loans),
|
|
146
|
+
"is_active": self.is_active,
|
|
147
|
+
"is_open_now": self.is_open_now(),
|
|
148
|
+
"opened_date": self.opened_date.isoformat(),
|
|
149
|
+
"closed_date": self.closed_date.isoformat() if self.closed_date else None,
|
|
150
|
+
"created_at": self.created_at.isoformat(),
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@dataclass
|
|
155
|
+
class Employee:
|
|
156
|
+
"""
|
|
157
|
+
Bank employee
|
|
158
|
+
"""
|
|
159
|
+
employee_id: str = field(default_factory=lambda: f"EMP{uuid.uuid4().hex[:8].upper()}")
|
|
160
|
+
employee_number: str = "" # Matricule
|
|
161
|
+
|
|
162
|
+
# Personal info
|
|
163
|
+
first_name: str = ""
|
|
164
|
+
last_name: str = ""
|
|
165
|
+
email: str = ""
|
|
166
|
+
phone: str = ""
|
|
167
|
+
mobile: str = ""
|
|
168
|
+
|
|
169
|
+
# Role
|
|
170
|
+
role: EmployeeRole = EmployeeRole.ADVISOR
|
|
171
|
+
title: str = "" # Job title
|
|
172
|
+
department: str = ""
|
|
173
|
+
branch_id: Optional[str] = None
|
|
174
|
+
manager_id: Optional[str] = None
|
|
175
|
+
|
|
176
|
+
# Permissions
|
|
177
|
+
permissions: List[str] = field(default_factory=list)
|
|
178
|
+
max_approval_amount: Decimal = Decimal("0.00") # Max amount they can approve
|
|
179
|
+
can_approve_loans: bool = False
|
|
180
|
+
can_manage_accounts: bool = True
|
|
181
|
+
can_view_all_customers: bool = False
|
|
182
|
+
|
|
183
|
+
# Work info
|
|
184
|
+
hire_date: date = field(default_factory=date.today)
|
|
185
|
+
contract_type: str = "permanent" # permanent, temporary, intern
|
|
186
|
+
work_schedule: str = "full_time" # full_time, part_time
|
|
187
|
+
|
|
188
|
+
# Status
|
|
189
|
+
status: EmployeeStatus = EmployeeStatus.ACTIVE
|
|
190
|
+
|
|
191
|
+
# Performance
|
|
192
|
+
customers_managed: int = 0
|
|
193
|
+
transactions_processed: int = 0
|
|
194
|
+
loans_approved: int = 0
|
|
195
|
+
total_loan_amount_approved: Decimal = Decimal("0.00")
|
|
196
|
+
|
|
197
|
+
# Metadata
|
|
198
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
199
|
+
last_login: Optional[datetime] = None
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def full_name(self) -> str:
|
|
203
|
+
return f"{self.first_name} {self.last_name}"
|
|
204
|
+
|
|
205
|
+
def can_approve(self, amount: Decimal) -> bool:
|
|
206
|
+
"""Check if employee can approve this amount"""
|
|
207
|
+
return self.can_approve_loans and amount <= self.max_approval_amount
|
|
208
|
+
|
|
209
|
+
def to_dict(self) -> dict:
|
|
210
|
+
return {
|
|
211
|
+
"employee_id": self.employee_id,
|
|
212
|
+
"employee_number": self.employee_number,
|
|
213
|
+
"first_name": self.first_name,
|
|
214
|
+
"last_name": self.last_name,
|
|
215
|
+
"full_name": self.full_name,
|
|
216
|
+
"email": self.email,
|
|
217
|
+
"phone": self.phone,
|
|
218
|
+
"mobile": self.mobile,
|
|
219
|
+
"role": self.role.value,
|
|
220
|
+
"title": self.title,
|
|
221
|
+
"department": self.department,
|
|
222
|
+
"branch_id": self.branch_id,
|
|
223
|
+
"manager_id": self.manager_id,
|
|
224
|
+
"permissions": self.permissions,
|
|
225
|
+
"max_approval_amount": str(self.max_approval_amount),
|
|
226
|
+
"can_approve_loans": self.can_approve_loans,
|
|
227
|
+
"can_manage_accounts": self.can_manage_accounts,
|
|
228
|
+
"can_view_all_customers": self.can_view_all_customers,
|
|
229
|
+
"hire_date": self.hire_date.isoformat(),
|
|
230
|
+
"contract_type": self.contract_type,
|
|
231
|
+
"work_schedule": self.work_schedule,
|
|
232
|
+
"status": self.status.value,
|
|
233
|
+
"customers_managed": self.customers_managed,
|
|
234
|
+
"transactions_processed": self.transactions_processed,
|
|
235
|
+
"loans_approved": self.loans_approved,
|
|
236
|
+
"total_loan_amount_approved": str(self.total_loan_amount_approved),
|
|
237
|
+
"created_at": self.created_at.isoformat(),
|
|
238
|
+
"last_login": self.last_login.isoformat() if self.last_login else None,
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@dataclass
|
|
243
|
+
class ATM:
|
|
244
|
+
"""
|
|
245
|
+
ATM / Distributeur automatique
|
|
246
|
+
"""
|
|
247
|
+
atm_id: str = field(default_factory=lambda: f"ATM{uuid.uuid4().hex[:6].upper()}")
|
|
248
|
+
branch_id: Optional[str] = None
|
|
249
|
+
|
|
250
|
+
# Location
|
|
251
|
+
name: str = ""
|
|
252
|
+
address: str = ""
|
|
253
|
+
city: str = ""
|
|
254
|
+
postal_code: str = ""
|
|
255
|
+
country: str = "FR"
|
|
256
|
+
latitude: Optional[float] = None
|
|
257
|
+
longitude: Optional[float] = None
|
|
258
|
+
location_type: str = "indoor" # indoor, outdoor, mall, station
|
|
259
|
+
|
|
260
|
+
# Services
|
|
261
|
+
can_withdraw: bool = True
|
|
262
|
+
can_deposit_cash: bool = False
|
|
263
|
+
can_deposit_check: bool = False
|
|
264
|
+
can_check_balance: bool = True
|
|
265
|
+
can_transfer: bool = False
|
|
266
|
+
can_print_statement: bool = True
|
|
267
|
+
|
|
268
|
+
# Limits
|
|
269
|
+
max_withdrawal: Decimal = Decimal("500.00")
|
|
270
|
+
max_deposit: Decimal = Decimal("3000.00")
|
|
271
|
+
|
|
272
|
+
# Status
|
|
273
|
+
is_active: bool = True
|
|
274
|
+
is_online: bool = True
|
|
275
|
+
last_maintenance: Optional[datetime] = None
|
|
276
|
+
next_maintenance: Optional[date] = None
|
|
277
|
+
|
|
278
|
+
# Cash levels
|
|
279
|
+
cash_level: int = 100 # Percentage
|
|
280
|
+
low_cash_threshold: int = 20
|
|
281
|
+
|
|
282
|
+
# Statistics
|
|
283
|
+
transactions_today: int = 0
|
|
284
|
+
total_withdrawn_today: Decimal = Decimal("0.00")
|
|
285
|
+
|
|
286
|
+
# Metadata
|
|
287
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
288
|
+
|
|
289
|
+
def to_dict(self) -> dict:
|
|
290
|
+
return {
|
|
291
|
+
"atm_id": self.atm_id,
|
|
292
|
+
"branch_id": self.branch_id,
|
|
293
|
+
"name": self.name,
|
|
294
|
+
"address": self.address,
|
|
295
|
+
"city": self.city,
|
|
296
|
+
"postal_code": self.postal_code,
|
|
297
|
+
"country": self.country,
|
|
298
|
+
"latitude": self.latitude,
|
|
299
|
+
"longitude": self.longitude,
|
|
300
|
+
"location_type": self.location_type,
|
|
301
|
+
"can_withdraw": self.can_withdraw,
|
|
302
|
+
"can_deposit_cash": self.can_deposit_cash,
|
|
303
|
+
"can_deposit_check": self.can_deposit_check,
|
|
304
|
+
"can_check_balance": self.can_check_balance,
|
|
305
|
+
"can_transfer": self.can_transfer,
|
|
306
|
+
"can_print_statement": self.can_print_statement,
|
|
307
|
+
"max_withdrawal": str(self.max_withdrawal),
|
|
308
|
+
"max_deposit": str(self.max_deposit),
|
|
309
|
+
"is_active": self.is_active,
|
|
310
|
+
"is_online": self.is_online,
|
|
311
|
+
"last_maintenance": self.last_maintenance.isoformat() if self.last_maintenance else None,
|
|
312
|
+
"next_maintenance": self.next_maintenance.isoformat() if self.next_maintenance else None,
|
|
313
|
+
"cash_level": self.cash_level,
|
|
314
|
+
"low_cash_threshold": self.low_cash_threshold,
|
|
315
|
+
"needs_cash": self.cash_level < self.low_cash_threshold,
|
|
316
|
+
"transactions_today": self.transactions_today,
|
|
317
|
+
"total_withdrawn_today": str(self.total_withdrawn_today),
|
|
318
|
+
"created_at": self.created_at.isoformat(),
|
|
319
|
+
}
|
nanopy_bank/core/fees.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Fees and Rates - Interest rates, commissions, banking fees
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime, date
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Optional, List
|
|
10
|
+
import uuid
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FeeType(Enum):
|
|
14
|
+
"""Types of banking fees"""
|
|
15
|
+
# Account fees
|
|
16
|
+
ACCOUNT_MAINTENANCE = "account_maintenance" # Frais de tenue de compte
|
|
17
|
+
CARD_ANNUAL = "card_annual" # Cotisation carte
|
|
18
|
+
CARD_REPLACEMENT = "card_replacement" # Remplacement carte
|
|
19
|
+
|
|
20
|
+
# Transaction fees
|
|
21
|
+
TRANSFER_DOMESTIC = "transfer_domestic" # Virement national
|
|
22
|
+
TRANSFER_SEPA = "transfer_sepa" # Virement SEPA
|
|
23
|
+
TRANSFER_INTERNATIONAL = "transfer_international" # Virement international
|
|
24
|
+
DIRECT_DEBIT = "direct_debit" # Prélèvement
|
|
25
|
+
CHECK_PROCESSING = "check_processing" # Traitement chèque
|
|
26
|
+
|
|
27
|
+
# Overdraft fees
|
|
28
|
+
OVERDRAFT_INTEREST = "overdraft_interest" # Agios
|
|
29
|
+
OVERDRAFT_FEE = "overdraft_fee" # Commission d'intervention
|
|
30
|
+
REJECTED_PAYMENT = "rejected_payment" # Rejet de prélèvement
|
|
31
|
+
|
|
32
|
+
# Service fees
|
|
33
|
+
STATEMENT_PAPER = "statement_paper" # Relevé papier
|
|
34
|
+
CERTIFICATE = "certificate" # Attestation
|
|
35
|
+
WIRE_TRANSFER = "wire_transfer" # Virement urgent
|
|
36
|
+
|
|
37
|
+
# ATM fees
|
|
38
|
+
ATM_WITHDRAWAL_OTHER = "atm_withdrawal_other" # Retrait autre banque
|
|
39
|
+
ATM_WITHDRAWAL_FOREIGN = "atm_withdrawal_foreign" # Retrait étranger
|
|
40
|
+
|
|
41
|
+
# FX fees
|
|
42
|
+
CURRENCY_CONVERSION = "currency_conversion" # Conversion devise
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class RateType(Enum):
|
|
46
|
+
"""Types of interest rates"""
|
|
47
|
+
SAVINGS = "savings" # Taux épargne
|
|
48
|
+
LOAN = "loan" # Taux crédit
|
|
49
|
+
OVERDRAFT = "overdraft" # Taux découvert
|
|
50
|
+
MORTGAGE = "mortgage" # Taux immobilier
|
|
51
|
+
DEPOSIT = "deposit" # Taux dépôt
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class Fee:
|
|
56
|
+
"""
|
|
57
|
+
Banking fee definition
|
|
58
|
+
"""
|
|
59
|
+
fee_id: str = field(default_factory=lambda: f"FEE{uuid.uuid4().hex[:8].upper()}")
|
|
60
|
+
|
|
61
|
+
# Fee details
|
|
62
|
+
fee_type: FeeType = FeeType.ACCOUNT_MAINTENANCE
|
|
63
|
+
name: str = ""
|
|
64
|
+
description: str = ""
|
|
65
|
+
|
|
66
|
+
# Amount
|
|
67
|
+
amount: Decimal = Decimal("0.00")
|
|
68
|
+
is_percentage: bool = False # True = percentage, False = fixed amount
|
|
69
|
+
min_amount: Optional[Decimal] = None # Minimum if percentage
|
|
70
|
+
max_amount: Optional[Decimal] = None # Maximum if percentage
|
|
71
|
+
|
|
72
|
+
# Applicability
|
|
73
|
+
currency: str = "EUR"
|
|
74
|
+
account_types: List[str] = field(default_factory=list) # Empty = all types
|
|
75
|
+
customer_types: List[str] = field(default_factory=list) # "individual", "business"
|
|
76
|
+
|
|
77
|
+
# Frequency
|
|
78
|
+
frequency: str = "per_transaction" # per_transaction, monthly, yearly
|
|
79
|
+
|
|
80
|
+
# Tax
|
|
81
|
+
vat_rate: Decimal = Decimal("20.00") # TVA
|
|
82
|
+
vat_included: bool = True
|
|
83
|
+
|
|
84
|
+
# Status
|
|
85
|
+
is_active: bool = True
|
|
86
|
+
effective_from: date = field(default_factory=date.today)
|
|
87
|
+
effective_until: Optional[date] = None
|
|
88
|
+
|
|
89
|
+
# Metadata
|
|
90
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
91
|
+
|
|
92
|
+
def calculate(self, base_amount: Decimal = Decimal("0.00")) -> Decimal:
|
|
93
|
+
"""Calculate fee amount"""
|
|
94
|
+
if self.is_percentage:
|
|
95
|
+
fee = base_amount * self.amount / 100
|
|
96
|
+
if self.min_amount and fee < self.min_amount:
|
|
97
|
+
fee = self.min_amount
|
|
98
|
+
if self.max_amount and fee > self.max_amount:
|
|
99
|
+
fee = self.max_amount
|
|
100
|
+
return fee
|
|
101
|
+
return self.amount
|
|
102
|
+
|
|
103
|
+
def to_dict(self) -> dict:
|
|
104
|
+
return {
|
|
105
|
+
"fee_id": self.fee_id,
|
|
106
|
+
"fee_type": self.fee_type.value,
|
|
107
|
+
"name": self.name,
|
|
108
|
+
"description": self.description,
|
|
109
|
+
"amount": str(self.amount),
|
|
110
|
+
"is_percentage": self.is_percentage,
|
|
111
|
+
"min_amount": str(self.min_amount) if self.min_amount else None,
|
|
112
|
+
"max_amount": str(self.max_amount) if self.max_amount else None,
|
|
113
|
+
"currency": self.currency,
|
|
114
|
+
"account_types": self.account_types,
|
|
115
|
+
"customer_types": self.customer_types,
|
|
116
|
+
"frequency": self.frequency,
|
|
117
|
+
"vat_rate": str(self.vat_rate),
|
|
118
|
+
"vat_included": self.vat_included,
|
|
119
|
+
"is_active": self.is_active,
|
|
120
|
+
"effective_from": self.effective_from.isoformat(),
|
|
121
|
+
"effective_until": self.effective_until.isoformat() if self.effective_until else None,
|
|
122
|
+
"created_at": self.created_at.isoformat(),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass
|
|
127
|
+
class InterestRate:
|
|
128
|
+
"""
|
|
129
|
+
Interest rate definition
|
|
130
|
+
"""
|
|
131
|
+
rate_id: str = field(default_factory=lambda: f"RATE{uuid.uuid4().hex[:8].upper()}")
|
|
132
|
+
|
|
133
|
+
# Rate details
|
|
134
|
+
rate_type: RateType = RateType.SAVINGS
|
|
135
|
+
name: str = ""
|
|
136
|
+
description: str = ""
|
|
137
|
+
|
|
138
|
+
# Rate value
|
|
139
|
+
rate: Decimal = Decimal("0.00") # Annual rate (e.g., 3.00 = 3%)
|
|
140
|
+
is_variable: bool = False
|
|
141
|
+
base_rate: str = "" # Reference rate (EURIBOR, etc.)
|
|
142
|
+
margin: Decimal = Decimal("0.00") # Margin over base rate
|
|
143
|
+
|
|
144
|
+
# Tiered rates (optional)
|
|
145
|
+
tiers: List[dict] = field(default_factory=list) # [{"min": 0, "max": 10000, "rate": 2.0}, ...]
|
|
146
|
+
|
|
147
|
+
# Applicability
|
|
148
|
+
product_types: List[str] = field(default_factory=list)
|
|
149
|
+
min_amount: Optional[Decimal] = None
|
|
150
|
+
max_amount: Optional[Decimal] = None
|
|
151
|
+
min_duration_months: Optional[int] = None
|
|
152
|
+
max_duration_months: Optional[int] = None
|
|
153
|
+
|
|
154
|
+
# Status
|
|
155
|
+
is_active: bool = True
|
|
156
|
+
effective_from: date = field(default_factory=date.today)
|
|
157
|
+
effective_until: Optional[date] = None
|
|
158
|
+
|
|
159
|
+
# Metadata
|
|
160
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
161
|
+
|
|
162
|
+
def get_rate_for_amount(self, amount: Decimal) -> Decimal:
|
|
163
|
+
"""Get applicable rate for an amount (considering tiers)"""
|
|
164
|
+
if not self.tiers:
|
|
165
|
+
return self.rate
|
|
166
|
+
|
|
167
|
+
for tier in sorted(self.tiers, key=lambda t: t.get("min", 0)):
|
|
168
|
+
tier_min = Decimal(str(tier.get("min", 0)))
|
|
169
|
+
tier_max = Decimal(str(tier.get("max", float("inf"))))
|
|
170
|
+
if tier_min <= amount < tier_max:
|
|
171
|
+
return Decimal(str(tier.get("rate", self.rate)))
|
|
172
|
+
|
|
173
|
+
return self.rate
|
|
174
|
+
|
|
175
|
+
def to_dict(self) -> dict:
|
|
176
|
+
return {
|
|
177
|
+
"rate_id": self.rate_id,
|
|
178
|
+
"rate_type": self.rate_type.value,
|
|
179
|
+
"name": self.name,
|
|
180
|
+
"description": self.description,
|
|
181
|
+
"rate": str(self.rate),
|
|
182
|
+
"is_variable": self.is_variable,
|
|
183
|
+
"base_rate": self.base_rate,
|
|
184
|
+
"margin": str(self.margin),
|
|
185
|
+
"tiers": self.tiers,
|
|
186
|
+
"product_types": self.product_types,
|
|
187
|
+
"min_amount": str(self.min_amount) if self.min_amount else None,
|
|
188
|
+
"max_amount": str(self.max_amount) if self.max_amount else None,
|
|
189
|
+
"min_duration_months": self.min_duration_months,
|
|
190
|
+
"max_duration_months": self.max_duration_months,
|
|
191
|
+
"is_active": self.is_active,
|
|
192
|
+
"effective_from": self.effective_from.isoformat(),
|
|
193
|
+
"effective_until": self.effective_until.isoformat() if self.effective_until else None,
|
|
194
|
+
"created_at": self.created_at.isoformat(),
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@dataclass
|
|
199
|
+
class AppliedFee:
|
|
200
|
+
"""
|
|
201
|
+
A fee that was applied to an account/transaction
|
|
202
|
+
"""
|
|
203
|
+
applied_fee_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
204
|
+
fee_id: str = ""
|
|
205
|
+
account_iban: str = ""
|
|
206
|
+
transaction_id: Optional[str] = None
|
|
207
|
+
|
|
208
|
+
# Amount
|
|
209
|
+
amount: Decimal = Decimal("0.00")
|
|
210
|
+
vat_amount: Decimal = Decimal("0.00")
|
|
211
|
+
total_amount: Decimal = Decimal("0.00")
|
|
212
|
+
|
|
213
|
+
# Details
|
|
214
|
+
description: str = ""
|
|
215
|
+
period: str = "" # "2024-01" for monthly fees
|
|
216
|
+
|
|
217
|
+
# Status
|
|
218
|
+
status: str = "pending" # pending, applied, reversed
|
|
219
|
+
applied_at: Optional[datetime] = None
|
|
220
|
+
reversed_at: Optional[datetime] = None
|
|
221
|
+
|
|
222
|
+
# Metadata
|
|
223
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
224
|
+
|
|
225
|
+
def to_dict(self) -> dict:
|
|
226
|
+
return {
|
|
227
|
+
"applied_fee_id": self.applied_fee_id,
|
|
228
|
+
"fee_id": self.fee_id,
|
|
229
|
+
"account_iban": self.account_iban,
|
|
230
|
+
"transaction_id": self.transaction_id,
|
|
231
|
+
"amount": str(self.amount),
|
|
232
|
+
"vat_amount": str(self.vat_amount),
|
|
233
|
+
"total_amount": str(self.total_amount),
|
|
234
|
+
"description": self.description,
|
|
235
|
+
"period": self.period,
|
|
236
|
+
"status": self.status,
|
|
237
|
+
"applied_at": self.applied_at.isoformat() if self.applied_at else None,
|
|
238
|
+
"reversed_at": self.reversed_at.isoformat() if self.reversed_at else None,
|
|
239
|
+
"created_at": self.created_at.isoformat(),
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# Note: Demo fees and rates are in nanopy_bank/data/demo.py
|