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.
Files changed (47) hide show
  1. nanopy_bank/__init__.py +20 -0
  2. nanopy_bank/api/__init__.py +10 -0
  3. nanopy_bank/api/server.py +242 -0
  4. nanopy_bank/app.py +282 -0
  5. nanopy_bank/cli.py +152 -0
  6. nanopy_bank/core/__init__.py +85 -0
  7. nanopy_bank/core/audit.py +404 -0
  8. nanopy_bank/core/auth.py +306 -0
  9. nanopy_bank/core/bank.py +407 -0
  10. nanopy_bank/core/beneficiary.py +258 -0
  11. nanopy_bank/core/branch.py +319 -0
  12. nanopy_bank/core/fees.py +243 -0
  13. nanopy_bank/core/holding.py +416 -0
  14. nanopy_bank/core/models.py +308 -0
  15. nanopy_bank/core/products.py +300 -0
  16. nanopy_bank/data/__init__.py +31 -0
  17. nanopy_bank/data/demo.py +846 -0
  18. nanopy_bank/documents/__init__.py +11 -0
  19. nanopy_bank/documents/statement.py +304 -0
  20. nanopy_bank/sepa/__init__.py +10 -0
  21. nanopy_bank/sepa/sepa.py +452 -0
  22. nanopy_bank/storage/__init__.py +11 -0
  23. nanopy_bank/storage/json_storage.py +127 -0
  24. nanopy_bank/storage/sqlite_storage.py +326 -0
  25. nanopy_bank/ui/__init__.py +14 -0
  26. nanopy_bank/ui/pages/__init__.py +33 -0
  27. nanopy_bank/ui/pages/accounts.py +85 -0
  28. nanopy_bank/ui/pages/advisor.py +140 -0
  29. nanopy_bank/ui/pages/audit.py +73 -0
  30. nanopy_bank/ui/pages/beneficiaries.py +115 -0
  31. nanopy_bank/ui/pages/branches.py +64 -0
  32. nanopy_bank/ui/pages/cards.py +36 -0
  33. nanopy_bank/ui/pages/common.py +18 -0
  34. nanopy_bank/ui/pages/dashboard.py +100 -0
  35. nanopy_bank/ui/pages/fees.py +60 -0
  36. nanopy_bank/ui/pages/holding.py +943 -0
  37. nanopy_bank/ui/pages/loans.py +105 -0
  38. nanopy_bank/ui/pages/login.py +174 -0
  39. nanopy_bank/ui/pages/sepa.py +118 -0
  40. nanopy_bank/ui/pages/settings.py +48 -0
  41. nanopy_bank/ui/pages/transfers.py +94 -0
  42. nanopy_bank/ui/pages.py +16 -0
  43. nanopy_bank-1.0.8.dist-info/METADATA +72 -0
  44. nanopy_bank-1.0.8.dist-info/RECORD +47 -0
  45. nanopy_bank-1.0.8.dist-info/WHEEL +5 -0
  46. nanopy_bank-1.0.8.dist-info/entry_points.txt +2 -0
  47. 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
+ }
@@ -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