Compare commits
6
Commits
75fd76b0d9
...
afe2478cc7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afe2478cc7 | ||
|
|
46bdd5e68c | ||
|
|
58dc585e7e | ||
|
|
7ab7276db5 | ||
|
|
500b76b8a8 | ||
|
|
e9dcb88da5 |
@@ -0,0 +1,107 @@
|
|||||||
|
# finanz-cockpit Agent Instructions
|
||||||
|
|
||||||
|
## Setup Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
uv sync
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
|
alembic upgrade head
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
pytest
|
||||||
|
|
||||||
|
# Run a single test file
|
||||||
|
pytest tests/test_model_b4wexport.py -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
- **`src/`**: Application code
|
||||||
|
- `main.py`: CLI entrypoint for CSV ingestion
|
||||||
|
- `db.py`: Database connection management (uses global `_engine` pattern)
|
||||||
|
- `models/`: Pydantic models + SQLAlchemy tables for b4w and comdirect imports
|
||||||
|
- `ingest/`: Data ingestion logic per source
|
||||||
|
- **`tests/`**: Tests with fixtures in `conftest.py`
|
||||||
|
- **`alembic/versions/`**: Database migrations
|
||||||
|
|
||||||
|
## Key Patterns & Gotchas
|
||||||
|
|
||||||
|
### Database Configuration
|
||||||
|
|
||||||
|
```python
|
||||||
|
from src.db import get_engine, get_session, set_engine
|
||||||
|
|
||||||
|
# Development: use sqlite://
|
||||||
|
os.environ["DATABASE_URL"] = "sqlite://"
|
||||||
|
|
||||||
|
# Production/default: sqlite:///finanz.db
|
||||||
|
engine = get_engine() # reads DATABASE_URL or uses default
|
||||||
|
with get_session() as session:
|
||||||
|
session.query(...).all()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Critical**: Tests override engine via `conftest.py`. Never assume global state - always use `get_session()` context manager.
|
||||||
|
|
||||||
|
### CSV Format Expectations
|
||||||
|
|
||||||
|
- **b4w**: German bank export (CSV) from `/mnt/anbau/Software/Python/finanz-cockpit/scripts/dummy_data/b4w_export_dummy_01.csv`
|
||||||
|
- **comdirect**: German bank export (CSV) with fields like `Buchungstag`, `Umsatz in EUR`, `Vorgang`
|
||||||
|
|
||||||
|
### Testing Workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Tests automatically run migrations via conftest fixture
|
||||||
|
pytest tests/ -v --tb=short
|
||||||
|
|
||||||
|
# To test without migrations (in-memory SQLite), use DATABASE_URL="sqlite://"
|
||||||
|
export DATABASE_URL="sqlite://" pytest tests/test_db.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
- `DATABASE_URL`: Database connection string (default: `sqlite:///finanz.db`)
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
Two sources with shared patterns:
|
||||||
|
|
||||||
|
- **b4w**: `B4wExport`, `Category`, transactional import to `import_b4w` table + `category` table
|
||||||
|
- **comdirect**: `ComdirectGiro`, `ComdirectCreditCard`, `ComdirectDepot`, import to `comdirect_giro` table
|
||||||
|
|
||||||
|
All models inherit from common base in `src/models/_common.py`:
|
||||||
|
- `_hash()`: SHA256-based deduplication key
|
||||||
|
- `_parse_date_german()`: Handles "07.09.2026" format and values like "offen", "--"
|
||||||
|
- `_parse_decimal_german()`: Converts "1,23" to Decimal
|
||||||
|
|
||||||
|
## Factory Pattern for Testing
|
||||||
|
|
||||||
|
Import from `tests/factories.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from tests.factories import B4wExportFactory, ComdirectGiroFactory
|
||||||
|
from src.constants import Currency
|
||||||
|
|
||||||
|
B4wExportFactory.__faker__ = Faker("de_DE")
|
||||||
|
ComdirectGiroFactory.__faker__ = Faker("de_DE")
|
||||||
|
|
||||||
|
# Use in tests:
|
||||||
|
item = B4wExportFactory.build( # build() for partial, create() for full
|
||||||
|
Amt="50.00",
|
||||||
|
RmtInf="Supermarket",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Mistakes to Avoid
|
||||||
|
|
||||||
|
1. **Assuming global engine state**: Always use `get_session()` context manager
|
||||||
|
2. **Ignoring German date formats**: Use `_parse_date_german()` for CSV parsing
|
||||||
|
3. **Forgetting migration setup in tests**: Tests auto-migrate, but integration tests with real db need explicit upgrade
|
||||||
|
4. **Wrong Faker locale**: Use `Faker("de_DE")` for realistic names/IBANs
|
||||||
|
|
||||||
|
## Testing Tips
|
||||||
|
|
||||||
|
- Run single test: `pytest tests/test_model_b4wexport.py -v`
|
||||||
|
- View migrations: `ls alembic/versions/`
|
||||||
|
- Test data location: `/scripts/dummy_data/`
|
||||||
@@ -3,13 +3,12 @@ from pathlib import Path
|
|||||||
from faker import Faker
|
from faker import Faker
|
||||||
|
|
||||||
from src.constants import Currency
|
from src.constants import Currency
|
||||||
from src.ingest.b4w import ingest_b4w, reader_b4w
|
|
||||||
from src.ingest.comdirect import reader_comdirect
|
|
||||||
from src.models import (
|
from src.models import (
|
||||||
ComdirectCreditCard,
|
ComdirectCreditCard,
|
||||||
ComdirectDepot,
|
ComdirectDepot,
|
||||||
ComdirectGiro,
|
ComdirectGiro,
|
||||||
)
|
)
|
||||||
|
from src.models.b4w import B4wExport
|
||||||
from src.models.settings import Account, Source
|
from src.models.settings import Account, Source
|
||||||
|
|
||||||
fake = Faker()
|
fake = Faker()
|
||||||
@@ -18,11 +17,10 @@ BANK_ACCOUNTS = {
|
|||||||
"b4w": Source(
|
"b4w": Source(
|
||||||
name="b4w",
|
name="b4w",
|
||||||
source_path=Path(""),
|
source_path=Path(""),
|
||||||
reader_function=reader_b4w,
|
|
||||||
accounts=[
|
accounts=[
|
||||||
Account(
|
Account(
|
||||||
name="b4w",
|
name="b4w",
|
||||||
ingestion_function=ingest_b4w,
|
model=B4wExport,
|
||||||
iban=fake.iban(),
|
iban=fake.iban(),
|
||||||
currency=Currency.EUR,
|
currency=Currency.EUR,
|
||||||
)
|
)
|
||||||
@@ -31,29 +29,28 @@ BANK_ACCOUNTS = {
|
|||||||
"comdirect": Source(
|
"comdirect": Source(
|
||||||
name="comdirect",
|
name="comdirect",
|
||||||
source_path=Path(""),
|
source_path=Path(""),
|
||||||
reader_function=reader_comdirect,
|
|
||||||
accounts=[
|
accounts=[
|
||||||
Account(
|
Account(
|
||||||
name="Girokonto",
|
name="Girokonto",
|
||||||
ingestion_function=lambda x: ComdirectGiro.model_validate(x),
|
model=ComdirectGiro,
|
||||||
iban=fake.iban(),
|
iban=fake.iban(),
|
||||||
currency=Currency.EUR,
|
currency=Currency.EUR,
|
||||||
),
|
),
|
||||||
Account(
|
Account(
|
||||||
name="Visa-Karte (Kreditkarte)",
|
name="Visa-Karte (Kreditkarte)",
|
||||||
ingestion_function=lambda x: ComdirectCreditCard.model_validate(x),
|
model=ComdirectCreditCard,
|
||||||
iban=fake.iban(),
|
iban=fake.iban(),
|
||||||
currency=Currency.EUR,
|
currency=Currency.EUR,
|
||||||
),
|
),
|
||||||
Account(
|
Account(
|
||||||
name="Tagesgeld PLUS-Konto",
|
name="Tagesgeld PLUS-Konto",
|
||||||
ingestion_function=lambda x: ComdirectGiro.model_validate(x),
|
model=ComdirectGiro,
|
||||||
iban=fake.iban(),
|
iban=fake.iban(),
|
||||||
currency=Currency.EUR,
|
currency=Currency.EUR,
|
||||||
),
|
),
|
||||||
Account(
|
Account(
|
||||||
name="Depot",
|
name="Depot",
|
||||||
ingestion_function=lambda x: ComdirectDepot.model_validate(x),
|
model=ComdirectDepot,
|
||||||
iban=fake.iban(),
|
iban=fake.iban(),
|
||||||
currency=Currency.EUR,
|
currency=Currency.EUR,
|
||||||
),
|
),
|
||||||
|
|||||||
+2
-2
@@ -27,8 +27,8 @@ def main():
|
|||||||
assert isinstance(giro_account, Account)
|
assert isinstance(giro_account, Account)
|
||||||
ingest_comdirect(
|
ingest_comdirect(
|
||||||
Path(args.input_comdirect),
|
Path(args.input_comdirect),
|
||||||
account_iban=giro_account.iban,
|
account_iban=giro_account.iban or "",
|
||||||
account_currency=giro_account.currency,
|
account_currency=giro_account.currency or "",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+19
-1
@@ -61,7 +61,25 @@ type CsV2DictMulti = dict[str, Csv2Dict]
|
|||||||
|
|
||||||
|
|
||||||
class ExportBaseModel(BaseModel):
|
class ExportBaseModel(BaseModel):
|
||||||
pass
|
|
||||||
|
@classmethod
|
||||||
|
def _my_name(cls):
|
||||||
|
return cls.__name__
|
||||||
|
|
||||||
|
def parameters_to_dict(self) -> dict[str, str]:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def parameters_to_str(self):
|
||||||
|
parameters = ", ".join(
|
||||||
|
f"{key}='{value}'" for key, value in self.parameters_to_dict().items()
|
||||||
|
)
|
||||||
|
return parameters
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"{self._my_name()}({self.parameters_to_str()})"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self._my_name()}({self.parameters_to_str()})"
|
||||||
|
|
||||||
|
|
||||||
class BaseModelDb(DeclarativeBase):
|
class BaseModelDb(DeclarativeBase):
|
||||||
|
|||||||
+9
-14
@@ -27,7 +27,7 @@ class B4wExport(ExportBaseModel):
|
|||||||
sign: Sign = Field(alias="CdtDbtInd")
|
sign: Sign = Field(alias="CdtDbtInd")
|
||||||
reference: str = Field(alias="RmtInf")
|
reference: str = Field(alias="RmtInf")
|
||||||
other_name: str = Field(alias="RmtdNm")
|
other_name: str = Field(alias="RmtdNm")
|
||||||
other_iban: str = Field(alias="RmtdAcctIBAN", pattern=IBAN_PATTERN)
|
other_iban: str | None = Field(alias="RmtdAcctIBAN", pattern=IBAN_PATTERN)
|
||||||
category: Category | None = Field(
|
category: Category | None = Field(
|
||||||
alias="Category",
|
alias="Category",
|
||||||
examples=[
|
examples=[
|
||||||
@@ -51,23 +51,11 @@ class B4wExport(ExportBaseModel):
|
|||||||
"CdtDbtInd": f"{self.sign!s}",
|
"CdtDbtInd": f"{self.sign!s}",
|
||||||
"RmtInf": f"{self.reference}",
|
"RmtInf": f"{self.reference}",
|
||||||
"RmtdNm": f"{self.other_name}",
|
"RmtdNm": f"{self.other_name}",
|
||||||
"RmtdAcctIBAN": f"{self.other_iban}",
|
"RmtdAcctIBAN": f"{self.other_iban or ''}",
|
||||||
"Category": f"{self.category.to_str() if self.category else ''}",
|
"Category": f"{self.category.to_str() if self.category else ''}",
|
||||||
"Notes": f"{self.notes}",
|
"Notes": f"{self.notes}",
|
||||||
}
|
}
|
||||||
|
|
||||||
def parameters_to_str(self):
|
|
||||||
parameters = ", ".join(
|
|
||||||
f"{key}='{value}'" for key, value in self.parameters_to_dict().items()
|
|
||||||
)
|
|
||||||
return parameters
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"B4wExport({self.parameters_to_str()})"
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"B4wExport({self.parameters_to_str()})"
|
|
||||||
|
|
||||||
@computed_field
|
@computed_field
|
||||||
@property
|
@property
|
||||||
def hash_id(self) -> str:
|
def hash_id(self) -> str:
|
||||||
@@ -77,6 +65,13 @@ class B4wExport(ExportBaseModel):
|
|||||||
def amount_signed(self) -> Decimal:
|
def amount_signed(self) -> Decimal:
|
||||||
return self.amount * self.sign.to_sign()
|
return self.amount * self.sign.to_sign()
|
||||||
|
|
||||||
|
@field_validator("other_iban", mode="before")
|
||||||
|
@classmethod
|
||||||
|
def other_iban_blank_is_none(cls, iban_in: str | None) -> str | None:
|
||||||
|
if iban_in is None or (isinstance(iban_in, str) and not iban_in.strip()):
|
||||||
|
return None
|
||||||
|
return iban_in
|
||||||
|
|
||||||
@field_validator("category", mode="before")
|
@field_validator("category", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def category_str_is_parsable(
|
def category_str_is_parsable(
|
||||||
|
|||||||
+69
-2
@@ -1,7 +1,7 @@
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from pydantic import Field, computed_field, field_validator
|
from pydantic import ConfigDict, Field, computed_field, field_validator
|
||||||
from sqlalchemy import Column, Date, String
|
from sqlalchemy import Column, Date, String
|
||||||
|
|
||||||
from src.models._common import (
|
from src.models._common import (
|
||||||
@@ -10,10 +10,13 @@ from src.models._common import (
|
|||||||
_hash,
|
_hash,
|
||||||
_parse_date_german,
|
_parse_date_german,
|
||||||
_parse_decimal_german,
|
_parse_decimal_german,
|
||||||
|
date2str,
|
||||||
|
decimal2str,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ComdirectBase(ExportBaseModel):
|
class ComdirectBase(ExportBaseModel):
|
||||||
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
date_val: date | None = Field(
|
date_val: date | None = Field(
|
||||||
alias="Wertstellung (Valuta)", examples=["07.09.2026"]
|
alias="Wertstellung (Valuta)", examples=["07.09.2026"]
|
||||||
)
|
)
|
||||||
@@ -21,7 +24,9 @@ class ComdirectBase(ExportBaseModel):
|
|||||||
|
|
||||||
@field_validator("date_val", mode="before")
|
@field_validator("date_val", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse_date_val(cls, date_str: str) -> date | None:
|
def parse_date_val(cls, date_str: str | None) -> date | None:
|
||||||
|
if date_str is None:
|
||||||
|
return
|
||||||
return _parse_date_german(date_str)
|
return _parse_date_german(date_str)
|
||||||
|
|
||||||
@field_validator("amount_eur", mode="before")
|
@field_validator("amount_eur", mode="before")
|
||||||
@@ -29,6 +34,29 @@ class ComdirectBase(ExportBaseModel):
|
|||||||
def parse_amount(cls, decimal_str: str) -> Decimal:
|
def parse_amount(cls, decimal_str: str) -> Decimal:
|
||||||
return _parse_decimal_german(decimal_str)
|
return _parse_decimal_german(decimal_str)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _date2str(cls, date_value: date | None) -> str:
|
||||||
|
return date2str(date_value, default="--", date_format="%d.%m.%Y")
|
||||||
|
|
||||||
|
def parameters_to_dict(self, by_name=True) -> dict[str, str]:
|
||||||
|
return {
|
||||||
|
"date_val" if by_name else "Wertstellung (Valuta)": self._date2str(
|
||||||
|
self.date_val
|
||||||
|
),
|
||||||
|
"amount_eur" if by_name else "Umsatz in EUR": decimal2str(self.amount_eur),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_orig_headers(self, sep: str = ";", bracket: str = '"'):
|
||||||
|
return sep.join(
|
||||||
|
f"{bracket}{txt}{bracket}" for txt in self.parameters_to_dict(by_name=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_orig_row(self, sep: str = ";", bracket: str = '"'):
|
||||||
|
return sep.join(
|
||||||
|
f"{bracket}{txt}{bracket}"
|
||||||
|
for txt in self.parameters_to_dict(by_name=False).values()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ComdirectGiro(ComdirectBase):
|
class ComdirectGiro(ComdirectBase):
|
||||||
# # Fields from parents:
|
# # Fields from parents:
|
||||||
@@ -50,6 +78,19 @@ class ComdirectGiro(ComdirectBase):
|
|||||||
def parse_date_book(cls, date_str: str) -> date | None:
|
def parse_date_book(cls, date_str: str) -> date | None:
|
||||||
return _parse_date_german(date_str)
|
return _parse_date_german(date_str)
|
||||||
|
|
||||||
|
def parameters_to_dict(self, by_name=True) -> dict[str, str]:
|
||||||
|
parameters = dict(**super().parameters_to_dict(by_name=by_name))
|
||||||
|
parameters.update(
|
||||||
|
{
|
||||||
|
"date_book" if by_name else "Buchungstag": self._date2str(
|
||||||
|
self.date_book
|
||||||
|
),
|
||||||
|
"type" if by_name else "Vorgang": self.type,
|
||||||
|
"text" if by_name else "Buchungstext": self.text,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return parameters
|
||||||
|
|
||||||
|
|
||||||
class ComdirectCreditCard(ComdirectGiro):
|
class ComdirectCreditCard(ComdirectGiro):
|
||||||
# # Fields from parents:
|
# # Fields from parents:
|
||||||
@@ -71,6 +112,18 @@ class ComdirectCreditCard(ComdirectGiro):
|
|||||||
alias="Umsatz in EUR", examples=["32,43"], default=None
|
alias="Umsatz in EUR", examples=["32,43"], default=None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def parameters_to_str(self, sep=", ", str_qualifier=False): ...
|
||||||
|
|
||||||
|
def parameters_to_dict(self) -> dict[str, str]:
|
||||||
|
parameters = dict(**super().parameters_to_dict())
|
||||||
|
parameters.update(
|
||||||
|
{
|
||||||
|
"Umsatztag": self._date2str(self.date_val),
|
||||||
|
"Referenz": self.reference,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return parameters
|
||||||
|
|
||||||
|
|
||||||
class ComdirectDepot(ComdirectBase):
|
class ComdirectDepot(ComdirectBase):
|
||||||
# # Fields from parents:
|
# # Fields from parents:
|
||||||
@@ -88,6 +141,20 @@ class ComdirectDepot(ComdirectBase):
|
|||||||
def parse_decimals(cls, decimal_in: str) -> Decimal:
|
def parse_decimals(cls, decimal_in: str) -> Decimal:
|
||||||
return _parse_decimal_german(decimal_in)
|
return _parse_decimal_german(decimal_in)
|
||||||
|
|
||||||
|
def parameters_to_dict(self) -> dict[str, str]:
|
||||||
|
parameters = dict(**super().parameters_to_dict())
|
||||||
|
parameters.update(
|
||||||
|
{
|
||||||
|
"Geschäftstag": self._date2str(self.date_val),
|
||||||
|
"Stück / Nom.": decimal2str(self.num),
|
||||||
|
"Bezeichnung": self.name,
|
||||||
|
"WKN": self.wkn,
|
||||||
|
"Währung": self.currency,
|
||||||
|
"Ausführungskurs": decimal2str(self.spot_price),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return parameters
|
||||||
|
|
||||||
|
|
||||||
class ComdirectBaseDb(IngestdbBase):
|
class ComdirectBaseDb(IngestdbBase):
|
||||||
__abstract__ = True
|
__abstract__ = True
|
||||||
|
|||||||
+30
-6
@@ -1,4 +1,5 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from faker import Faker
|
from faker import Faker
|
||||||
from polyfactory.factories.pydantic_factory import ModelFactory
|
from polyfactory.factories.pydantic_factory import ModelFactory
|
||||||
@@ -10,6 +11,12 @@ from src.models.comdirect import ComdirectCreditCard, ComdirectDepot, ComdirectG
|
|||||||
DEFAULT_CATEGORY = "root_category:parent_category:leaf_category"
|
DEFAULT_CATEGORY = "root_category:parent_category:leaf_category"
|
||||||
|
|
||||||
|
|
||||||
|
def _faker_or_none(faker: Faker, non_default: Any, default: Any = None, p_value=0.5):
|
||||||
|
if faker.pyfloat(min_value=0.0, max_value=1.0) < p_value:
|
||||||
|
return default
|
||||||
|
return non_default
|
||||||
|
|
||||||
|
|
||||||
def _amount() -> Decimal:
|
def _amount() -> Decimal:
|
||||||
return B4wExportFactory.__faker__.pydecimal(
|
return B4wExportFactory.__faker__.pydecimal(
|
||||||
left_digits=6,
|
left_digits=6,
|
||||||
@@ -19,6 +26,18 @@ def _amount() -> Decimal:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _amount_eur() -> str:
|
||||||
|
"""Generate German-formatted decimal string (e.g., '32,43')."""
|
||||||
|
val = B4wExportFactory.__faker__.pydecimal(
|
||||||
|
left_digits=6,
|
||||||
|
right_digits=2,
|
||||||
|
positive=True,
|
||||||
|
min_value=Decimal("0.01"),
|
||||||
|
)
|
||||||
|
# Format as German string with comma decimal separator
|
||||||
|
return f"{val:.2f}".replace(".", ",").strip()
|
||||||
|
|
||||||
|
|
||||||
def _sentence() -> str:
|
def _sentence() -> str:
|
||||||
return B4wExportFactory.__faker__.sentence(
|
return B4wExportFactory.__faker__.sentence(
|
||||||
nb_words=8,
|
nb_words=8,
|
||||||
@@ -34,6 +53,11 @@ def _iban() -> str:
|
|||||||
return B4wExportFactory.__faker__.iban()
|
return B4wExportFactory.__faker__.iban()
|
||||||
|
|
||||||
|
|
||||||
|
def _iban_or_none() -> str:
|
||||||
|
faker = B4wExportFactory.__faker__
|
||||||
|
return _faker_or_none(faker, faker.iban(), default="")
|
||||||
|
|
||||||
|
|
||||||
class B4wExportFactory(ModelFactory[B4wExport]):
|
class B4wExportFactory(ModelFactory[B4wExport]):
|
||||||
__faker__ = Faker("de_DE")
|
__faker__ = Faker("de_DE")
|
||||||
__use_examples__ = True
|
__use_examples__ = True
|
||||||
@@ -43,22 +67,22 @@ class B4wExportFactory(ModelFactory[B4wExport]):
|
|||||||
Notes = Use(_sentence)
|
Notes = Use(_sentence)
|
||||||
RmtdNm = Use(_names)
|
RmtdNm = Use(_names)
|
||||||
OwnrAcctIBAN = Use(_iban)
|
OwnrAcctIBAN = Use(_iban)
|
||||||
RmtdAcctIBAN = Use(_iban)
|
RmtdAcctIBAN = Use(_iban_or_none)
|
||||||
|
|
||||||
|
|
||||||
class ComdirectGiroFactory(ModelFactory[ComdirectGiro]):
|
class ComdirectGiroFactory(ModelFactory[ComdirectGiro]):
|
||||||
__faker__ = Faker("de_DE")
|
__faker__ = Faker("de_DE")
|
||||||
__use_example__ = True
|
__use_examples__ = True
|
||||||
|
|
||||||
Buchungstext = Use(_sentence)
|
text = Use(_sentence)
|
||||||
|
amount_eur = Use(_amount_eur)
|
||||||
|
|
||||||
|
|
||||||
class ComdirectCreditCardFactory(ModelFactory[ComdirectCreditCard]):
|
class ComdirectCreditCardFactory(ModelFactory[ComdirectCreditCard]):
|
||||||
__faker__ = Faker("de_DE")
|
__faker__ = Faker("de_DE")
|
||||||
__use_example__ = True
|
__use_examples__ = True
|
||||||
|
|
||||||
|
|
||||||
class ComdirectDepotFactory(ModelFactory[ComdirectDepot]):
|
class ComdirectDepotFactory(ModelFactory[ComdirectDepot]):
|
||||||
__faker__ = Faker("de_DE")
|
__faker__ = Faker("de_DE")
|
||||||
__use_example__ = True
|
__use_examples__ = True
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,27 @@ def test_b4w_export_account_iban_to_short():
|
|||||||
_ = B4wExportFactory.build(OwnrAcctIBAN="DE123")
|
_ = B4wExportFactory.build(OwnrAcctIBAN="DE123")
|
||||||
|
|
||||||
|
|
||||||
|
def test_b4w_export_other_iban_empty_is_none():
|
||||||
|
data = B4wExportFactory.build(RmtdAcctIBAN="")
|
||||||
|
assert data.other_iban is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_b4w_export_other_iban_blank_is_none():
|
||||||
|
data = B4wExportFactory.build(RmtdAcctIBAN=" ")
|
||||||
|
assert data.other_iban is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_b4w_export_other_iban_invalid():
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
_ = B4wExportFactory.build(RmtdAcctIBAN="not an IBAN")
|
||||||
|
|
||||||
|
|
||||||
|
def test_b4w_export_other_iban_repr_round_trip_empty():
|
||||||
|
data = B4wExportFactory.build(RmtdAcctIBAN="")
|
||||||
|
data2 = eval(str(data))
|
||||||
|
assert data == data2
|
||||||
|
|
||||||
|
|
||||||
def test_b4w_export_parameters_to_dict():
|
def test_b4w_export_parameters_to_dict():
|
||||||
data = B4wExportFactory.build()
|
data = B4wExportFactory.build()
|
||||||
params = data.parameters_to_dict()
|
params = data.parameters_to_dict()
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
from src.models.comdirect import ComdirectGiro
|
||||||
|
from tests.factories import ComdirectGiroFactory
|
||||||
|
|
||||||
|
|
||||||
|
def test_comdirect_giro_basic_and_stringification():
|
||||||
|
data = ComdirectGiroFactory.build()
|
||||||
|
assert data
|
||||||
|
assert isinstance(data, ComdirectGiro)
|
||||||
|
orig_headers = data.get_orig_headers()
|
||||||
|
assert orig_headers
|
||||||
|
assert isinstance(orig_headers, str)
|
||||||
|
header_list = orig_headers.split(";")
|
||||||
|
assert len(header_list) > 0
|
||||||
|
assert set(header_list) == {
|
||||||
|
'"Wertstellung (Valuta)"',
|
||||||
|
'"Umsatz in EUR"',
|
||||||
|
'"Buchungstag"',
|
||||||
|
'"Vorgang"',
|
||||||
|
'"Buchungstext"',
|
||||||
|
}
|
||||||
|
orig_row = data.get_orig_row()
|
||||||
|
assert orig_row
|
||||||
|
assert isinstance(orig_row, str)
|
||||||
|
assert len(orig_row.split(";")) > 0
|
||||||
Reference in New Issue
Block a user