"""PlusROI invoice generation + lifecycle (services/money/invoices_plus.py;
phase-3 slice 2b, ADR #023).

SQLite fixtures per the house style (mirrors tests/projects_plus +
tests/money/test_settlement.py): an app-owned PlusROI tenant with a
seeded client + projects, a second app-owned tenant for isolation, a
sheet-owned tenant for the write-block, SimpleNamespace owner/worker
actors (carrying BOTH ``allowed`` and ``capabilities`` so the frozen
settlement engine and the books ledger can be exercised on the same
session).
"""

import uuid
from datetime import date
from decimal import Decimal
from types import SimpleNamespace

import pytest
from sqlalchemy import create_engine, func, select
from sqlalchemy.orm import Session
from sqlalchemy.pool import StaticPool

from app.models import (
    AuditLog,
    Base,
    Invoice,
    Party,
    ProjectBilling,
    ProjectMilestone,
    Tenant,
)
from app.services import books as books_svc
from app.services import clients as clients_svc
from app.services import projects_plus as projects_svc
from app.services.errors import ServiceError
from app.services.money import invoices_plus as svc
from app.services.money import settlement

JULY = "2026-07"
JULY_1 = date(2026, 7, 1)


def pid(raw: str) -> uuid.UUID:
    return uuid.UUID(raw)


@pytest.fixture()
def s():
    engine = create_engine(
        "sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool
    )
    Base.metadata.create_all(engine)
    with Session(engine) as session:
        yield session
    engine.dispose()


APP = {
    "books_owner": "app",
    "default_currency": "CAD",
    "bookkeeping_pct": 3.0,
    "partner_split": 0.5,
}
SHEET = {"books_owner": "sheet"}


def _tenant(s, name, slug, settings):
    party = Party(kind="org", name=name, slug=slug)
    s.add(party)
    s.flush()
    tenant = Tenant(party_id=party.id, settings=settings)
    s.add(tenant)
    s.flush()
    return tenant, party


def owner(tenant_id):
    return SimpleNamespace(
        tenant_id=tenant_id,
        user_id=None,
        real_user_id=None,
        is_viewing_as=False,
        allowed=lambda cap: True,
        capabilities={"can_see_income": True},
    )


def worker(tenant_id):
    return SimpleNamespace(
        tenant_id=tenant_id,
        user_id=None,
        real_user_id=None,
        is_viewing_as=False,
        allowed=lambda cap: False,
        capabilities={"can_see_income": False},
    )


@pytest.fixture()
def fx(s):
    plusroi, plusroi_party = _tenant(s, "PlusROI", "plusroi", APP)
    other, other_party = _tenant(s, "Other Books", "other", APP)
    sheet, sheet_party = _tenant(s, "SheetCo", "sheetco", SHEET)
    s.flush()
    # a USD client with a commission owner (Rob's Customer First Design shape)
    earner = Party(kind="org", name="Customer First Design", slug="cfd")
    s.add(earner)
    s.flush()
    client = clients_svc.create_client(
        s,
        owner(plusroi.id),
        {
            "name": "Copernic",
            "currency": "USD",
            "terms": "Net 30",
            "invoice_timing": "month_end",
        },
    )
    cad_client = clients_svc.create_client(
        s, owner(plusroi.id), {"name": "MapleCo", "currency": "CAD"}
    )
    return SimpleNamespace(
        plusroi=plusroi,
        plusroi_party=plusroi_party,
        other=other,
        sheet=sheet,
        earner=earner,
        client_id=client["row"]["party_id"],
        cad_client_id=cad_client["row"]["party_id"],
    )


def _recurring(s, fx, name="Monthly PPC", amount="1500", timing="mid_month",
               client=None, currency=None, **over):
    body = {
        "client_party_id": client or fx.client_id,
        "name": name,
        "is_recurring": True,
        "cycle_months": 1,
        "billing_timing": timing,
        "amount": amount,
    }
    if currency is not None:
        body["currency"] = currency
    body.update(over)
    res = projects_svc.create_project(s, owner(fx.plusroi.id), body)
    return pid(res["row"]["project_id"])


def _milestoned(s, fx, name="Website Rebuild", client=None):
    res = projects_svc.create_project(
        s,
        owner(fx.plusroi.id),
        {
            "client_party_id": client or fx.cad_client_id,
            "name": name,
            "currency": "CAD",
            "is_recurring": False,
            "milestones": [
                {"label": "Deposit", "amount": "1000"},
                {"label": "Milestone", "amount": "2000"},
                {"label": "On completion", "amount": "2000"},
            ],
        },
    )
    return pid(res["row"]["project_id"])


