from __future__ import annotations import re from decimal import Decimal from .models import ( FundPortfolioReport, Severity, ValidationFinding, ValidationResult, ) ISIN_PATTERN = re.compile(r"^TR[A-Z0-9]{10}$") CENT = Decimal("0.01") def _difference_finding( *, code: str, message: str, expected: Decimal, actual: Decimal, tolerance: Decimal, sheet: str, severity: Severity = Severity.ERROR, record_key: str | None = None, page: int | None = None, ) -> ValidationFinding | None: if abs(expected - actual) <= tolerance: return None return ValidationFinding( severity=severity, code=code, message=message, sheet=sheet, record_key=record_key, page=page, expected=str(expected), actual=str(actual), ) def validate_report(report: FundPortfolioReport) -> ValidationResult: findings: list[ValidationFinding] = [] for row in report.equities: if not ISIN_PATTERN.match(row.isin): findings.append( ValidationFinding( severity=Severity.ERROR, code="INVALID_ISIN", message="ISIN biçimi geçersiz.", sheet="Hisse Senetleri", record_key=row.code, page=row.source.page, actual=row.isin, ) ) calculated = row.nominal * row.current_price tolerance = max(Decimal("1.00"), abs(row.total_value) * Decimal("0.000001")) finding = _difference_finding( code="POSITION_VALUE_MISMATCH", message="Nominal değer × güncel fiyat, toplam değerle uzlaşmıyor.", expected=row.total_value, actual=calculated, tolerance=tolerance, sheet="Hisse Senetleri", record_key=row.code, page=row.source.page, ) if finding: findings.append(finding) totals = report.declared_totals calculated_equity_nominal = sum( (row.nominal for row in report.equities), start=Decimal("0") ) calculated_equity_value = sum( (row.total_value for row in report.equities), start=Decimal("0") ) calculated_repo_value = sum( (row.total_value for row in report.repos), start=Decimal("0") ) calculated_derivative = sum( (row.total_value for row in report.derivatives), start=Decimal("0") ) calculated_collateral = sum( (row.total_value for row in report.cash_collaterals), start=Decimal("0") ) aggregate_checks = [ _difference_finding( code="EQUITY_NOMINAL_ROUNDING", message=( "Detay nominal toplamı ile PDF beyan toplamı arasında fark var. " "Bu fark, kaynakta satırların iki ondalıkla gösterilmesinden kaynaklanabilir." ), expected=totals.equity_nominal, actual=calculated_equity_nominal, tolerance=Decimal("0.00"), sheet="Hisse Senetleri", severity=Severity.WARNING, ), _difference_finding( code="EQUITY_TOTAL_MISMATCH", message="Hisse detay toplamı PDF grup toplamıyla uzlaşmıyor.", expected=totals.equity_value, actual=calculated_equity_value, tolerance=CENT, sheet="Hisse Senetleri", ), _difference_finding( code="REPO_TOTAL_MISMATCH", message="Repo detay toplamı PDF grup toplamıyla uzlaşmıyor.", expected=totals.repo_value, actual=calculated_repo_value, tolerance=CENT, sheet="T. Repo", ), _difference_finding( code="DERIVATIVE_TOTAL_MISMATCH", message="Türev detay toplamı PDF grup toplamıyla uzlaşmıyor.", expected=totals.derivative_exposure, actual=calculated_derivative, tolerance=CENT, sheet="Türev", ), _difference_finding( code="COLLATERAL_TOTAL_MISMATCH", message="Nakit teminat toplamı PDF grup toplamıyla uzlaşmıyor.", expected=totals.cash_collateral_value, actual=calculated_collateral, tolerance=CENT, sheet="Türev", ), ] findings.extend(finding for finding in aggregate_checks if finding) calculated_portfolio = ( totals.equity_value + totals.repo_value + totals.cash_collateral_value ) portfolio_finding = _difference_finding( code="PORTFOLIO_RECONCILIATION_FAILED", message=( "Hisse + repo + VİOP nakit teminatı, fon portföy değeriyle uzlaşmıyor." ), expected=totals.fund_portfolio_value, actual=calculated_portfolio, tolerance=CENT, sheet="Özet", ) if portfolio_finding: findings.append(portfolio_finding) fpd_sum = ( totals.equity_fpd_pct + totals.repo_fpd_pct + totals.cash_collateral_fpd_pct ) fpd_finding = _difference_finding( code="FPD_PERCENT_MISMATCH", message="FPD yüzdeleri %100'e ulaşmıyor.", expected=Decimal("1"), actual=fpd_sum, tolerance=Decimal("0.0001"), sheet="Özet", ) if fpd_finding: findings.append(fpd_finding) findings.append( ValidationFinding( severity=Severity.INFO, code="DERIVATIVE_EXPOSURE_DISCLOSURE", message=( "Futures değeri maruziyet olarak raporlandı; fon portföy değeri " "uzlaştırmasına dahil edilmedi." ), sheet="Özet", expected=str(totals.derivative_exposure), ) ) error_count = sum(item.severity == Severity.ERROR for item in findings) warning_count = sum(item.severity == Severity.WARNING for item in findings) info_count = sum(item.severity == Severity.INFO for item in findings) status = "Başarısız" if error_count else "Uyarılı" if warning_count else "Doğrulandı" return ValidationResult( status=status, findings=findings, error_count=error_count, warning_count=warning_count, info_count=info_count, )