beancount-gocardless 0.1.12__py3-none-any.whl → 0.1.14__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.
- beancount_gocardless/__main__.py +4 -0
- beancount_gocardless/cli.py +614 -113
- beancount_gocardless/client.py +13 -0
- beancount_gocardless/importer.py +145 -76
- beancount_gocardless/mock_client.py +220 -0
- beancount_gocardless/models.py +54 -9
- beancount_gocardless/utils.py +39 -0
- beancount_gocardless-0.1.14.dist-info/METADATA +252 -0
- beancount_gocardless-0.1.14.dist-info/RECORD +14 -0
- {beancount_gocardless-0.1.12.dist-info → beancount_gocardless-0.1.14.dist-info}/entry_points.txt +0 -1
- beancount_gocardless-0.1.14.dist-info/licenses/LICENSE +21 -0
- beancount_gocardless/tui.py +0 -669
- beancount_gocardless-0.1.12.dist-info/METADATA +0 -130
- beancount_gocardless-0.1.12.dist-info/RECORD +0 -12
- beancount_gocardless-0.1.12.dist-info/licenses/LICENSE +0 -24
- {beancount_gocardless-0.1.12.dist-info → beancount_gocardless-0.1.14.dist-info}/WHEEL +0 -0
beancount_gocardless/client.py
CHANGED
|
@@ -404,17 +404,30 @@ class GoCardlessClient:
|
|
|
404
404
|
|
|
405
405
|
def get_all_accounts(self) -> List[AccountInfo]:
|
|
406
406
|
"""Get all accounts from all requisitions"""
|
|
407
|
+
from datetime import datetime, timedelta
|
|
408
|
+
|
|
407
409
|
accounts = []
|
|
408
410
|
for req in self.get_requisitions():
|
|
409
411
|
for account_id in req.accounts:
|
|
410
412
|
try:
|
|
411
413
|
account = self.get_account(account_id)
|
|
412
414
|
account_dict = account.model_dump()
|
|
415
|
+
|
|
416
|
+
access_valid_days = req.access_valid_for_days or 90
|
|
417
|
+
created_date = datetime.fromisoformat(
|
|
418
|
+
req.created.replace("Z", "+00:00")
|
|
419
|
+
)
|
|
420
|
+
expiry_date = created_date + timedelta(days=access_valid_days)
|
|
421
|
+
is_expired = req.status == "EX"
|
|
422
|
+
|
|
413
423
|
account_dict.update(
|
|
414
424
|
{
|
|
415
425
|
"requisition_id": req.id,
|
|
416
426
|
"requisition_reference": req.reference,
|
|
417
427
|
"institution_id": req.institution_id,
|
|
428
|
+
"requisition_status": req.status,
|
|
429
|
+
"access_valid_until": expiry_date.isoformat(),
|
|
430
|
+
"is_expired": is_expired,
|
|
418
431
|
}
|
|
419
432
|
)
|
|
420
433
|
accounts.append(account_dict)
|
beancount_gocardless/importer.py
CHANGED
|
@@ -8,11 +8,23 @@ from beancount.core import amount, data, flags
|
|
|
8
8
|
from beancount.core.number import D
|
|
9
9
|
|
|
10
10
|
from .client import GoCardlessClient
|
|
11
|
-
from .models import BankTransaction, GoCardlessConfig
|
|
11
|
+
from .models import BankTransaction, GoCardlessConfig, AccountConfig
|
|
12
12
|
|
|
13
13
|
logger = logging.getLogger(__name__)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
def _flatten_dict(d: Dict[str, Any], prefix: str = "") -> Dict[str, Any]:
|
|
17
|
+
"""Recursively flatten a nested dict to dotted paths."""
|
|
18
|
+
result: Dict[str, Any] = {}
|
|
19
|
+
for key, value in d.items():
|
|
20
|
+
new_key = f"{prefix}.{key}" if prefix else key
|
|
21
|
+
if isinstance(value, dict):
|
|
22
|
+
result.update(_flatten_dict(value, new_key))
|
|
23
|
+
else:
|
|
24
|
+
result[new_key] = value
|
|
25
|
+
return result
|
|
26
|
+
|
|
27
|
+
|
|
16
28
|
class ReferenceDuplicatesComparator:
|
|
17
29
|
def __init__(self, refs: List[str] = ["ref"]) -> None:
|
|
18
30
|
self.refs = refs
|
|
@@ -38,6 +50,13 @@ class GoCardLessImporter(beangulp.Importer):
|
|
|
38
50
|
_client (Optional[GoCardlessClient]): Instance of the GoCardless API client.
|
|
39
51
|
"""
|
|
40
52
|
|
|
53
|
+
DEFAULT_METADATA_FIELDS: Dict[str, str] = {
|
|
54
|
+
"nordref": "transactionId",
|
|
55
|
+
"creditorName": "creditorName",
|
|
56
|
+
"debtorName": "debtorName",
|
|
57
|
+
"bookingDate": "bookingDate",
|
|
58
|
+
}
|
|
59
|
+
|
|
41
60
|
def __init__(self) -> None:
|
|
42
61
|
"""Initialize the GoCardLessImporter."""
|
|
43
62
|
logger.debug("Initializing GoCardLessImporter")
|
|
@@ -136,47 +155,45 @@ class GoCardLessImporter(beangulp.Importer):
|
|
|
136
155
|
)
|
|
137
156
|
|
|
138
157
|
def add_metadata(
|
|
139
|
-
self,
|
|
158
|
+
self,
|
|
159
|
+
transaction: BankTransaction,
|
|
160
|
+
custom_metadata: Dict[str, Any],
|
|
161
|
+
account_config: Optional[AccountConfig] = None,
|
|
140
162
|
) -> Dict[str, Any]:
|
|
141
|
-
"""
|
|
142
|
-
Extracts metadata from a transaction and returns it as a dictionary.
|
|
143
|
-
|
|
144
|
-
This method can be overridden in subclasses to customize metadata extraction.
|
|
145
|
-
|
|
146
|
-
Args:
|
|
147
|
-
transaction (BankTransaction): The transaction data from the API.
|
|
148
|
-
custom_metadata (Dict[str, Any]): Custom metadata from the config file.
|
|
149
|
-
|
|
150
|
-
Returns:
|
|
151
|
-
Dict[str, Any]: A dictionary of metadata key-value pairs.
|
|
152
|
-
"""
|
|
153
163
|
metakv: Dict[str, Any] = {}
|
|
154
164
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
165
|
+
exclude_fields: List[str] = []
|
|
166
|
+
custom_fields: Dict[str, str] = {}
|
|
167
|
+
|
|
168
|
+
if account_config is not None:
|
|
169
|
+
exclude_fields = account_config.exclude_default_metadata or []
|
|
170
|
+
custom_fields = account_config.metadata_fields or {}
|
|
171
|
+
|
|
172
|
+
# Start with defaults, merge with custom fields
|
|
173
|
+
fields = dict(self.DEFAULT_METADATA_FIELDS)
|
|
174
|
+
fields.update(custom_fields)
|
|
175
|
+
|
|
176
|
+
# Remove excluded fields
|
|
177
|
+
for key in exclude_fields:
|
|
178
|
+
fields.pop(key, None)
|
|
179
|
+
|
|
180
|
+
for out_key, gcl_path in fields.items():
|
|
181
|
+
if gcl_path is None:
|
|
182
|
+
continue
|
|
183
|
+
val = self._get_gcl_path(transaction, gcl_path)
|
|
184
|
+
if val is None:
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
if (
|
|
188
|
+
out_key == "original"
|
|
189
|
+
and hasattr(val, "currency")
|
|
190
|
+
and hasattr(val, "amount")
|
|
191
|
+
):
|
|
192
|
+
metakv[out_key] = f"{val.currency} {val.amount}"
|
|
193
|
+
else:
|
|
194
|
+
metakv[out_key] = val
|
|
177
195
|
|
|
178
196
|
metakv.update(custom_metadata)
|
|
179
|
-
|
|
180
197
|
return metakv
|
|
181
198
|
|
|
182
199
|
def get_narration(self, transaction: BankTransaction) -> str:
|
|
@@ -264,6 +281,7 @@ class GoCardLessImporter(beangulp.Importer):
|
|
|
264
281
|
status: str,
|
|
265
282
|
asset_account: str,
|
|
266
283
|
custom_metadata: Dict[str, Any],
|
|
284
|
+
account_config: Optional[AccountConfig] = None,
|
|
267
285
|
) -> Optional[data.Transaction]:
|
|
268
286
|
"""
|
|
269
287
|
Creates a Beancount transaction entry from a GoCardless transaction.
|
|
@@ -275,6 +293,7 @@ class GoCardLessImporter(beangulp.Importer):
|
|
|
275
293
|
status (str): The transaction status ('booked' or 'pending').
|
|
276
294
|
asset_account (str): The Beancount asset account.
|
|
277
295
|
custom_metadata (Dict[str, Any]): Custom metadata from config
|
|
296
|
+
account_config (Optional[AccountConfig]): Account configuration for metadata options.
|
|
278
297
|
|
|
279
298
|
Returns:
|
|
280
299
|
Optional[data.Transaction]: The created Beancount transaction entry, or None if date is invalid.
|
|
@@ -282,7 +301,7 @@ class GoCardLessImporter(beangulp.Importer):
|
|
|
282
301
|
logger.debug(
|
|
283
302
|
"Creating entry for transaction %s (%s)", transaction.transaction_id, status
|
|
284
303
|
)
|
|
285
|
-
metakv = self.add_metadata(transaction, custom_metadata)
|
|
304
|
+
metakv = self.add_metadata(transaction, custom_metadata, account_config)
|
|
286
305
|
meta = data.new_metadata("", 0, metakv)
|
|
287
306
|
|
|
288
307
|
trx_date = self.get_transaction_date(transaction)
|
|
@@ -377,7 +396,7 @@ class GoCardLessImporter(beangulp.Importer):
|
|
|
377
396
|
skipped = 0
|
|
378
397
|
for transaction, status in all_transactions:
|
|
379
398
|
entry = self.create_transaction_entry(
|
|
380
|
-
transaction, status, asset_account, custom_metadata
|
|
399
|
+
transaction, status, asset_account, custom_metadata, account
|
|
381
400
|
)
|
|
382
401
|
if entry is not None:
|
|
383
402
|
entries.append(entry)
|
|
@@ -400,36 +419,61 @@ class GoCardLessImporter(beangulp.Importer):
|
|
|
400
419
|
for b in balances.balances
|
|
401
420
|
],
|
|
402
421
|
)
|
|
403
|
-
expected_balance = None
|
|
404
|
-
other_balances = []
|
|
405
|
-
for bal in balances.balances:
|
|
406
|
-
if bal.balance_type == "expected":
|
|
407
|
-
expected_balance = bal
|
|
408
|
-
else:
|
|
409
|
-
other_balances.append(bal)
|
|
410
422
|
|
|
411
|
-
|
|
423
|
+
# Prioritized balance selection
|
|
424
|
+
PRIORITY = {
|
|
425
|
+
"expected": 0,
|
|
426
|
+
"closingBooked": 1,
|
|
427
|
+
"interimBooked": 2,
|
|
428
|
+
"interimAvailable": 3,
|
|
429
|
+
"openingBooked": 4,
|
|
430
|
+
}
|
|
431
|
+
if account.preferred_balance_type:
|
|
432
|
+
PRIORITY[account.preferred_balance_type] = -1
|
|
433
|
+
|
|
434
|
+
# Sort balances based on priority, with unknown types at the end
|
|
435
|
+
sorted_balances = sorted(
|
|
436
|
+
balances.balances, key=lambda b: PRIORITY.get(b.balance_type, 99)
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
if sorted_balances:
|
|
440
|
+
selected_balance = sorted_balances[0]
|
|
412
441
|
balance_amount = amount.Amount(
|
|
413
|
-
D(str(
|
|
414
|
-
|
|
442
|
+
D(str(selected_balance.balance_amount.amount)),
|
|
443
|
+
selected_balance.balance_amount.currency,
|
|
415
444
|
)
|
|
445
|
+
|
|
446
|
+
# Determine balance date
|
|
447
|
+
if selected_balance.reference_date:
|
|
448
|
+
try:
|
|
449
|
+
balance_date = date.fromisoformat(
|
|
450
|
+
selected_balance.reference_date
|
|
451
|
+
) + timedelta(days=1)
|
|
452
|
+
except ValueError:
|
|
453
|
+
balance_date = date.today() + timedelta(days=1)
|
|
454
|
+
else:
|
|
455
|
+
balance_date = date.today() + timedelta(days=1)
|
|
456
|
+
|
|
416
457
|
balance_meta = {}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
]
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
458
|
+
|
|
459
|
+
# Collect all distinct balance values for metadata
|
|
460
|
+
distinct_details = []
|
|
461
|
+
seen_values = set()
|
|
462
|
+
for b in sorted_balances:
|
|
463
|
+
val_str = f"{b.balance_amount.amount} {b.balance_amount.currency}"
|
|
464
|
+
if val_str not in seen_values:
|
|
465
|
+
distinct_details.append(f"{b.balance_type}: {val_str}")
|
|
466
|
+
seen_values.add(val_str)
|
|
467
|
+
|
|
468
|
+
balance_meta["detail"] = " / ".join(distinct_details)
|
|
469
|
+
|
|
427
470
|
# Include custom metadata from config for consistency with transactions
|
|
428
471
|
balance_meta.update(custom_metadata)
|
|
429
472
|
meta = data.new_metadata("", 0, balance_meta)
|
|
473
|
+
|
|
430
474
|
balance_entry = data.Balance(
|
|
431
475
|
meta=meta,
|
|
432
|
-
date=
|
|
476
|
+
date=balance_date,
|
|
433
477
|
account=asset_account,
|
|
434
478
|
amount=balance_amount,
|
|
435
479
|
tolerance=None,
|
|
@@ -437,26 +481,13 @@ class GoCardLessImporter(beangulp.Importer):
|
|
|
437
481
|
)
|
|
438
482
|
entries.append(balance_entry)
|
|
439
483
|
logger.debug(
|
|
440
|
-
"Added balance assertion for account %s using
|
|
484
|
+
"Added balance assertion for account %s using %s balance: %s %s",
|
|
441
485
|
account_id,
|
|
486
|
+
selected_balance.balance_type,
|
|
442
487
|
balance_amount,
|
|
443
|
-
|
|
488
|
+
balance_date,
|
|
444
489
|
)
|
|
445
490
|
|
|
446
|
-
# Log other balances if they differ from expected
|
|
447
|
-
expected_amount = D(str(expected_balance.balance_amount.amount))
|
|
448
|
-
for bal in other_balances:
|
|
449
|
-
other_amount = D(str(bal.balance_amount.amount))
|
|
450
|
-
if other_amount != expected_amount:
|
|
451
|
-
logger.info(
|
|
452
|
-
"Account %s has different balance for type %s: %s %s vs expected %s",
|
|
453
|
-
account_id,
|
|
454
|
-
bal.balance_type,
|
|
455
|
-
other_amount,
|
|
456
|
-
bal.balance_amount.currency,
|
|
457
|
-
expected_amount,
|
|
458
|
-
)
|
|
459
|
-
|
|
460
491
|
logger.info(
|
|
461
492
|
"Processed %d total transactions across %d accounts, created %d entries",
|
|
462
493
|
total_transactions,
|
|
@@ -465,4 +496,42 @@ class GoCardLessImporter(beangulp.Importer):
|
|
|
465
496
|
)
|
|
466
497
|
return entries
|
|
467
498
|
|
|
499
|
+
def _get_gcl_path(self, root: Any, dotted: str) -> Any:
|
|
500
|
+
cur: Any = root
|
|
501
|
+
for seg in dotted.split("."):
|
|
502
|
+
if cur is None:
|
|
503
|
+
return None
|
|
504
|
+
|
|
505
|
+
if isinstance(cur, list):
|
|
506
|
+
if not seg.isdigit():
|
|
507
|
+
return None
|
|
508
|
+
idx = int(seg)
|
|
509
|
+
if idx >= len(cur):
|
|
510
|
+
return None
|
|
511
|
+
cur = cur[idx]
|
|
512
|
+
continue
|
|
513
|
+
|
|
514
|
+
if isinstance(cur, dict):
|
|
515
|
+
cur = cur.get(seg)
|
|
516
|
+
continue
|
|
517
|
+
|
|
518
|
+
if hasattr(cur, seg):
|
|
519
|
+
cur = getattr(cur, seg)
|
|
520
|
+
continue
|
|
521
|
+
|
|
522
|
+
if hasattr(type(cur), "model_fields"):
|
|
523
|
+
model_fields = type(cur).model_fields
|
|
524
|
+
name = next(
|
|
525
|
+
(n for n, f in model_fields.items() if f.alias == seg), None
|
|
526
|
+
)
|
|
527
|
+
if name and hasattr(cur, name):
|
|
528
|
+
cur = getattr(cur, name)
|
|
529
|
+
continue
|
|
530
|
+
|
|
531
|
+
return None
|
|
532
|
+
|
|
533
|
+
if isinstance(cur, (dict, list)):
|
|
534
|
+
return None
|
|
535
|
+
return cur
|
|
536
|
+
|
|
468
537
|
cmp = ReferenceDuplicatesComparator(["nordref"])
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mock client for testing without real API calls.
|
|
3
|
+
Returns synthetic demo data for CLI testing.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Optional, List, Dict, Any
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
|
|
10
|
+
from .client import GoCardlessClient
|
|
11
|
+
from .models import (
|
|
12
|
+
Institution,
|
|
13
|
+
Requisition,
|
|
14
|
+
Account,
|
|
15
|
+
AccountBalance,
|
|
16
|
+
AccountInfo,
|
|
17
|
+
BalanceSchema,
|
|
18
|
+
BalanceAmountSchema,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MockGoCardlessClient(GoCardlessClient):
|
|
25
|
+
"""Mock client that returns synthetic demo data instead of making API calls."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
secret_id: str,
|
|
30
|
+
secret_key: str,
|
|
31
|
+
cache_options: Optional[Dict[str, Any]] = None,
|
|
32
|
+
):
|
|
33
|
+
"""Initialize mock client."""
|
|
34
|
+
logger.info("Initializing MockGoCardlessClient")
|
|
35
|
+
self._token = "mock-token"
|
|
36
|
+
self.secret_id = secret_id
|
|
37
|
+
self.secret_key = secret_key
|
|
38
|
+
|
|
39
|
+
def list_banks(self, country: Optional[str] = None) -> List[str]:
|
|
40
|
+
"""Get bank names."""
|
|
41
|
+
institutions = self.get_institutions(country)
|
|
42
|
+
return [inst.name for inst in institutions]
|
|
43
|
+
|
|
44
|
+
def get_institutions(self, country: Optional[str] = None) -> List[Institution]:
|
|
45
|
+
"""Get demo institutions."""
|
|
46
|
+
logger.debug(f"MockClient: Getting institutions for country {country}")
|
|
47
|
+
institutions = [
|
|
48
|
+
Institution(
|
|
49
|
+
id="SOGEFRPP",
|
|
50
|
+
name="Société Générale",
|
|
51
|
+
bic="SOGEFRPP",
|
|
52
|
+
transaction_total_days="730",
|
|
53
|
+
countries=["FR"],
|
|
54
|
+
logo=None,
|
|
55
|
+
),
|
|
56
|
+
Institution(
|
|
57
|
+
id="BNPAFRPP",
|
|
58
|
+
name="BNP Paribas",
|
|
59
|
+
bic="BNPAFRPP",
|
|
60
|
+
transaction_total_days="730",
|
|
61
|
+
countries=["FR"],
|
|
62
|
+
logo=None,
|
|
63
|
+
),
|
|
64
|
+
Institution(
|
|
65
|
+
id="DEUTSCHE",
|
|
66
|
+
name="Deutsche Bank",
|
|
67
|
+
bic="DEUTSCH",
|
|
68
|
+
transaction_total_days="730",
|
|
69
|
+
countries=["DE"],
|
|
70
|
+
logo=None,
|
|
71
|
+
),
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
if country:
|
|
75
|
+
institutions = [i for i in institutions if country in i.countries]
|
|
76
|
+
|
|
77
|
+
return institutions
|
|
78
|
+
|
|
79
|
+
def get_institution(self, institution_id: str) -> Institution:
|
|
80
|
+
"""Get specific institution."""
|
|
81
|
+
logger.debug(f"MockClient: Getting institution {institution_id}")
|
|
82
|
+
institutions = self.get_institutions()
|
|
83
|
+
for inst in institutions:
|
|
84
|
+
if inst.id == institution_id:
|
|
85
|
+
return inst
|
|
86
|
+
raise ValueError(f"Institution {institution_id} not found")
|
|
87
|
+
|
|
88
|
+
def get_requisitions(self) -> List[Requisition]:
|
|
89
|
+
"""Get demo requisitions."""
|
|
90
|
+
logger.debug("MockClient: Getting requisitions")
|
|
91
|
+
accounts = self.get_accounts()
|
|
92
|
+
if not accounts:
|
|
93
|
+
return []
|
|
94
|
+
|
|
95
|
+
requisitions = []
|
|
96
|
+
references = [
|
|
97
|
+
"main-checking",
|
|
98
|
+
"savings-account",
|
|
99
|
+
"joint-account",
|
|
100
|
+
"business-account",
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
for i in range(0, len(accounts), 2):
|
|
104
|
+
ref_idx = (i // 2) % len(references)
|
|
105
|
+
requisitions.append(
|
|
106
|
+
Requisition(
|
|
107
|
+
id=f"req_{i}",
|
|
108
|
+
created=datetime.now().isoformat(),
|
|
109
|
+
redirect="http://localhost",
|
|
110
|
+
reference=references[ref_idx],
|
|
111
|
+
status="LINKED",
|
|
112
|
+
institution_id="SOGEFRPP" if i % 2 == 0 else "BNPAFRPP",
|
|
113
|
+
accounts=[acc.id for acc in accounts[i : i + 2]],
|
|
114
|
+
access_valid_for_days=90,
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
return requisitions
|
|
118
|
+
|
|
119
|
+
def get_account(self, account_id: str) -> Account:
|
|
120
|
+
"""Get demo account."""
|
|
121
|
+
logger.debug(f"MockClient: Getting account {account_id}")
|
|
122
|
+
accounts = self.get_accounts()
|
|
123
|
+
for acc in accounts:
|
|
124
|
+
if acc.id == account_id:
|
|
125
|
+
return acc
|
|
126
|
+
raise ValueError(f"Account {account_id} not found")
|
|
127
|
+
|
|
128
|
+
def get_accounts(self) -> List[Account]:
|
|
129
|
+
"""Get demo accounts."""
|
|
130
|
+
return [
|
|
131
|
+
Account(
|
|
132
|
+
id="acc_001",
|
|
133
|
+
created=datetime.now().isoformat(),
|
|
134
|
+
status="READY",
|
|
135
|
+
institution_id="SOGEFRPP",
|
|
136
|
+
name="Main Checking",
|
|
137
|
+
iban="FR1420041010050500013M02606",
|
|
138
|
+
),
|
|
139
|
+
Account(
|
|
140
|
+
id="acc_002",
|
|
141
|
+
created=datetime.now().isoformat(),
|
|
142
|
+
status="READY",
|
|
143
|
+
institution_id="BNPAFRPP",
|
|
144
|
+
name="Savings Account",
|
|
145
|
+
iban="FR7613807000013000060004391",
|
|
146
|
+
),
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
def get_account_balances(self, account_id: str) -> AccountBalance:
|
|
150
|
+
"""Get demo account balances."""
|
|
151
|
+
logger.debug(f"MockClient: Getting balances for account {account_id}")
|
|
152
|
+
return AccountBalance(
|
|
153
|
+
balances=[
|
|
154
|
+
BalanceSchema(
|
|
155
|
+
balance_amount=BalanceAmountSchema(
|
|
156
|
+
amount="2547.83", currency="EUR"
|
|
157
|
+
),
|
|
158
|
+
balance_type="interimAvailable",
|
|
159
|
+
),
|
|
160
|
+
BalanceSchema(
|
|
161
|
+
balance_amount=BalanceAmountSchema(
|
|
162
|
+
amount="2490.00", currency="EUR"
|
|
163
|
+
),
|
|
164
|
+
balance_type="closingBooked",
|
|
165
|
+
),
|
|
166
|
+
]
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def list_accounts(self) -> List[AccountInfo]:
|
|
170
|
+
"""Get all accounts with expiry info."""
|
|
171
|
+
logger.debug("MockClient: Getting all accounts")
|
|
172
|
+
accounts = self.get_accounts()
|
|
173
|
+
requisitions = self.get_requisitions()
|
|
174
|
+
result = []
|
|
175
|
+
|
|
176
|
+
for i, account in enumerate(accounts):
|
|
177
|
+
account_dict = account.model_dump()
|
|
178
|
+
req = requisitions[i // 2] if requisitions else None
|
|
179
|
+
req_status = req.status if req else "LINKED"
|
|
180
|
+
req_created = req.created if req else datetime.now().isoformat()
|
|
181
|
+
req_id = req.id if req else f"mock-req-{i // 2}"
|
|
182
|
+
req_ref = req.reference if req else f"mock-ref-{i // 2}"
|
|
183
|
+
access_days = (
|
|
184
|
+
req.access_valid_for_days if req and req.access_valid_for_days else 90
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
created_date = datetime.fromisoformat(req_created.replace("Z", "+00:00"))
|
|
188
|
+
expiry_date = created_date + timedelta(days=access_days)
|
|
189
|
+
is_expired = req_status == "EX"
|
|
190
|
+
|
|
191
|
+
account_dict.update(
|
|
192
|
+
{
|
|
193
|
+
"requisition_id": req_id,
|
|
194
|
+
"requisition_reference": req_ref,
|
|
195
|
+
"institution_id": account.institution_id or "mock-inst",
|
|
196
|
+
"requisition_status": req_status,
|
|
197
|
+
"access_valid_until": expiry_date.isoformat(),
|
|
198
|
+
"is_expired": is_expired,
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
result.append(account_dict)
|
|
202
|
+
return result
|
|
203
|
+
|
|
204
|
+
def create_requisition(self, *args, **kwargs):
|
|
205
|
+
raise NotImplementedError("MockClient does not support creating requisitions")
|
|
206
|
+
|
|
207
|
+
def create_bank_link(self, *args, **kwargs):
|
|
208
|
+
raise NotImplementedError("MockClient does not support creating bank links")
|
|
209
|
+
|
|
210
|
+
def delete_requisition(self, *args, **kwargs):
|
|
211
|
+
raise NotImplementedError("MockClient does not support deleting requisitions")
|
|
212
|
+
|
|
213
|
+
def find_requisition_by_reference(self, reference: str) -> Optional[Requisition]:
|
|
214
|
+
"""Find requisition by reference."""
|
|
215
|
+
requisitions = self.get_requisitions()
|
|
216
|
+
return next((req for req in requisitions if req.reference == reference), None)
|
|
217
|
+
|
|
218
|
+
def get_all_accounts(self) -> List[AccountInfo]:
|
|
219
|
+
"""Alias for list_accounts."""
|
|
220
|
+
return self.list_accounts()
|