def _generate(s, fx, project_id, month=JULY):
    return svc.generate_for_project(
        s, owner(fx.plusroi.id), project_id, {"month": month}
    )


def _invoice(s, invoice_id) -> Invoice:
    return s.get(Invoice, pid(invoice_id))


# ----------------------------------------------------- recurring generation


def test_recurring_generate_roundtrip(s, fx):
    project_id = _recurring(s, fx)
    res = _generate(s, fx, project_id)
    assert res["ok"] is True and res["month"] == JULY
    assert len(res["created"]) == 1
    row = res["created"][0]
    assert row["name"] == "Monthly PPC — 2026-07"
    assert row["status"] == "pending_approval"
    assert row["amount"] == "1500.00"
    assert row["currency"] == "USD"  # inherited from the client profile
    assert row["client"] == "Copernic"
    assert row["settlement_month"] == JULY
    assert row["relevant_period"] == JULY
    assert row["billing_type"] == "recurring"
    # the USD face is stamped at generation (realized-FX lands at paid)
    assert row["original_amount"] == "1500.00"
    assert row["original_currency"] == "USD"

    inv = _invoice(s, row["id"])
    assert inv.tenant_id == fx.plusroi.id
    assert inv.from_party_id == fx.plusroi_party.id
    assert str(inv.to_party_id) == fx.client_id
    assert inv.settlement_month == JULY_1


def test_recurring_generate_stamps_commission(s, fx):
    project_id = _recurring(
        s,
        fx,
        name="Commissioned",
        amount="1000",
        default_commission_pct="10",
        commission_party_id=str(fx.earner.id),
    )
    row = _generate(s, fx, project_id)["created"][0]
    inv = _invoice(s, row["id"])
    assert str(inv.commission_pct) in ("10.0000", "10.00")
    assert str(inv.commission_amount) == "100.00"
    assert inv.commission_party_id == fx.earner.id


def test_recurring_generate_duplicate_month_409(s, fx):
    project_id = _recurring(s, fx)
    _generate(s, fx, project_id)
    with pytest.raises(ServiceError) as err:
        _generate(s, fx, project_id)
    assert err.value.code == "ALREADY_GENERATED" and err.value.status == 409
    # a DIFFERENT month generates fine
    res = _generate(s, fx, project_id, month="2026-08")
    assert len(res["created"]) == 1


def test_recurring_name_suffix_on_unrelated_collision(s, fx):
    # an unrelated invoice already owns the deterministic name
    s.add(
        Invoice(
            tenant_id=fx.plusroi.id,
            name="Monthly PPC — 2026-07",
            status="paid",
            currency="CAD",
            manual_total=Decimal("1.00"),
        )
    )
    s.flush()
    project_id = _recurring(s, fx)
    row = _generate(s, fx, project_id)["created"][0]
    assert row["name"] == "Monthly PPC — 2026-07 (2)"


def test_generate_bad_month_422(s, fx):
    project_id = _recurring(s, fx)
    with pytest.raises(ServiceError) as err:
        _generate(s, fx, project_id, month="not-a-month")
    assert err.value.code == "BAD_MONTH"


# --------------------------------------- amount edit vs the project default


def test_invoice_amount_edit_leaves_project_default_alone(s, fx):
    """Rob's flow: $1,500 project → generate → correct THIS invoice to
    $2,000 → the project still defaults $1,500 next month."""
    project_id = _recurring(s, fx, amount="1500")
    row = _generate(s, fx, project_id)["created"][0]

    res = svc.update_plus_invoice(
        s, owner(fx.plusroi.id), pid(row["id"]), {"amount": "2000"}
    )
    assert res["row"]["amount"] == "2000.00"
    assert res["row"]["original_amount"] == "2000.00"  # USD face follows

    billing = s.get(ProjectBilling, project_id)
    assert str(billing.amount) == "1500.00"  # the default NEVER moved

    # next month still defaults from the project
    nxt = _generate(s, fx, project_id, month="2026-08")["created"][0]
    assert nxt["amount"] == "1500.00"


