96 lines
3.6 KiB
Python
96 lines
3.6 KiB
Python
"""Real PostgreSQL semantics for all-source portfolio return aggregates.
|
|
|
|
Run with PORTFOLIOHUB_RUN_LEDGER_AGGREGATE_POSTGRES=1 against a disposable/local
|
|
PostgreSQL endpoint. Normal unit-test environments skip this module.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from unittest.mock import MagicMock
|
|
|
|
import psycopg
|
|
import pytest
|
|
|
|
from app.repositories.postgres import PostgresPortfolioRepository
|
|
|
|
pytestmark = pytest.mark.skipif(
|
|
os.getenv("PORTFOLIOHUB_RUN_LEDGER_AGGREGATE_POSTGRES") != "1",
|
|
reason="set PORTFOLIOHUB_RUN_LEDGER_AGGREGATE_POSTGRES=1 to run real PostgreSQL tests",
|
|
)
|
|
|
|
|
|
def _connect() -> psycopg.Connection:
|
|
return psycopg.connect(
|
|
host=os.getenv("TIMESCALEDB_HOST", "127.0.0.1"),
|
|
port=int(os.getenv("TIMESCALEDB_PORT", "5432")),
|
|
dbname=os.getenv("TIMESCALEDB_NAME", "portfolio_hub_migration_test"),
|
|
user=os.getenv("TIMESCALEDB_USER", "portfolio_hub_user"),
|
|
password=os.getenv("TIMESCALEDB_PASSWORD", "change-me"),
|
|
sslmode=os.getenv("TIMESCALEDB_SSLMODE", "prefer"),
|
|
)
|
|
|
|
|
|
def test_all_sources_mixed_sides_and_cross_source_reversals() -> None:
|
|
connection = _connect()
|
|
try:
|
|
with connection.cursor() as cursor:
|
|
cursor.execute(
|
|
"""
|
|
CREATE TEMP TABLE portfolio_ledger (
|
|
ledger_entry_id BIGINT PRIMARY KEY,
|
|
portfolio_id TEXT NOT NULL,
|
|
price DOUBLE PRECISION NOT NULL,
|
|
source TEXT NOT NULL,
|
|
side TEXT NOT NULL,
|
|
target_ledger_entry_id BIGINT
|
|
)
|
|
"""
|
|
)
|
|
cursor.executemany(
|
|
"""
|
|
INSERT INTO portfolio_ledger (
|
|
ledger_entry_id, portfolio_id, price, source, side, target_ledger_entry_id
|
|
) VALUES (%s, %s, %s, %s, %s, %s)
|
|
""",
|
|
[
|
|
(1, "broker-only", 700, "broker", "invest", None),
|
|
(2, "mixed", 1000, "manual", "invest", None),
|
|
(3, "mixed", 500, "broker", "invest", None),
|
|
(4, "mixed", 200, "broker", "withdraw", None),
|
|
(5, "mixed", 50, "manual", "profit", None),
|
|
# A manual reversal excludes a broker original.
|
|
(6, "mixed", 300, "broker", "invest", None),
|
|
(7, "mixed", 300, "manual", "reversal", 6),
|
|
# A broker reversal excludes a manual original.
|
|
(8, "mixed", 100, "manual", "withdraw", None),
|
|
(9, "mixed", 100, "broker", "reversal", 8),
|
|
# Non-capital sides are never inferred as INVEST.
|
|
(10, "mixed", 800_000, "broker", "buffer_out", None),
|
|
],
|
|
)
|
|
|
|
repo = PostgresPortfolioRepository.__new__(PostgresPortfolioRepository)
|
|
repo.ensure_schema = MagicMock()
|
|
repo._connect = MagicMock(return_value=connection)
|
|
|
|
summaries = repo.get_return_summaries(["broker-only", "mixed"])
|
|
|
|
assert summaries == {
|
|
"broker-only": {
|
|
"total_invested": 700.0,
|
|
"total_withdrawn": 0.0,
|
|
"net_contributed_capital": 700.0,
|
|
"cumulative_profit_withdrawn": 0.0,
|
|
},
|
|
"mixed": {
|
|
"total_invested": 1500.0,
|
|
"total_withdrawn": 200.0,
|
|
"net_contributed_capital": 1300.0,
|
|
"cumulative_profit_withdrawn": 50.0,
|
|
},
|
|
}
|
|
repo.ensure_schema.assert_called_once_with()
|
|
finally:
|
|
connection.close()
|