test(req-001): add missing dividend verification regressions and tighten catalog mappings
Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
co-authored by
genichin
parent
f193aa6da3
commit
1015b465bb
@@ -0,0 +1,25 @@
|
|||||||
|
# REQ-001 Provenance Reapproval Request
|
||||||
|
|
||||||
|
- 대상 requirement: `REQ-001` (`docs/requirements/req-001_manual-cash-dividend.md`)
|
||||||
|
- 현재 canonical SHA-256: `b9498b5d9b6dd4638d701f68273dbbde1a37dbbaa74d1d3c9923fb1702b6f076`
|
||||||
|
- 상태: 승인된 canonical 본문은 변경하지 않음
|
||||||
|
|
||||||
|
## 요청 배경
|
||||||
|
|
||||||
|
`REQ-001` 승인 기록의 문구(`OQ-001 선택지 A`)가 본문 계약(`tax` 허용) 및 이슈 #31의 확정 OQ-001(B)와 provenance 상 충돌한다는 리뷰 코멘트가 확인되었다.
|
||||||
|
|
||||||
|
## 요청 사항
|
||||||
|
|
||||||
|
승인된 canonical 파일 자체를 구현 PR에서 직접 수정하지 않고, 아래 절차 중 하나를 요청한다.
|
||||||
|
|
||||||
|
1. `REQ-001` 한정 1회 예외를 승인하여 provenance 문구를 정정하고 재승인한다.
|
||||||
|
2. 또는 별도 canonical artifact(새 revision)로 provenance 정정본을 생성하고 재승인한다.
|
||||||
|
|
||||||
|
## 정정 대상 문구(제안)
|
||||||
|
|
||||||
|
- 기존: `이 대화에서 REQ-001 승인과 OQ-001 선택지 A를 명시적으로 확정함`
|
||||||
|
- 제안: `이 대화에서 REQ-001 승인과 비-DIVIDEND confirm_duplicate=false 허용/true HTTP 422 결정을 명시적으로 확정함`
|
||||||
|
|
||||||
|
## 비고
|
||||||
|
|
||||||
|
본 문서는 절차 요청 artifact이며, `REQ-001` canonical bytes/revision/status를 직접 변경하지 않는다.
|
||||||
@@ -3,47 +3,43 @@
|
|||||||
Test ID: TC-REQ-001-001
|
Test ID: TC-REQ-001-001
|
||||||
Verifies:
|
Verifies:
|
||||||
- REQ-001.01-AC01
|
- REQ-001.01-AC01
|
||||||
- REQ-001.02-AC01
|
Test reference: `tests/test_v1_portfolios_manual_ledger.py::test_dividend_updates_only_profit_buffer_and_records_executed_at_and_tax`, `tests/test_v1_portfolios_manual_ledger.py::test_dividend_tax_defaults_to_zero_when_omitted`
|
||||||
Test reference: `tests/test_v1_portfolios_manual_ledger.py::test_dividend_updates_only_profit_buffer_and_records_executed_at_and_tax`, `tests/test_v1_portfolios_manual_ledger.py::test_dividend_requires_executed_at_and_rejects_fee_field`, `tests/test_v1_portfolios_manual_ledger.py::test_dividend_rejects_sub_minimum_numeric_precision`, `tests/test_v1_portfolios_manual_ledger.py::test_dividend_rejects_amount_exceeding_numeric_20_8`
|
|
||||||
Automated: true
|
Automated: true
|
||||||
|
|
||||||
Test ID: TC-REQ-001-002
|
Test ID: TC-REQ-001-002
|
||||||
Verifies:
|
Verifies:
|
||||||
- REQ-001.02-AC01
|
- REQ-001.01-AC02
|
||||||
Test reference: `tests/test_v1_portfolios_manual_ledger.py::test_dividend_requires_executed_at_and_rejects_fee_field`
|
Test reference: `tests/test_v1_portfolios_manual_ledger.py::test_dividend_requires_executed_at_and_rejects_fee_field`, `tests/test_v1_portfolios_manual_ledger.py::test_dividend_rejects_sub_minimum_numeric_precision`, `tests/test_v1_portfolios_manual_ledger.py::test_dividend_rejects_amount_exceeding_numeric_20_8`
|
||||||
Automated: true
|
Automated: true
|
||||||
|
|
||||||
Test ID: TC-REQ-001-003
|
Test ID: TC-REQ-001-003
|
||||||
Verifies:
|
Verifies:
|
||||||
- REQ-001.02-AC01
|
- REQ-001.02-AC01
|
||||||
- REQ-001.07-AC01
|
Test reference: `tests/test_v1_portfolios_manual_ledger.py::test_dividend_updates_only_profit_buffer_and_records_executed_at_and_tax`, `tests/test_v1_portfolios_manual_ledger.py::test_dividend_tax_defaults_to_zero_when_omitted`, `tests/test_bat025_portfolio_ledger_manual_metadata_migration.py::test_dividend_precision_and_reversal_round_trip_on_real_postgres`
|
||||||
Test reference: `tests/test_bat025_portfolio_ledger_manual_metadata_migration.py::test_dividend_precision_and_reversal_round_trip_on_real_postgres`, `tests/test_bat025_portfolio_ledger_manual_metadata_migration.py::test_dividend_large_integer_with_8dp_is_rejected_to_protect_double_snapshot`
|
|
||||||
Automated: true
|
Automated: true
|
||||||
|
|
||||||
Test ID: TC-REQ-001-004
|
Test ID: TC-REQ-001-004
|
||||||
Verifies:
|
Verifies:
|
||||||
- REQ-001.02-AC01
|
- REQ-001.03-AC01
|
||||||
- REQ-001.08-AC01
|
Test reference: `tests/test_v1_portfolios_manual_ledger.py::test_dividend_executed_at_uses_utc_midnight_independent_of_server_timezone`
|
||||||
Test reference: `tests/test_bat025_portfolio_ledger_manual_metadata_migration.py::test_existing_high_precision_snapshot_still_allows_legacy_manual_actions`, `tests/test_bat025_portfolio_ledger_manual_metadata_migration.py::test_large_invest_and_large_dividend_tax_metadata_are_accepted_when_snapshot_is_safe`
|
|
||||||
Automated: true
|
Automated: true
|
||||||
|
|
||||||
Test ID: TC-REQ-001-005
|
Test ID: TC-REQ-001-005
|
||||||
Verifies:
|
Verifies:
|
||||||
- REQ-001.03-AC01
|
|
||||||
Test reference: `tests/test_v1_portfolios_manual_ledger.py::test_dividend_updates_only_profit_buffer_and_records_executed_at_and_tax`
|
|
||||||
Automated: true
|
|
||||||
|
|
||||||
Test ID: TC-REQ-001-006
|
|
||||||
Verifies:
|
|
||||||
- REQ-001.04-AC01
|
- REQ-001.04-AC01
|
||||||
Test reference: `tests/test_v1_portfolios_manual_ledger.py::test_unified_ledger_includes_dividend_fields`, `tests/test_v1_portfolios_manual_ledger.py::test_unified_ledger_openapi_contains_nullable_amount`
|
Test reference: `tests/test_v1_portfolios_manual_ledger.py::test_unified_ledger_includes_dividend_fields`, `tests/test_v1_portfolios_manual_ledger.py::test_unified_ledger_openapi_contains_nullable_amount`
|
||||||
Automated: true
|
Automated: true
|
||||||
|
|
||||||
Test ID: TC-REQ-001-007
|
Test ID: TC-REQ-001-006
|
||||||
Verifies:
|
Verifies:
|
||||||
- REQ-001.05-AC01
|
- REQ-001.05-AC01
|
||||||
|
Test reference: `tests/test_v1_portfolios_manual_ledger.py::test_dividend_duplicate_requires_confirmation`, `tests/test_v1_portfolios_manual_ledger.py::test_dividend_with_different_decimal_amount_is_not_treated_as_duplicate`
|
||||||
|
Automated: true
|
||||||
|
|
||||||
|
Test ID: TC-REQ-001-007
|
||||||
|
Verifies:
|
||||||
- REQ-001.05-AC02
|
- REQ-001.05-AC02
|
||||||
Test reference: `tests/test_v1_portfolios_manual_ledger.py::test_dividend_duplicate_requires_confirmation`
|
Test reference: `tests/test_v1_portfolios_manual_ledger.py::test_dividend_duplicate_requires_confirmation`, `tests/test_v1_portfolios_manual_ledger.py::test_reversed_dividend_is_excluded_from_future_duplicate_candidate_matching`
|
||||||
Automated: true
|
Automated: true
|
||||||
|
|
||||||
Test ID: TC-REQ-001-008
|
Test ID: TC-REQ-001-008
|
||||||
@@ -55,24 +51,23 @@ Automated: true
|
|||||||
Test ID: TC-REQ-001-009
|
Test ID: TC-REQ-001-009
|
||||||
Verifies:
|
Verifies:
|
||||||
- REQ-001.06-AC01
|
- REQ-001.06-AC01
|
||||||
- REQ-001.07-AC01
|
Test reference: `tests/test_v1_portfolios_manual_ledger.py::test_dividend_reversal_decreases_profit_buffer`, `tests/test_v1_portfolios_manual_ledger.py::test_duplicate_reversal_is_rejected_without_state_change`, `tests/test_v1_portfolios_manual_ledger.py::test_dividend_reversal_insufficient_buffer_is_rejected_without_state_change`, `tests/test_bat025_portfolio_ledger_manual_metadata_migration.py::test_dividend_precision_and_reversal_round_trip_on_real_postgres`
|
||||||
Test reference: `tests/test_v1_portfolios_manual_ledger.py::test_dividend_reversal_decreases_profit_buffer`, `tests/test_bat025_portfolio_ledger_manual_metadata_migration.py::test_dividend_precision_and_reversal_round_trip_on_real_postgres`
|
|
||||||
Automated: true
|
Automated: true
|
||||||
|
|
||||||
Test ID: TC-REQ-001-010
|
Test ID: TC-REQ-001-010
|
||||||
Verifies:
|
Verifies:
|
||||||
- REQ-001.07-AC01
|
- REQ-001.07-AC01
|
||||||
Test reference: `tests/test_bat025_portfolio_ledger_manual_metadata_migration.py::test_large_existing_buffer_plus_min_dividend_is_rejected_before_snapshot_divergence`
|
Test reference: `tests/test_v1_portfolios_manual_ledger.py::test_dividend_with_different_decimal_amount_is_not_treated_as_duplicate`, `tests/test_bat025_portfolio_ledger_manual_metadata_migration.py::test_dividend_precision_and_reversal_round_trip_on_real_postgres`, `tests/test_bat025_portfolio_ledger_manual_metadata_migration.py::test_large_existing_buffer_plus_min_dividend_is_rejected_before_snapshot_divergence`
|
||||||
Automated: true
|
Automated: true
|
||||||
|
|
||||||
Test ID: TC-REQ-001-011
|
Test ID: TC-REQ-001-011
|
||||||
Verifies:
|
Verifies:
|
||||||
- REQ-001.08-AC01
|
- REQ-001.08-AC01
|
||||||
Test reference: `tests/test_v1_portfolios_manual_ledger.py::test_confirm_duplicate_false_is_accepted_for_non_dividend_actions`, `tests/test_v1_portfolios_manual_ledger.py::test_confirm_duplicate_true_is_rejected_for_non_dividend_actions`
|
Test reference: `tests/test_v1_portfolios_manual_ledger.py::test_confirm_duplicate_false_is_accepted_for_non_dividend_actions`, `tests/test_v1_portfolios_manual_ledger.py::test_confirm_duplicate_true_is_rejected_for_non_dividend_actions`, `tests/test_bat025_portfolio_ledger_manual_metadata_migration.py::test_existing_high_precision_snapshot_still_allows_legacy_manual_actions`, `tests/test_bat025_portfolio_ledger_manual_metadata_migration.py::test_large_invest_and_large_dividend_tax_metadata_are_accepted_when_snapshot_is_safe`
|
||||||
Automated: true
|
Automated: true
|
||||||
|
|
||||||
Test ID: TC-REQ-001-012
|
Test ID: TC-REQ-001-012
|
||||||
Verifies:
|
Verifies:
|
||||||
- REQ-001.09-AC01
|
- REQ-001.09-AC01
|
||||||
Test reference: `tests/test_v1_portfolios_manual_ledger.py::test_manual_write_rejects_service_token`, `tests/test_v1_portfolios_manual_ledger.py::test_owner_mismatch_returns_403`, `tests/test_v1_portfolios_manual_ledger.py::test_daemon_managed_requires_confirmation_then_records_evidence`, `tests/test_v1_portfolios_manual_ledger.py::test_manual_ledger_conflict_message_is_sanitized`
|
Test reference: `tests/test_v1_portfolios_manual_ledger.py::test_manual_write_rejects_service_token`, `tests/test_v1_portfolios_manual_ledger.py::test_owner_mismatch_returns_403`, `tests/test_v1_portfolios_manual_ledger.py::test_daemon_managed_requires_confirmation_then_records_evidence`, `tests/test_v1_portfolios_manual_ledger.py::test_manual_ledger_conflict_message_is_sanitized`, `tests/test_v1_portfolios_manual_ledger.py::test_manual_ledger_conflict_secret_is_not_rendered_in_logs`
|
||||||
Automated: true
|
Automated: true
|
||||||
|
|||||||
@@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
from contextlib import ExitStack
|
from contextlib import ExitStack
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from app.core.config import Settings, get_settings
|
from app.core.config import Settings, get_settings
|
||||||
@@ -506,6 +509,54 @@ def test_dividend_updates_only_profit_buffer_and_records_executed_at_and_tax() -
|
|||||||
assert body["source"] == "manual"
|
assert body["source"] == "manual"
|
||||||
|
|
||||||
|
|
||||||
|
def test_dividend_tax_defaults_to_zero_when_omitted() -> None:
|
||||||
|
repo = _FakeRepo(_portfolio(cash=1000, quantity=10, price=10, buffer=500))
|
||||||
|
client = _make_client(repo)
|
||||||
|
|
||||||
|
resp = client.post(
|
||||||
|
f"/v1/portfolios/{_PORTFOLIO_ID}/ledger/manual",
|
||||||
|
json={"action": "DIVIDEND", "amount": 120, "executed_at": "2026-09-01", "note": "Q3"},
|
||||||
|
headers=_auth(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resp.status_code == 201
|
||||||
|
body = resp.json()
|
||||||
|
assert body["tax"] == 0.0
|
||||||
|
assert body["cash_balance_after"] == 1000.0
|
||||||
|
assert body["asset_quantity_after"] == 10.0
|
||||||
|
assert body["profit_buffer_after"] == 620.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_dividend_executed_at_uses_utc_midnight_independent_of_server_timezone(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
repo = _FakeRepo(_portfolio())
|
||||||
|
client = _make_client(repo)
|
||||||
|
|
||||||
|
original_tz = None
|
||||||
|
if hasattr(time, "tzset"):
|
||||||
|
original_tz = os.environ.get("TZ")
|
||||||
|
monkeypatch.setenv("TZ", "Asia/Seoul")
|
||||||
|
time.tzset()
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = client.post(
|
||||||
|
f"/v1/portfolios/{_PORTFOLIO_ID}/ledger/manual",
|
||||||
|
json={"action": "DIVIDEND", "amount": 1, "executed_at": "2026-09-01"},
|
||||||
|
headers=_auth(),
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
if hasattr(time, "tzset"):
|
||||||
|
if original_tz is None:
|
||||||
|
monkeypatch.delenv("TZ", raising=False)
|
||||||
|
else:
|
||||||
|
monkeypatch.setenv("TZ", original_tz)
|
||||||
|
time.tzset()
|
||||||
|
|
||||||
|
assert resp.status_code == 201
|
||||||
|
body = resp.json()
|
||||||
|
assert body["executed_at"] == "2026-09-01T00:00:00Z"
|
||||||
|
assert body["recorded_at"] != body["executed_at"]
|
||||||
|
|
||||||
|
|
||||||
def test_dividend_requires_executed_at_and_rejects_fee_field() -> None:
|
def test_dividend_requires_executed_at_and_rejects_fee_field() -> None:
|
||||||
repo = _FakeRepo(_portfolio())
|
repo = _FakeRepo(_portfolio())
|
||||||
client = _make_client(repo)
|
client = _make_client(repo)
|
||||||
@@ -583,6 +634,51 @@ def test_dividend_duplicate_requires_confirmation() -> None:
|
|||||||
assert confirmed.status_code == 201
|
assert confirmed.status_code == 201
|
||||||
|
|
||||||
|
|
||||||
|
def test_dividend_with_different_decimal_amount_is_not_treated_as_duplicate() -> None:
|
||||||
|
repo = _FakeRepo(_portfolio())
|
||||||
|
client = _make_client(repo)
|
||||||
|
|
||||||
|
first = client.post(
|
||||||
|
f"/v1/portfolios/{_PORTFOLIO_ID}/ledger/manual",
|
||||||
|
json={"action": "DIVIDEND", "amount": "1.00000000", "executed_at": "2026-09-01", "note": "Q3"},
|
||||||
|
headers=_auth(),
|
||||||
|
)
|
||||||
|
assert first.status_code == 201
|
||||||
|
|
||||||
|
second = client.post(
|
||||||
|
f"/v1/portfolios/{_PORTFOLIO_ID}/ledger/manual",
|
||||||
|
json={"action": "DIVIDEND", "amount": "1.00000001", "executed_at": "2026-09-01", "note": "Q3"},
|
||||||
|
headers=_auth(),
|
||||||
|
)
|
||||||
|
assert second.status_code == 201
|
||||||
|
|
||||||
|
|
||||||
|
def test_reversed_dividend_is_excluded_from_future_duplicate_candidate_matching() -> None:
|
||||||
|
repo = _FakeRepo(_portfolio())
|
||||||
|
client = _make_client(repo)
|
||||||
|
|
||||||
|
first = client.post(
|
||||||
|
f"/v1/portfolios/{_PORTFOLIO_ID}/ledger/manual",
|
||||||
|
json={"action": "DIVIDEND", "amount": 50, "executed_at": "2026-09-01", "note": "Q3"},
|
||||||
|
headers=_auth(),
|
||||||
|
)
|
||||||
|
assert first.status_code == 201
|
||||||
|
|
||||||
|
reversal = client.post(
|
||||||
|
f"/v1/portfolios/{_PORTFOLIO_ID}/ledger/manual",
|
||||||
|
json={"action": "REVERSAL", "target_ledger_entry_id": first.json()["ledger_entry_id"]},
|
||||||
|
headers=_auth(),
|
||||||
|
)
|
||||||
|
assert reversal.status_code == 201
|
||||||
|
|
||||||
|
replay = client.post(
|
||||||
|
f"/v1/portfolios/{_PORTFOLIO_ID}/ledger/manual",
|
||||||
|
json={"action": "DIVIDEND", "amount": 50, "executed_at": "2026-09-01", "note": "Q3"},
|
||||||
|
headers=_auth(),
|
||||||
|
)
|
||||||
|
assert replay.status_code == 201
|
||||||
|
|
||||||
|
|
||||||
def test_confirm_duplicate_false_is_accepted_for_non_dividend_actions() -> None:
|
def test_confirm_duplicate_false_is_accepted_for_non_dividend_actions() -> None:
|
||||||
repo = _FakeRepo(_portfolio())
|
repo = _FakeRepo(_portfolio())
|
||||||
client = _make_client(repo)
|
client = _make_client(repo)
|
||||||
@@ -635,6 +731,89 @@ def test_dividend_reversal_decreases_profit_buffer() -> None:
|
|||||||
assert reversal.json()["profit_buffer_after"] == 200.0
|
assert reversal.json()["profit_buffer_after"] == 200.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_duplicate_reversal_is_rejected_without_state_change() -> None:
|
||||||
|
repo = _FakeRepo(_portfolio(buffer=200))
|
||||||
|
client = _make_client(repo)
|
||||||
|
|
||||||
|
dividend = client.post(
|
||||||
|
f"/v1/portfolios/{_PORTFOLIO_ID}/ledger/manual",
|
||||||
|
json={"action": "DIVIDEND", "amount": 80, "executed_at": "2026-09-01", "note": "Q3"},
|
||||||
|
headers=_auth(),
|
||||||
|
)
|
||||||
|
assert dividend.status_code == 201
|
||||||
|
|
||||||
|
first_reversal = client.post(
|
||||||
|
f"/v1/portfolios/{_PORTFOLIO_ID}/ledger/manual",
|
||||||
|
json={"action": "REVERSAL", "target_ledger_entry_id": dividend.json()["ledger_entry_id"]},
|
||||||
|
headers=_auth(),
|
||||||
|
)
|
||||||
|
assert first_reversal.status_code == 201
|
||||||
|
before_count = len(repo.entries)
|
||||||
|
|
||||||
|
second_reversal = client.post(
|
||||||
|
f"/v1/portfolios/{_PORTFOLIO_ID}/ledger/manual",
|
||||||
|
json={"action": "REVERSAL", "target_ledger_entry_id": dividend.json()["ledger_entry_id"]},
|
||||||
|
headers=_auth(),
|
||||||
|
)
|
||||||
|
assert second_reversal.status_code == 409
|
||||||
|
assert second_reversal.json()["detail"]["code"] == "LEDGER_ENTRY_ALREADY_REVERSED"
|
||||||
|
assert len(repo.entries) == before_count
|
||||||
|
assert repo.portfolio.profit_buffer == 200.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_dividend_reversal_insufficient_buffer_is_rejected_without_state_change() -> None:
|
||||||
|
repo = _FakeRepo(_portfolio(buffer=0, cash=1000, quantity=10, price=10))
|
||||||
|
client = _make_client(repo)
|
||||||
|
|
||||||
|
dividend = client.post(
|
||||||
|
f"/v1/portfolios/{_PORTFOLIO_ID}/ledger/manual",
|
||||||
|
json={"action": "DIVIDEND", "amount": 100, "executed_at": "2026-09-01", "note": "Q3"},
|
||||||
|
headers=_auth(),
|
||||||
|
)
|
||||||
|
assert dividend.status_code == 201
|
||||||
|
|
||||||
|
withdraw = client.post(
|
||||||
|
f"/v1/portfolios/{_PORTFOLIO_ID}/ledger/manual",
|
||||||
|
json={"action": "WITHDRAW", "amount": 100},
|
||||||
|
headers=_auth(),
|
||||||
|
)
|
||||||
|
assert withdraw.status_code == 201
|
||||||
|
assert repo.portfolio.profit_buffer == 0.0
|
||||||
|
before_count = len(repo.entries)
|
||||||
|
|
||||||
|
reversal = client.post(
|
||||||
|
f"/v1/portfolios/{_PORTFOLIO_ID}/ledger/manual",
|
||||||
|
json={"action": "REVERSAL", "target_ledger_entry_id": dividend.json()["ledger_entry_id"]},
|
||||||
|
headers=_auth(),
|
||||||
|
)
|
||||||
|
assert reversal.status_code == 409
|
||||||
|
assert reversal.json()["detail"]["code"] == "INSUFFICIENT_BUFFER"
|
||||||
|
assert len(repo.entries) == before_count
|
||||||
|
assert repo.portfolio.profit_buffer == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_manual_ledger_conflict_secret_is_not_rendered_in_logs(caplog: pytest.LogCaptureFixture) -> None:
|
||||||
|
repo = _FakeRepo(_portfolio())
|
||||||
|
client = _make_client(repo)
|
||||||
|
secret = "SENTINEL_SECRET_TOKEN_67890"
|
||||||
|
|
||||||
|
def _raise_secret(*args: Any, **kwargs: Any) -> LedgerEntry:
|
||||||
|
raise ValueError(f"synthetic persistence {secret}")
|
||||||
|
|
||||||
|
repo.record_manual_ledger = _raise_secret # type: ignore[method-assign]
|
||||||
|
caplog.clear()
|
||||||
|
caplog.set_level("INFO")
|
||||||
|
|
||||||
|
resp = client.post(
|
||||||
|
f"/v1/portfolios/{_PORTFOLIO_ID}/ledger/manual",
|
||||||
|
json={"action": "DIVIDEND", "amount": 1, "executed_at": "2026-09-01"},
|
||||||
|
headers=_auth(),
|
||||||
|
)
|
||||||
|
assert resp.status_code == 422
|
||||||
|
rendered = "\n".join(f"{record.name}:{record.getMessage()}" for record in caplog.records)
|
||||||
|
assert secret not in rendered
|
||||||
|
|
||||||
|
|
||||||
def test_unified_ledger_includes_dividend_fields() -> None:
|
def test_unified_ledger_includes_dividend_fields() -> None:
|
||||||
repo = _FakeRepo(_portfolio())
|
repo = _FakeRepo(_portfolio())
|
||||||
client = _make_client(repo)
|
client = _make_client(repo)
|
||||||
|
|||||||
Reference in New Issue
Block a user