def test_amount_edit_recomputes_commission(s, fx):
    project_id = _recurring(
        s, fx, name="Commissioned", amount="1000", default_commission_pct="10"
    )
    row = _generate(s, fx, project_id)["created"][0]
    svc.update_plus_invoice(s, owner(fx.plusroi.id), pid(row["id"]), {"amount": "2000"})
    inv = _invoice(s, row["id"])
    assert str(inv.commission_amount) == "200.00"


def test_amount_locked_after_sent(s, fx):
    project_id = _recurring(s, fx, client=fx.cad_client_id, currency="CAD")
    row = _generate(s, fx, project_id)["created"][0]
    invoice_id = pid(row["id"])
    o = owner(fx.plusroi.id)
    svc.update_plus_invoice(s, o, invoice_id, {"status": "ready"})
    svc.update_plus_invoice(
        s, o, invoice_id, {"status": "sent", "external_id": "QB-1"}
    )
    with pytest.raises(ServiceError) as err:
        svc.update_plus_invoice(s, o, invoice_id, {"amount": "999"})
    assert err.value.code == "AMOUNT_LOCKED" and err.value.status == 409


# ----------------------------------------------------- milestone generation


def test_milestone_generate_all_and_idempotent(s, fx):
    project_id = _milestoned(s, fx)
    res = _generate(s, fx, project_id)
    assert [r["name"] for r in res["created"]] == [
        "Website Rebuild — Deposit",
        "Website Rebuild — Milestone",
        "Website Rebuild — On completion",
    ]
    assert [r["amount"] for r in res["created"]] == [
        "1000.00",
        "2000.00",
        "2000.00",
    ]
    assert all(r["status"] == "pending_approval" for r in res["created"])
    assert all(r["settlement_month"] == JULY for r in res["created"])
    assert all(r["billing_type"] == "milestone" for r in res["created"])

    # every milestone is stamped with ITS invoice
    milestones = s.scalars(
        select(ProjectMilestone)
        .where(ProjectMilestone.project_id == project_id)
        .order_by(ProjectMilestone.sort_order)
    ).all()
    assert [str(m.generated_invoice_id) for m in milestones] == [
        r["id"] for r in res["created"]
    ]

    # a second call generates NOTHING new
    again = _generate(s, fx, project_id, month="2026-08")
    assert again["created"] == []
    assert again["skipped_already_generated"] == 3
    total = s.scalar(
        select(func.count()).select_from(Invoice).where(
            Invoice.tenant_id == fx.plusroi.id
        )
    )
    assert total == 3


def test_milestone_generate_partial_only_ungenerated(s, fx):
    project_id = _milestoned(s, fx)
    deposit = s.scalars(
        select(ProjectMilestone).where(
            ProjectMilestone.project_id == project_id,
            ProjectMilestone.label == "Deposit",
        )
    ).one()
    marker = uuid.uuid4()
    deposit.generated_invoice_id = marker  # already generated elsewhere
    s.flush()
    res = _generate(s, fx, project_id)
    assert [r["name"] for r in res["created"]] == [
        "Website Rebuild — Milestone",
        "Website Rebuild — On completion",
    ]
    assert res["skipped_already_generated"] == 1
    s.refresh(deposit)
    assert deposit.generated_invoice_id == marker  # untouched


def test_milestone_project_without_schedule_409(s, fx):
    res = projects_svc.create_project(
        s,
        owner(fx.plusroi.id),
        {
            "client_party_id": fx.cad_client_id,
            "name": "Empty",
            "is_recurring": False,
            "milestones": [{"label": "x", "amount": "1"}],
        },
    )
    project_id = pid(res["row"]["project_id"])
    s.execute(
        ProjectMilestone.__table__.delete().where(
            ProjectMilestone.project_id == project_id
        )
    )
    s.flush()
    with pytest.raises(ServiceError) as err:
        _generate(s, fx, project_id)
    assert err.value.code == "NO_MILESTONES" and err.value.status == 409


# ------------------------------------------------------------------- batch


