Compare commits

...
6 changed files with 82 additions and 36 deletions
-2
View File
@@ -25,7 +25,6 @@ from .payments import (
DateTrunc,
check_internal,
create_payment,
delete_expired_invoices,
delete_wallet_payment,
get_latest_payments_by_extension,
get_payment,
@@ -103,7 +102,6 @@ __all__ = [
"delete_accounts_no_wallets",
"delete_admin_settings",
"delete_dbversion",
"delete_expired_invoices",
"delete_installed_extension",
"delete_tinyurl",
"delete_unused_wallets",
-26
View File
@@ -238,32 +238,6 @@ async def get_payments_status_count() -> PaymentsStatusCount:
)
async def delete_expired_invoices(
conn: Connection | None = None,
) -> None:
# first we delete all invoices older than one month
await (conn or db).execute(
# Timestamp placeholder is safe from SQL injection (not user input)
f"""
DELETE FROM apipayments
WHERE status = :status AND amount > 0
AND time < {db.timestamp_placeholder("delta")}
""", # noqa: S608
{"status": f"{PaymentState.PENDING}", "delta": int(time() - 2592000)},
)
# then we delete all invoices whose expiry date is in the past
await (conn or db).execute(
# Timestamp placeholder is safe from SQL injection (not user input)
f"""
DELETE FROM apipayments
WHERE status = :status AND amount > 0
AND expiry < {db.timestamp_placeholder("now")}
""", # noqa: S608
{"status": f"{PaymentState.PENDING}", "now": int(time())},
)
async def create_payment(
checking_id: str,
data: CreatePayment,
+7
View File
@@ -374,6 +374,13 @@ async def update_pending_payments(wallet_id: str):
async def update_pending_payment(
payment: Payment, conn: Connection | None = None
) -> Payment:
if payment.is_in and payment.is_expired:
payment.status = PaymentState.FAILED
payment.labels.append("expired")
await update_payment(payment, conn=conn)
logger.info(f"invoice {payment.checking_id} expired, marked as failed")
return payment
status = await check_payment_status(payment)
if status.failed:
payment.status = PaymentState.FAILED
+6 -5
View File
@@ -497,7 +497,7 @@ class Filter(BaseModel, Generic[TFilterModel]):
validated, errors = compare_field.validate(raw_value, {}, loc="none")
if errors:
raise ValidationError(errors=[errors], model=model)
values[f"{field}__{index}"] = validated
values[f"{field}__{i}_{index}"] = validated
else:
raise ValueError("Unknown filter field")
@@ -510,11 +510,12 @@ class Filter(BaseModel, Generic[TFilterModel]):
for key in self.values.keys() if self.values else []:
if self.model and self.model.__fields__[self.field].type_ == datetime:
placeholder = compat_timestamp_placeholder(key)
stmt.append(f"{prefix}{self.field} {self.op.as_sql} {placeholder}")
if self.op in {Operator.INCLUDE, Operator.EXCLUDE}:
stmt.append(f":{key}")
else:
stmt.append(f"{prefix}{self.field} {self.op.as_sql} :{key}")
placeholder = f":{key}"
if self.op in {Operator.INCLUDE, Operator.EXCLUDE}:
stmt.append(placeholder)
else:
stmt.append(f"{prefix}{self.field} {self.op.as_sql} {placeholder}")
if self.op in {Operator.INCLUDE, Operator.EXCLUDE}:
statement = f"{prefix}{self.field} {self.op.as_sql} ({', '.join(stmt)})"
+35 -3
View File
@@ -1,4 +1,4 @@
from datetime import date, timezone
from datetime import date, datetime, timezone
import pytest
@@ -9,9 +9,41 @@ from lnbits.core.crud import (
get_wallet_for_key,
)
from lnbits.core.crud.payments import get_payment
from lnbits.core.models import CreateInvoice
from lnbits.core.models import CreateInvoice, PaymentFilters
from lnbits.core.services.payments import create_wallet_invoice
from lnbits.db import POSTGRES
from lnbits.db import POSTGRES, SQLITE, Filter, Filters
@pytest.mark.parametrize(
("db_type", "lower_statement", "upper_statement"),
[
(
POSTGRES,
"(time >= to_timestamp(:time__0_0))",
"(time <= to_timestamp(:time__1_0))",
),
(SQLITE, "(time >= :time__0_0)", "(time <= :time__1_0)"),
],
)
def test_datetime_filter_uses_database_timestamp_placeholder(
monkeypatch, db_type, lower_statement, upper_statement
):
monkeypatch.setattr("lnbits.db.DB_TYPE", db_type)
lower_bound = Filter.parse_query(
"time[ge]", ["2026-06-16T00:00:00"], PaymentFilters, 0
)
upper_bound = Filter.parse_query(
"time[le]", ["2026-06-23T23:59:59"], PaymentFilters, 1
)
filters = Filters(filters=[lower_bound, upper_bound], model=PaymentFilters)
values = filters.values()
assert isinstance(values["time__0_0"], datetime)
assert values["time__0_0"] == datetime(2026, 6, 16)
assert values["time__1_0"] == datetime(2026, 6, 23, 23, 59, 59)
assert filters.where() == f"WHERE {lower_statement} AND {upper_statement}"
assert lower_bound.statement == lower_statement
assert upper_bound.statement == upper_statement
@pytest.mark.anyio
+34
View File
@@ -111,6 +111,36 @@ async def test_update_pending_payment_and_bulk_pending_updates(mocker: MockerFix
assert bulk_statuses == {PaymentState.FAILED, PaymentState.SUCCESS}
@pytest.mark.anyio
async def test_update_pending_payment_marks_expired_incoming_invoice_failed(
app,
mocker: MockerFixture,
):
wallet = await _create_wallet()
checking_id = await _create_payment(
wallet,
expiry=datetime.now(timezone.utc) - timedelta(seconds=1),
labels=["test"],
)
payment = await get_payment(checking_id)
check_status_mock = mocker.patch(
"lnbits.core.services.payments.check_payment_status",
mocker.AsyncMock(
side_effect=AssertionError("expired invoices should not be checked")
),
)
updated_payment = await update_pending_payment(payment)
assert updated_payment.status == PaymentState.FAILED
assert updated_payment.labels == ["test", "expired"]
check_status_mock.assert_not_awaited()
stored_payment = await get_payment(checking_id)
assert stored_payment.status == PaymentState.FAILED
assert stored_payment.labels == ["test", "expired"]
@pytest.mark.anyio
async def test_check_pending_payments_skips_voidwallet_and_updates_recent_items(
mocker: MockerFixture,
@@ -451,6 +481,8 @@ async def _create_payment(
payment_hash: str | None = None,
fee: int = 0,
time: datetime | None = None,
expiry: datetime | None = None,
labels: list[str] | None = None,
) -> str:
checking_id = checking_id or f"checking_{uuid4().hex[:8]}"
payment_hash = payment_hash or uuid4().hex
@@ -462,7 +494,9 @@ async def _create_payment(
bolt11=f"bolt11-{checking_id}",
amount_msat=amount_msat,
memo="memo",
expiry=expiry,
fee=fee,
labels=labels,
),
status=status,
)