Files
finance/backend/app/imports/adapters/base.py
T
k1nq d582f15681 feat: add tests for import polling, login page, money utilities, and query keys
- Implemented tests for import polling functionality, ensuring correct behavior for active and terminal import statuses.
- Created tests for the login page, validating user interactions and error handling.
- Added unit tests for money utility functions, covering parsing, formatting, and summation of monetary values.
- Developed tests for query keys, ensuring proper scoping and invalidation roots.

chore: setup testing environment and configuration

- Added setup file for testing with Vitest and Testing Library.
- Configured TypeScript and Vite for the frontend project.
- Created Docker and Nginx configurations for local development and production environments.
- Established a PostgreSQL initialization script for setting up application roles.

docs: add README and environment configuration for infra

- Included README for the infra directory, detailing local development setup and service descriptions.
- Created an example environment configuration file for local development with placeholders for sensitive information.
2026-09-14 09:26:33 +05:00

97 lines
2.8 KiB
Python

import hashlib
from dataclasses import dataclass
from datetime import date
from decimal import Decimal, InvalidOperation
from typing import Protocol
from app.errors import AppError
DIRECTION_IN = "in"
DIRECTION_OUT = "out"
PARSER_VERSION = "1"
@dataclass(frozen=True)
class SourceRow:
external_id: str | None
occurred_on: date | None
posted_on: date | None
amount_kopecks: int
direction: str
description: str | None
mcc: int | None
bank_category: str | None
location: str | None
row_ref: str
warnings: tuple[str, ...] = ()
@dataclass(frozen=True)
class StatementDraft:
adapter_id: str
parser_version: str
bank_code: str
product: str
account_fingerprint: str | None
currency: str
period_start: date | None
period_end: date | None
opening_balance_kopecks: int | None
closing_balance_kopecks: int | None
rows: tuple[SourceRow, ...]
class UnsupportedStatementLayout(AppError):
"""Stable diagnostic for unknown bank layouts; never guesses."""
def __init__(self, detail: str) -> None:
super().__init__(
detail,
code="IMPORT_UNSUPPORTED_LAYOUT",
status_code=422,
)
class StatementAdapter(Protocol):
adapter_id: str
def detect(self, raw: bytes) -> bool: ...
def parse(self, raw: bytes) -> StatementDraft: ...
def amount_to_kopecks(value: object) -> int:
if isinstance(value, int):
return value
if isinstance(value, float):
raise UnsupportedStatementLayout(
"float amounts are not accepted; cell must be text/decimal"
)
if isinstance(value, Decimal):
return int(value.quantize(Decimal("0.01")).scaleb(2))
if isinstance(value, str):
cleaned = value.replace(" ", "").replace("\u00a0", "").replace(",", ".")
try:
return int(Decimal(cleaned).quantize(Decimal("0.01")).scaleb(2))
except (InvalidOperation, ValueError) as exc:
raise UnsupportedStatementLayout(f"unparseable amount: {value!r}") from exc
raise UnsupportedStatementLayout(f"unsupported amount cell type: {type(value).__name__}")
def account_fingerprint(bank_code: str, masked_identifier: str) -> str:
digest = hashlib.sha256(f"{bank_code}|{masked_identifier}".encode()).hexdigest()
return digest[:64]
def normalize_description(description: str | None) -> str:
if not description:
return ""
return " ".join(description.lower().split())
def record_fingerprint(
occurred_on: date | None, amount_kopecks: int, description: str | None
) -> str:
occurred = occurred_on.isoformat() if occurred_on else ""
payload = f"{occurred}|{amount_kopecks}|{normalize_description(description)[:120]}"
return hashlib.sha256(payload.encode("utf-8")).hexdigest()