def test_batch_generates_per_timing_and_skips_existing(s, fx):
    end_a = _recurring(s, fx, name="End A", amount="100", timing="month_end")
    _recurring(s, fx, name="End B", amount="200", timing="month_end",
               client=fx.cad_client_id, currency="CAD")
    _recurring(s, fx, name="Mid C", amount="300", timing="mid_month")
    _milestoned(s, fx, name="Build")  # never in a recurring batch

    # End A already generated for July (single-path)
    _generate(s, fx, end_a)

    res = svc.generate_batch(
        s, owner(fx.plusroi.id), {"month": JULY, "timing": "month_end"}
    )
    by_name = {r["name"]: r for r in res["results"]}
    assert set(by_name) == {"End A", "End B"}  # Mid C + Build untouched
    assert by_name["End A"]["result"] == "skipped_exists"
    assert by_name["End B"]["result"] == "created"
    assert by_name["End B"]["invoice"]["amount"] == "200.00"
    assert res["created_count"] == 1 and res["skipped_count"] == 1

    # idempotent: the re-run creates nothing
    rerun = svc.generate_batch(
        s, owner(fx.plusroi.id), {"month": JULY, "timing": "month_end"}
    )
    assert rerun["created_count"] == 0
    assert {r["result"] for r in rerun["results"]} == {"skipped_exists"}


def test_batch_skips_archived_projects(s, fx):
    project_id = _recurring(s, fx, name="Paused", timing="month_end")
    projects_svc.update_project(
        s, owner(fx.plusroi.id), project_id, {"status": "archived"}
    )
    res = svc.generate_batch(
        s, owner(fx.plusroi.id), {"month": JULY, "timing": "month_end"}
    )
    assert res["results"] == []


def test_batch_cycle_guard(s, fx):
    """A quarterly project generates only on its due months (anchored on
    billing.start_date); with no anchor it's skipped, never over-invoiced."""
    anchored = _recurring(
        s, fx, name="Quarterly", amount="900", timing="month_end", cycle_months=3
    )
    s.get(ProjectBilling, anchored).start_date = date(2026, 1, 1)
    unanchored = _recurring(
        s, fx, name="Quarterly Unanchored", amount="900", timing="month_end",
        cycle_months=3, client=fx.cad_client_id,
    )
    assert s.get(ProjectBilling, unanchored).start_date is None
    s.flush()

    res = svc.generate_batch(
        s, owner(fx.plusroi.id), {"month": JULY, "timing": "month_end"}
    )
    by_name = {r["name"]: r for r in res["results"]}
    assert by_name["Quarterly"]["result"] == "created"  # Jan + 6 = on-cycle
    assert by_name["Quarterly Unanchored"]["result"] == "skipped_no_anchor"

    off = svc.generate_batch(
        s, owner(fx.plusroi.id), {"month": "2026-08", "timing": "month_end"}
    )
    by_name = {r["name"]: r for r in off["results"]}
    assert by_name["Quarterly"]["result"] == "skipped_off_cycle"


def test_batch_bad_timing_422(s, fx):
    with pytest.raises(ServiceError) as err:
        svc.generate_batch(
            s, owner(fx.plusroi.id), {"month": JULY, "timing": "whenever"}
        )
    assert err.value.code == "BAD_TIMING"


# --------------------------------------------------------------- lifecycle


def _cad_pending(s, fx, name="MapleCo Site"):
    project_id = _recurring(
        s, fx, name=name, amount="1000", client=fx.cad_client_id, currency="CAD"
    )
    row = _generate(s, fx, project_id)["created"][0]
    return pid(row["id"])


def test_full_lifecycle_cad(s, fx):
    invoice_id = _cad_pending(s, fx)
    o = owner(fx.plusroi.id)

    res = svc.update_plus_invoice(s, o, invoice_id, {"status": "ready"})
    assert res["row"]["status"] == "ready"

    # sent without a QuickBooks # is refused
    with pytest.raises(ServiceError) as err:
        svc.update_plus_invoice(s, o, invoice_id, {"status": "sent"})
    assert err.value.code == "EXTERNAL_ID_REQUIRED" and err.value.status == 422

    res = svc.update_plus_invoice(
        s, o, invoice_id, {"status": "sent", "external_id": "QB-2026-042"}
    )
    assert res["row"]["status"] == "sent"
    assert res["row"]["external_id"] == "QB-2026-042"
    inv = s.get(Invoice, invoice_id)
    assert inv.sent_at is not None and inv.paid_at is None

    # CAD invoice: collected is optional at paid
    res = svc.update_plus_invoice(s, o, invoice_id, {"status": "paid"})
    assert res["row"]["status"] == "paid"
    assert res["row"]["amount"] == "1000.00"  # book value untouched
    inv = s.get(Invoice, invoice_id)
    assert inv.paid_at is not None


