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,308 @@
1
+ """
2
+ Banking Models - Account, Transaction, Customer
3
+ """
4
+
5
+ from dataclasses import dataclass, field
6
+ from datetime import datetime
7
+ from decimal import Decimal
8
+ from enum import Enum
9
+ from typing import Optional, List
10
+ import uuid
11
+ import hashlib
12
+
13
+
14
+ class TransactionType(Enum):
15
+ """Transaction types"""
16
+ CREDIT = "credit" # Money in
17
+ DEBIT = "debit" # Money out
18
+ TRANSFER = "transfer"
19
+ CREDIT_TRANSFER = "credit_transfer" # Virement
20
+ DIRECT_DEBIT = "direct_debit" # Prelevement
21
+ SEPA_CREDIT = "sepa_credit"
22
+ SEPA_DEBIT = "sepa_debit"
23
+ CARD_PAYMENT = "card_payment"
24
+ ATM_WITHDRAWAL = "atm_withdrawal"
25
+ FEE = "fee"
26
+ INTEREST = "interest"
27
+ REFUND = "refund"
28
+
29
+
30
+ class AccountType(Enum):
31
+ """Account types"""
32
+ CHECKING = "checking" # Compte courant
33
+ SAVINGS = "savings" # Livret d'épargne
34
+ BUSINESS = "business" # Compte professionnel
35
+ JOINT = "joint" # Compte joint
36
+
37
+
38
+ class AccountStatus(Enum):
39
+ """Account status"""
40
+ ACTIVE = "active"
41
+ BLOCKED = "blocked"
42
+ CLOSED = "closed"
43
+ PENDING = "pending"
44
+
45
+
46
+ class Currency(Enum):
47
+ """Supported currencies"""
48
+ EUR = "EUR"
49
+ USD = "USD"
50
+ GBP = "GBP"
51
+ CHF = "CHF"
52
+
53
+
54
+ class CardType(Enum):
55
+ """Card types"""
56
+ DEBIT = "debit"
57
+ CREDIT = "credit"
58
+ PREPAID = "prepaid"
59
+ BUSINESS = "business"
60
+
61
+
62
+ class CardStatus(Enum):
63
+ """Card status"""
64
+ ACTIVE = "active"
65
+ BLOCKED = "blocked"
66
+ EXPIRED = "expired"
67
+ PENDING = "pending"
68
+
69
+
70
+ @dataclass
71
+ class Customer:
72
+ """Bank customer"""
73
+ customer_id: str = field(default_factory=lambda: str(uuid.uuid4())[:8].upper())
74
+ first_name: str = ""
75
+ last_name: str = ""
76
+ email: str = ""
77
+ phone: str = ""
78
+ address: str = ""
79
+ city: str = ""
80
+ postal_code: str = ""
81
+ country: str = "FR"
82
+ birth_date: Optional[datetime] = None
83
+ created_at: datetime = field(default_factory=datetime.now)
84
+
85
+ @property
86
+ def full_name(self) -> str:
87
+ return f"{self.first_name} {self.last_name}"
88
+
89
+ def to_dict(self) -> dict:
90
+ return {
91
+ "customer_id": self.customer_id,
92
+ "first_name": self.first_name,
93
+ "last_name": self.last_name,
94
+ "full_name": self.full_name,
95
+ "email": self.email,
96
+ "phone": self.phone,
97
+ "address": self.address,
98
+ "city": self.city,
99
+ "postal_code": self.postal_code,
100
+ "country": self.country,
101
+ "birth_date": self.birth_date.isoformat() if self.birth_date else None,
102
+ "created_at": self.created_at.isoformat(),
103
+ }
104
+
105
+
106
+ @dataclass
107
+ class Account:
108
+ """Bank account"""
109
+ iban: str = ""
110
+ bic: str = "NANPFRPP" # Default BIC for NanoPy Bank
111
+ account_type: AccountType = AccountType.CHECKING
112
+ currency: Currency = Currency.EUR
113
+ balance: Decimal = Decimal("0.00")
114
+ available_balance: Decimal = Decimal("0.00")
115
+ overdraft_limit: Decimal = Decimal("0.00")
116
+ customer_id: str = ""
117
+ account_name: str = ""
118
+ status: AccountStatus = AccountStatus.ACTIVE
119
+ created_at: datetime = field(default_factory=datetime.now)
120
+ last_transaction: Optional[datetime] = None
121
+
122
+ # Card info (optional)
123
+ card_number: Optional[str] = None
124
+ card_expiry: Optional[str] = None
125
+ card_cvv: Optional[str] = None
126
+
127
+ def __post_init__(self):
128
+ if not self.iban:
129
+ self.iban = self._generate_iban()
130
+ if not self.account_name:
131
+ self.account_name = f"Compte {self.account_type.value}"
132
+
133
+ def _generate_iban(self) -> str:
134
+ """Generate a valid French IBAN"""
135
+ # Bank code (5 digits) + Branch code (5 digits) + Account number (11 digits) + Key (2 digits)
136
+ bank_code = "30001" # NanoPy Bank code
137
+ branch_code = "00001"
138
+ account_num = str(uuid.uuid4().int)[:11].zfill(11)
139
+
140
+ # Calculate check digits (simplified)
141
+ bban = f"{bank_code}{branch_code}{account_num}00"
142
+ # Convert letters to numbers (A=10, B=11, etc.)
143
+ numeric = ""
144
+ for char in f"{bban}FR00":
145
+ if char.isalpha():
146
+ numeric += str(ord(char.upper()) - 55)
147
+ else:
148
+ numeric += char
149
+
150
+ check = 98 - (int(numeric) % 97)
151
+ return f"FR{check:02d}{bank_code}{branch_code}{account_num}00"
152
+
153
+ def can_debit(self, amount: Decimal) -> bool:
154
+ """Check if account can be debited"""
155
+ return self.status == AccountStatus.ACTIVE and \
156
+ (self.available_balance + self.overdraft_limit) >= amount
157
+
158
+ def to_dict(self) -> dict:
159
+ return {
160
+ "iban": self.iban,
161
+ "iban_formatted": self.format_iban(),
162
+ "bic": self.bic,
163
+ "account_type": self.account_type.value,
164
+ "currency": self.currency.value,
165
+ "balance": str(self.balance),
166
+ "available_balance": str(self.available_balance),
167
+ "overdraft_limit": str(self.overdraft_limit),
168
+ "customer_id": self.customer_id,
169
+ "account_name": self.account_name,
170
+ "status": self.status.value,
171
+ "created_at": self.created_at.isoformat(),
172
+ "last_transaction": self.last_transaction.isoformat() if self.last_transaction else None,
173
+ }
174
+
175
+ def format_iban(self) -> str:
176
+ """Format IBAN with spaces every 4 characters"""
177
+ return " ".join([self.iban[i:i+4] for i in range(0, len(self.iban), 4)])
178
+
179
+
180
+ @dataclass
181
+ class Transaction:
182
+ """Bank transaction"""
183
+ transaction_id: str = field(default_factory=lambda: str(uuid.uuid4()))
184
+ reference: str = field(default_factory=lambda: f"TXN{datetime.now().strftime('%Y%m%d%H%M%S')}{uuid.uuid4().hex[:6].upper()}")
185
+ transaction_type: TransactionType = TransactionType.CREDIT
186
+ amount: Decimal = Decimal("0.00")
187
+ currency: Currency = Currency.EUR
188
+
189
+ # Accounts
190
+ from_iban: Optional[str] = None
191
+ to_iban: Optional[str] = None
192
+ account_iban: str = "" # The account this transaction belongs to
193
+
194
+ # Details
195
+ label: str = ""
196
+ description: str = ""
197
+ category: str = ""
198
+
199
+ # Counterparty
200
+ counterparty_name: str = ""
201
+ counterparty_iban: Optional[str] = None
202
+ counterparty_bic: Optional[str] = None
203
+
204
+ # Metadata
205
+ status: str = "completed" # pending, completed, failed, cancelled
206
+ created_at: datetime = field(default_factory=datetime.now)
207
+ executed_at: Optional[datetime] = None
208
+ value_date: Optional[datetime] = None
209
+
210
+ # SEPA specific
211
+ end_to_end_id: Optional[str] = None
212
+ mandate_id: Optional[str] = None
213
+
214
+ # Balance after transaction
215
+ balance_after: Optional[Decimal] = None
216
+
217
+ def __post_init__(self):
218
+ if not self.end_to_end_id:
219
+ self.end_to_end_id = self.reference
220
+ if not self.executed_at:
221
+ self.executed_at = self.created_at
222
+ if not self.value_date:
223
+ self.value_date = self.created_at
224
+
225
+ @property
226
+ def is_credit(self) -> bool:
227
+ return self.transaction_type in [
228
+ TransactionType.CREDIT,
229
+ TransactionType.SEPA_CREDIT,
230
+ TransactionType.REFUND,
231
+ TransactionType.INTEREST,
232
+ ]
233
+
234
+ @property
235
+ def is_debit(self) -> bool:
236
+ return not self.is_credit
237
+
238
+ @property
239
+ def signed_amount(self) -> Decimal:
240
+ """Return amount with sign (positive for credit, negative for debit)"""
241
+ return self.amount if self.is_credit else -self.amount
242
+
243
+ def to_dict(self) -> dict:
244
+ return {
245
+ "transaction_id": self.transaction_id,
246
+ "reference": self.reference,
247
+ "transaction_type": self.transaction_type.value,
248
+ "amount": str(self.amount),
249
+ "signed_amount": str(self.signed_amount),
250
+ "currency": self.currency.value,
251
+ "from_iban": self.from_iban,
252
+ "to_iban": self.to_iban,
253
+ "account_iban": self.account_iban,
254
+ "label": self.label,
255
+ "description": self.description,
256
+ "category": self.category,
257
+ "counterparty_name": self.counterparty_name,
258
+ "counterparty_iban": self.counterparty_iban,
259
+ "counterparty_bic": self.counterparty_bic,
260
+ "status": self.status,
261
+ "created_at": self.created_at.isoformat(),
262
+ "executed_at": self.executed_at.isoformat() if self.executed_at else None,
263
+ "value_date": self.value_date.isoformat() if self.value_date else None,
264
+ "end_to_end_id": self.end_to_end_id,
265
+ "mandate_id": self.mandate_id,
266
+ "balance_after": str(self.balance_after) if self.balance_after else None,
267
+ "is_credit": self.is_credit,
268
+ "is_debit": self.is_debit,
269
+ }
270
+
271
+
272
+ @dataclass
273
+ class Card:
274
+ """Bank card"""
275
+ card_id: str = field(default_factory=lambda: str(uuid.uuid4()))
276
+ card_number: str = "" # Last 4 digits only for display
277
+ card_type: str = "visa" # visa, mastercard
278
+ expiry_month: int = 12
279
+ expiry_year: int = 2027
280
+ status: str = "active" # active, blocked, expired
281
+ account_iban: str = ""
282
+ daily_limit: Decimal = Decimal("1000.00")
283
+ monthly_limit: Decimal = Decimal("5000.00")
284
+ contactless_enabled: bool = True
285
+ online_payments_enabled: bool = True
286
+
287
+ def __post_init__(self):
288
+ if not self.card_number:
289
+ # Generate last 4 digits
290
+ self.card_number = f"**** **** **** {uuid.uuid4().int % 10000:04d}"
291
+
292
+ @property
293
+ def expiry(self) -> str:
294
+ return f"{self.expiry_month:02d}/{self.expiry_year % 100:02d}"
295
+
296
+ def to_dict(self) -> dict:
297
+ return {
298
+ "card_id": self.card_id,
299
+ "card_number": self.card_number,
300
+ "card_type": self.card_type,
301
+ "expiry": self.expiry,
302
+ "status": self.status,
303
+ "account_iban": self.account_iban,
304
+ "daily_limit": str(self.daily_limit),
305
+ "monthly_limit": str(self.monthly_limit),
306
+ "contactless_enabled": self.contactless_enabled,
307
+ "online_payments_enabled": self.online_payments_enabled,
308
+ }
@@ -0,0 +1,300 @@
1
+ """
2
+ Banking Products - Loans, Credits, Insurance, Savings
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 LoanType(Enum):
14
+ """Types of loans"""
15
+ PERSONAL = "personal" # Prêt personnel
16
+ MORTGAGE = "mortgage" # Prêt immobilier
17
+ AUTO = "auto" # Crédit auto
18
+ STUDENT = "student" # Prêt étudiant
19
+ BUSINESS = "business" # Prêt professionnel
20
+ REVOLVING = "revolving" # Crédit renouvelable
21
+
22
+
23
+ class LoanStatus(Enum):
24
+ """Loan status"""
25
+ PENDING = "pending" # En attente d'approbation
26
+ APPROVED = "approved" # Approuvé
27
+ ACTIVE = "active" # En cours
28
+ LATE = "late" # En retard
29
+ DEFAULT = "default" # Défaut de paiement
30
+ PAID_OFF = "paid_off" # Remboursé
31
+ CANCELLED = "cancelled" # Annulé
32
+
33
+
34
+ class InsuranceType(Enum):
35
+ """Types of insurance"""
36
+ LIFE = "life" # Assurance vie
37
+ HOME = "home" # Assurance habitation
38
+ AUTO = "auto" # Assurance auto
39
+ HEALTH = "health" # Mutuelle santé
40
+ CREDIT = "credit" # Assurance emprunteur
41
+ SAVINGS = "savings" # Assurance épargne
42
+
43
+
44
+ class SavingsType(Enum):
45
+ """Types of savings products"""
46
+ LIVRET_A = "livret_a" # Livret A
47
+ LDDS = "ldds" # Livret Développement Durable
48
+ LEP = "lep" # Livret d'Épargne Populaire
49
+ PEL = "pel" # Plan Épargne Logement
50
+ CEL = "cel" # Compte Épargne Logement
51
+ TERM_DEPOSIT = "term_deposit" # Dépôt à terme
52
+ PEA = "pea" # Plan d'Épargne en Actions
53
+
54
+
55
+ @dataclass
56
+ class Loan:
57
+ """
58
+ Loan / Credit product
59
+ """
60
+ loan_id: str = field(default_factory=lambda: f"LN{uuid.uuid4().hex[:10].upper()}")
61
+ customer_id: str = ""
62
+ account_iban: str = "" # Account for repayments
63
+
64
+ # Loan details
65
+ loan_type: LoanType = LoanType.PERSONAL
66
+ purpose: str = "" # Description of loan purpose
67
+
68
+ # Amounts
69
+ principal: Decimal = Decimal("0.00") # Montant emprunté
70
+ interest_rate: Decimal = Decimal("0.00") # Taux annuel (ex: 3.5 = 3.5%)
71
+ total_interest: Decimal = Decimal("0.00") # Total des intérêts
72
+ total_amount: Decimal = Decimal("0.00") # Principal + intérêts
73
+
74
+ # Current state
75
+ remaining_principal: Decimal = Decimal("0.00")
76
+ remaining_interest: Decimal = Decimal("0.00")
77
+ paid_principal: Decimal = Decimal("0.00")
78
+ paid_interest: Decimal = Decimal("0.00")
79
+
80
+ # Schedule
81
+ duration_months: int = 12
82
+ monthly_payment: Decimal = Decimal("0.00")
83
+ start_date: Optional[date] = None
84
+ end_date: Optional[date] = None
85
+ next_payment_date: Optional[date] = None
86
+
87
+ # Status
88
+ status: LoanStatus = LoanStatus.PENDING
89
+ payments_made: int = 0
90
+ payments_late: int = 0
91
+ days_overdue: int = 0
92
+
93
+ # Insurance
94
+ has_insurance: bool = False
95
+ insurance_premium: Decimal = Decimal("0.00")
96
+
97
+ # Collateral (for secured loans)
98
+ collateral_type: str = ""
99
+ collateral_value: Decimal = Decimal("0.00")
100
+
101
+ # Metadata
102
+ created_at: datetime = field(default_factory=datetime.now)
103
+ approved_at: Optional[datetime] = None
104
+ approved_by: str = ""
105
+
106
+ def __post_init__(self):
107
+ if self.principal > 0 and self.remaining_principal == 0:
108
+ self.remaining_principal = self.principal
109
+ self._calculate_loan()
110
+
111
+ def _calculate_loan(self):
112
+ """Calculate loan schedule"""
113
+ if self.principal <= 0 or self.duration_months <= 0:
114
+ return
115
+
116
+ # Monthly interest rate
117
+ monthly_rate = self.interest_rate / 100 / 12
118
+
119
+ if monthly_rate > 0:
120
+ # Amortization formula
121
+ self.monthly_payment = self.principal * (
122
+ monthly_rate * (1 + monthly_rate) ** self.duration_months
123
+ ) / ((1 + monthly_rate) ** self.duration_months - 1)
124
+ else:
125
+ self.monthly_payment = self.principal / self.duration_months
126
+
127
+ self.total_amount = self.monthly_payment * self.duration_months
128
+ self.total_interest = self.total_amount - self.principal
129
+ self.remaining_interest = self.total_interest
130
+
131
+ def to_dict(self) -> dict:
132
+ return {
133
+ "loan_id": self.loan_id,
134
+ "customer_id": self.customer_id,
135
+ "account_iban": self.account_iban,
136
+ "loan_type": self.loan_type.value,
137
+ "purpose": self.purpose,
138
+ "principal": str(self.principal),
139
+ "interest_rate": str(self.interest_rate),
140
+ "total_interest": str(self.total_interest),
141
+ "total_amount": str(self.total_amount),
142
+ "remaining_principal": str(self.remaining_principal),
143
+ "remaining_interest": str(self.remaining_interest),
144
+ "paid_principal": str(self.paid_principal),
145
+ "paid_interest": str(self.paid_interest),
146
+ "duration_months": self.duration_months,
147
+ "monthly_payment": str(self.monthly_payment),
148
+ "start_date": self.start_date.isoformat() if self.start_date else None,
149
+ "end_date": self.end_date.isoformat() if self.end_date else None,
150
+ "next_payment_date": self.next_payment_date.isoformat() if self.next_payment_date else None,
151
+ "status": self.status.value,
152
+ "payments_made": self.payments_made,
153
+ "payments_late": self.payments_late,
154
+ "days_overdue": self.days_overdue,
155
+ "has_insurance": self.has_insurance,
156
+ "insurance_premium": str(self.insurance_premium),
157
+ "collateral_type": self.collateral_type,
158
+ "collateral_value": str(self.collateral_value),
159
+ "created_at": self.created_at.isoformat(),
160
+ "approved_at": self.approved_at.isoformat() if self.approved_at else None,
161
+ "approved_by": self.approved_by,
162
+ "progress_percent": self.progress_percent,
163
+ }
164
+
165
+ @property
166
+ def progress_percent(self) -> float:
167
+ """Percentage of loan paid off"""
168
+ if self.total_amount <= 0:
169
+ return 0
170
+ paid = self.paid_principal + self.paid_interest
171
+ return min(100, float(paid / self.total_amount * 100))
172
+
173
+
174
+ @dataclass
175
+ class Insurance:
176
+ """
177
+ Insurance product
178
+ """
179
+ insurance_id: str = field(default_factory=lambda: f"INS{uuid.uuid4().hex[:10].upper()}")
180
+ customer_id: str = ""
181
+ policy_number: str = field(default_factory=lambda: f"POL{uuid.uuid4().hex[:8].upper()}")
182
+
183
+ # Insurance details
184
+ insurance_type: InsuranceType = InsuranceType.HOME
185
+ provider: str = "NanoPy Assurance"
186
+ product_name: str = ""
187
+
188
+ # Coverage
189
+ coverage_amount: Decimal = Decimal("0.00")
190
+ deductible: Decimal = Decimal("0.00") # Franchise
191
+ coverage_details: str = ""
192
+
193
+ # Premium
194
+ premium_amount: Decimal = Decimal("0.00")
195
+ premium_frequency: str = "monthly" # monthly, quarterly, yearly
196
+ account_iban: str = "" # For premium payments
197
+
198
+ # Dates
199
+ start_date: date = field(default_factory=date.today)
200
+ end_date: Optional[date] = None
201
+ next_payment_date: Optional[date] = None
202
+
203
+ # Status
204
+ status: str = "active" # active, suspended, cancelled, expired
205
+ is_auto_renew: bool = True
206
+
207
+ # Claims
208
+ claims_count: int = 0
209
+ total_claimed: Decimal = Decimal("0.00")
210
+
211
+ # Metadata
212
+ created_at: datetime = field(default_factory=datetime.now)
213
+
214
+ def to_dict(self) -> dict:
215
+ return {
216
+ "insurance_id": self.insurance_id,
217
+ "customer_id": self.customer_id,
218
+ "policy_number": self.policy_number,
219
+ "insurance_type": self.insurance_type.value,
220
+ "provider": self.provider,
221
+ "product_name": self.product_name,
222
+ "coverage_amount": str(self.coverage_amount),
223
+ "deductible": str(self.deductible),
224
+ "coverage_details": self.coverage_details,
225
+ "premium_amount": str(self.premium_amount),
226
+ "premium_frequency": self.premium_frequency,
227
+ "account_iban": self.account_iban,
228
+ "start_date": self.start_date.isoformat(),
229
+ "end_date": self.end_date.isoformat() if self.end_date else None,
230
+ "next_payment_date": self.next_payment_date.isoformat() if self.next_payment_date else None,
231
+ "status": self.status,
232
+ "is_auto_renew": self.is_auto_renew,
233
+ "claims_count": self.claims_count,
234
+ "total_claimed": str(self.total_claimed),
235
+ "created_at": self.created_at.isoformat(),
236
+ }
237
+
238
+
239
+ @dataclass
240
+ class SavingsProduct:
241
+ """
242
+ Savings product (Livret A, PEL, etc.)
243
+ """
244
+ savings_id: str = field(default_factory=lambda: f"SAV{uuid.uuid4().hex[:10].upper()}")
245
+ customer_id: str = ""
246
+ account_iban: str = "" # The savings account
247
+
248
+ # Product details
249
+ savings_type: SavingsType = SavingsType.LIVRET_A
250
+ product_name: str = ""
251
+
252
+ # Interest
253
+ interest_rate: Decimal = Decimal("3.00") # Current rate
254
+ interest_frequency: str = "yearly" # When interest is paid
255
+ interest_earned: Decimal = Decimal("0.00")
256
+ last_interest_date: Optional[date] = None
257
+
258
+ # Limits
259
+ min_balance: Decimal = Decimal("0.00")
260
+ max_balance: Decimal = Decimal("22950.00") # Livret A ceiling
261
+ min_deposit: Decimal = Decimal("10.00")
262
+ max_withdrawal_per_month: Optional[Decimal] = None
263
+
264
+ # Tax
265
+ is_tax_exempt: bool = True # Livret A, LDDS are tax-free
266
+ tax_rate: Decimal = Decimal("0.00") # Flat tax rate if applicable
267
+
268
+ # Status
269
+ status: str = "active"
270
+ opened_date: date = field(default_factory=date.today)
271
+ closed_date: Optional[date] = None
272
+
273
+ # Metadata
274
+ created_at: datetime = field(default_factory=datetime.now)
275
+
276
+ def to_dict(self) -> dict:
277
+ return {
278
+ "savings_id": self.savings_id,
279
+ "customer_id": self.customer_id,
280
+ "account_iban": self.account_iban,
281
+ "savings_type": self.savings_type.value,
282
+ "product_name": self.product_name,
283
+ "interest_rate": str(self.interest_rate),
284
+ "interest_frequency": self.interest_frequency,
285
+ "interest_earned": str(self.interest_earned),
286
+ "last_interest_date": self.last_interest_date.isoformat() if self.last_interest_date else None,
287
+ "min_balance": str(self.min_balance),
288
+ "max_balance": str(self.max_balance),
289
+ "min_deposit": str(self.min_deposit),
290
+ "max_withdrawal_per_month": str(self.max_withdrawal_per_month) if self.max_withdrawal_per_month else None,
291
+ "is_tax_exempt": self.is_tax_exempt,
292
+ "tax_rate": str(self.tax_rate),
293
+ "status": self.status,
294
+ "opened_date": self.opened_date.isoformat(),
295
+ "closed_date": self.closed_date.isoformat() if self.closed_date else None,
296
+ "created_at": self.created_at.isoformat(),
297
+ }
298
+
299
+
300
+ # Note: Demo savings products are in nanopy_bank/data/demo.py
@@ -0,0 +1,31 @@
1
+ """
2
+ Data module - Test and demo data
3
+ """
4
+
5
+ from .demo import (
6
+ create_demo_bank,
7
+ DEMO_CUSTOMERS,
8
+ DEMO_ACCOUNTS,
9
+ DEMO_TRANSACTIONS,
10
+ DEMO_CARDS,
11
+ DEMO_BENEFICIARIES,
12
+ DEMO_STANDING_ORDERS,
13
+ DEMO_FEES,
14
+ DEMO_RATES,
15
+ DEMO_BRANCHES,
16
+ DEMO_EMPLOYEES,
17
+ )
18
+
19
+ __all__ = [
20
+ "create_demo_bank",
21
+ "DEMO_CUSTOMERS",
22
+ "DEMO_ACCOUNTS",
23
+ "DEMO_TRANSACTIONS",
24
+ "DEMO_CARDS",
25
+ "DEMO_BENEFICIARIES",
26
+ "DEMO_STANDING_ORDERS",
27
+ "DEMO_FEES",
28
+ "DEMO_RATES",
29
+ "DEMO_BRANCHES",
30
+ "DEMO_EMPLOYEES",
31
+ ]