From 51e585c78cbe0cd94896264143d043248a092421 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 31 Jul 2026 16:22:55 +0300 Subject: [PATCH] fix: direct fiat payments (#4116) Co-authored-by: Arc <33088785+arcbtc@users.noreply.github.com> --- lnbits/core/models/payments.py | 3 +++ lnbits/core/services/payments.py | 2 ++ tests/api/test_api.py | 34 ++++++++++++++++++++++++++++ tests/unit/test_services_payments.py | 33 +++++++++++++++++++++++++++ 4 files changed, 72 insertions(+) diff --git a/lnbits/core/models/payments.py b/lnbits/core/models/payments.py index 8cd82e55d..4c3ebb4bb 100644 --- a/lnbits/core/models/payments.py +++ b/lnbits/core/models/payments.py @@ -275,6 +275,9 @@ class CreateInvoice(BaseModel): labels: list[str] = [] external_id: str | None = Query(default=None, max_length=256) + def is_fiat_subscription(self) -> bool: + return (self.extra or {}).get("fiat_method") == "subscription" + @validator("payment_hash") def check_hex(cls, v): if v: diff --git a/lnbits/core/services/payments.py b/lnbits/core/services/payments.py index 38439c876..95a7eb8ba 100644 --- a/lnbits/core/services/payments.py +++ b/lnbits/core/services/payments.py @@ -118,6 +118,8 @@ async def create_payment_request( Create a lightning invoice or a fiat payment request. """ if invoice_data.fiat_provider: + if invoice_data.is_fiat_subscription(): + raise ValueError("Cannot create direct fiat subscription payments.") return await create_fiat_invoice(wallet_id, invoice_data) return await create_wallet_invoice(wallet_id, invoice_data) diff --git a/tests/api/test_api.py b/tests/api/test_api.py index 2e111360f..2401c71fe 100644 --- a/tests/api/test_api.py +++ b/tests/api/test_api.py @@ -233,6 +233,40 @@ async def test_create_fiat_invoice( assert invoice["extra"]["fiat_payment_request"] == fiat_payment_request +@pytest.mark.anyio +async def test_create_fiat_subscription_invoice_rejected( + client, inkey_headers_to, mocker: MockerFixture +): + fiat_mock = mocker.patch( + "lnbits.core.services.payments.create_fiat_invoice", + AsyncMock(), + ) + + response = await client.post( + "/api/v1/payments", + headers=inkey_headers_to, + json={ + "unit": "USD", + "out": False, + "amount": 2100, + "fiat_provider": "stripe", + "extra": { + "fiat_method": "subscription", + "subscription": { + "checking_id": "fiat_stripe_cs_paid_session", + "payment_request": "", + }, + }, + }, + ) + + assert response.status_code == 400 + assert response.json()["detail"] == ( + "Cannot create direct fiat subscription payments." + ) + fiat_mock.assert_not_awaited() + + @pytest.mark.anyio @pytest.mark.parametrize("currency", ("msat", "RRR")) async def test_create_invoice_validates_used_currency( diff --git a/tests/unit/test_services_payments.py b/tests/unit/test_services_payments.py index a55ee06b4..e2e3e2d49 100644 --- a/tests/unit/test_services_payments.py +++ b/tests/unit/test_services_payments.py @@ -75,6 +75,39 @@ async def test_create_payment_request_routes_by_invoice_type(mocker: MockerFixtu fiat_mock.assert_awaited_once() +@pytest.mark.anyio +@pytest.mark.parametrize("fiat_provider", ("stripe", "square", "paypal")) +async def test_create_payment_request_rejects_fiat_subscription( + fiat_provider: str, mocker: MockerFixture +): + fiat_mock = mocker.patch( + "lnbits.core.services.payments.create_fiat_invoice", + mocker.AsyncMock(), + ) + + with pytest.raises( + ValueError, + match="Cannot create direct fiat subscription payments.", + ): + await create_payment_request( + "wallet-1", + CreateInvoice( + unit="USD", + amount=2100, + fiat_provider=fiat_provider, + extra={ + "fiat_method": "subscription", + "subscription": { + "checking_id": "fiat_stripe_cs_paid_session", + "payment_request": "", + }, + }, + ), + ) + + fiat_mock.assert_not_awaited() + + @pytest.mark.anyio async def test_update_pending_payment_and_bulk_pending_updates(mocker: MockerFixture): wallet = await _create_wallet()