def test_paid_usd_requires_and_records_collected_cad(s, fx):
    project_id = _recurring(s, fx, amount="1500")  # USD client
    row = _generate(s, fx, project_id)["created"][0]
    invoice_id = pid(row["id"])
    o = owner(fx.plusroi.id)
    svc.update_plus_invoice(s, o, invoice_id, {"status": "ready"})
    svc.update_plus_invoice(
        s, o, invoice_id, {"status": "sent", "external_id": "QB-7"}
    )

    with pytest.raises(ServiceError) as err:
        svc.update_plus_invoice(s, o, invoice_id, {"status": "paid"})
    assert err.value.code == "COLLECTED_AMOUNT_REQUIRED" and err.value.status == 422

    res = svc.update_plus_invoice(
        s, o, invoice_id, {"status": "paid", "collected_amount": "2050.00"}
    )
    row = res["row"]
    assert row["status"] == "paid"
    assert row["collected_amount"] == "2050.00"
    assert row["collected_currency"] == "CAD"
    # realized FX: the collected CAD IS the book value; the USD face survives
    assert row["amount"] == "2050.00"
    assert row["currency"] == "CAD"
    assert row["original_amount"] == "1500.00"
    assert row["original_currency"] == "USD"


def test_illegal_transitions_409(s, fx):
    invoice_id = _cad_pending(s, fx)
    o = owner(fx.plusroi.id)
    for target in ("sent", "paid"):  # no skipping steps
        with pytest.raises(ServiceError) as err:
            svc.update_plus_invoice(s, o, invoice_id, {"status": target})
        assert err.value.code == "ILLEGAL_TRANSITION" and err.value.status == 409

    # terminal states never move again
    svc.update_plus_invoice(s, o, invoice_id, {"status": "cancelled"})
    with pytest.raises(ServiceError) as err:
        svc.update_plus_invoice(s, o, invoice_id, {"status": "ready"})
    assert err.value.code == "ILLEGAL_TRANSITION"


def test_bw_open_invoice_never_enters_the_lifecycle(s, fx):
    """A Bowden-Works-shaped ``open`` invoice is not a PlusROI lifecycle
    state — any transition from it is refused (BW semantics preserved)."""
    inv = Invoice(
        tenant_id=fx.plusroi.id,
        name="BW-shaped",
        status="open",
        currency="CAD",
        manual_total=Decimal("10.00"),
    )
    s.add(inv)
    s.flush()
    with pytest.raises(ServiceError) as err:
        svc.update_plus_invoice(
            s, owner(fx.plusroi.id), inv.id, {"status": "ready"}
        )
    assert err.value.code == "ILLEGAL_TRANSITION" and err.value.status == 409


def test_cancel_clears_settlement_tag(s, fx):
    """The frozen engine sums by tag with NO status filter — cancelling
    must untag, or a dead invoice would pollute the month's split."""
    invoice_id = _cad_pending(s, fx)
    svc.update_plus_invoice(
        s, owner(fx.plusroi.id), invoice_id, {"status": "cancelled"}
    )
    inv = s.get(Invoice, invoice_id)
    assert inv.status == "cancelled" and inv.settlement_month is None


def test_delay_and_resume(s, fx):
    invoice_id = _cad_pending(s, fx)
    o = owner(fx.plusroi.id)
    svc.update_plus_invoice(s, o, invoice_id, {"status": "delayed"})
    inv = s.get(Invoice, invoice_id)
    assert inv.settlement_month == JULY_1  # delayed keeps its tag
    res = svc.update_plus_invoice(s, o, invoice_id, {"status": "ready"})
    assert res["row"]["status"] == "ready"


def test_same_status_patch_is_a_noop(s, fx):
    invoice_id = _cad_pending(s, fx)
    res = svc.update_plus_invoice(
        s, owner(fx.plusroi.id), invoice_id, {"status": "pending_approval"}
    )
    assert res["ok"] is True and res["changed"] is False


# ------------------------------------------------------------ gates + scope


def test_owner_gate_403(s, fx):
    project_id = _recurring(s, fx)
    w = worker(fx.plusroi.id)
    for call in (
        lambda: svc.generate_for_project(s, w, project_id, {"month": JULY}),
        lambda: svc.generate_batch(s, w, {"month": JULY, "timing": "month_end"}),
        lambda: svc.update_plus_invoice(s, w, uuid.uuid4(), {"status": "ready"}),
    ):
        with pytest.raises(ServiceError) as err:
            call()
        assert err.value.code == "NOT_ALLOWED" and err.value.status == 403


def test_sheet_owned_409_on_every_mutation(s, fx):
    o = owner(fx.sheet.id)
    for call in (
        lambda: svc.generate_for_project(s, o, uuid.uuid4(), {"month": JULY}),
        lambda: svc.generate_batch(s, o, {"month": JULY, "timing": "month_end"}),
        lambda: svc.update_plus_invoice(s, o, uuid.uuid4(), {"status": "ready"}),
    ):
        with pytest.raises(ServiceError) as err:
            call()
        assert err.value.code == "SHEET_OWNED_READ_ONLY" and err.value.status == 409


def test_tenant_isolation(s, fx):
    project_id = _recurring(s, fx)
    row = _generate(s, fx, project_id)["created"][0]

    # the other tenant can't generate for PlusROI's project…
    with pytest.raises(ServiceError) as err:
        svc.generate_for_project(
            s, owner(fx.other.id), project_id, {"month": "2026-08"}
        )
    assert err.value.code == "PROJECT_NOT_FOUND" and err.value.status == 404

    # …nor touch PlusROI's invoice…
    with pytest.raises(ServiceError) as err:
        svc.update_plus_invoice(
            s, owner(fx.other.id), pid(row["id"]), {"status": "ready"}
        )
    assert err.value.code == "INVOICE_NOT_FOUND" and err.value.status == 404

    # …and its batch sees no PlusROI projects.
    res = svc.generate_batch(
        s, owner(fx.other.id), {"month": JULY, "timing": "mid_month"}
    )
    assert res["results"] == []


def test_every_mutation_is_audited(s, fx):
    project_id = _recurring(s, fx, client=fx.cad_client_id, currency="CAD")
    row = _generate(s, fx, project_id)["created"][0]
    svc.update_plus_invoice(
        s, owner(fx.plusroi.id), pid(row["id"]), {"status": "ready"}
    )
    svc.generate_batch(
        s, owner(fx.plusroi.id), {"month": JULY, "timing": "mid_month"}
    )
    actions = set(
        s.scalars(
            select(AuditLog.action).where(
                AuditLog.tenant_id == fx.plusroi.id,
                AuditLog.action.like("invoices_plus.%"),
            )
        ).all()
    )
    assert actions == {
        "invoices_plus.generate",
        "invoices_plus.update",
        "invoices_plus.generate_batch",
    }
    update_row = s.scalars(
        select(AuditLog).where(AuditLog.action == "invoices_plus.update")
    ).one()
    assert update_row.before["invoice"]["status"] == "pending_approval"
    assert update_row.after["invoice"]["status"] == "ready"


# --------------------------------------- the frozen engine + the books list


def test_generated_invoice_shows_in_books_ledger(s, fx):
    project_id = _recurring(s, fx, client=fx.cad_client_id, currency="CAD")
    row = _generate(s, fx, project_id)["created"][0]
    res = books_svc.list_invoices(s, owner(fx.plusroi.id), month=JULY)
    assert res["total_count"] == 1
    listed = res["rows"][0]
    assert listed["id"] == row["id"]
    assert listed["status"] == "pending_approval"
    assert listed["amount"] == "1500.00"
    assert listed["collected_amount"] is None  # the new pair is surfaced


def test_settlement_engine_reconciles_generated_invoices(s, fx):
    """The FROZEN engine (untouched) sums a generated+tagged month
    correctly: gross = CAD book values (incl. realized-FX collected CAD),
    commissions = the stamped commission."""
    cad = _recurring(
        s, fx, name="CAD Retainer", amount="1000", client=fx.cad_client_id,
        currency="CAD", default_commission_pct="10",
    )
    usd = _recurring(s, fx, name="USD Retainer", amount="1500")
    _generate(s, fx, cad)
    usd_row = _generate(s, fx, usd)["created"][0]

    o = owner(fx.plusroi.id)
    invoice_id = pid(usd_row["id"])
    svc.update_plus_invoice(s, o, invoice_id, {"status": "ready"})
    svc.update_plus_invoice(
        s, o, invoice_id, {"status": "sent", "external_id": "QB-9"}
    )
    svc.update_plus_invoice(
        s, o, invoice_id, {"status": "paid", "collected_amount": "2050.00"}
    )

    detail = settlement.settlement_detail(s, o, JULY)
    # gross = 1000.00 (CAD book) + 2050.00 (realized CAD) — never the USD face
    assert detail["gross"] == "3050.00"
    assert detail["commissions"] == "100.00"
    assert detail["invoice_count"] == 2
