fix: mark expired incoming invoices as failed and add periodic cleanup (#3867)
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
This commit is contained in:
@@ -25,7 +25,6 @@ from .payments import (
|
|||||||
DateTrunc,
|
DateTrunc,
|
||||||
check_internal,
|
check_internal,
|
||||||
create_payment,
|
create_payment,
|
||||||
delete_expired_invoices,
|
|
||||||
delete_wallet_payment,
|
delete_wallet_payment,
|
||||||
get_latest_payments_by_extension,
|
get_latest_payments_by_extension,
|
||||||
get_payment,
|
get_payment,
|
||||||
@@ -103,7 +102,6 @@ __all__ = [
|
|||||||
"delete_accounts_no_wallets",
|
"delete_accounts_no_wallets",
|
||||||
"delete_admin_settings",
|
"delete_admin_settings",
|
||||||
"delete_dbversion",
|
"delete_dbversion",
|
||||||
"delete_expired_invoices",
|
|
||||||
"delete_installed_extension",
|
"delete_installed_extension",
|
||||||
"delete_tinyurl",
|
"delete_tinyurl",
|
||||||
"delete_unused_wallets",
|
"delete_unused_wallets",
|
||||||
|
|||||||
@@ -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(
|
async def create_payment(
|
||||||
checking_id: str,
|
checking_id: str,
|
||||||
data: CreatePayment,
|
data: CreatePayment,
|
||||||
|
|||||||
@@ -374,6 +374,13 @@ async def update_pending_payments(wallet_id: str):
|
|||||||
async def update_pending_payment(
|
async def update_pending_payment(
|
||||||
payment: Payment, conn: Connection | None = None
|
payment: Payment, conn: Connection | None = None
|
||||||
) -> Payment:
|
) -> 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)
|
status = await check_payment_status(payment)
|
||||||
if status.failed:
|
if status.failed:
|
||||||
payment.status = PaymentState.FAILED
|
payment.status = PaymentState.FAILED
|
||||||
|
|||||||
@@ -111,6 +111,36 @@ async def test_update_pending_payment_and_bulk_pending_updates(mocker: MockerFix
|
|||||||
assert bulk_statuses == {PaymentState.FAILED, PaymentState.SUCCESS}
|
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
|
@pytest.mark.anyio
|
||||||
async def test_check_pending_payments_skips_voidwallet_and_updates_recent_items(
|
async def test_check_pending_payments_skips_voidwallet_and_updates_recent_items(
|
||||||
mocker: MockerFixture,
|
mocker: MockerFixture,
|
||||||
@@ -451,6 +481,8 @@ async def _create_payment(
|
|||||||
payment_hash: str | None = None,
|
payment_hash: str | None = None,
|
||||||
fee: int = 0,
|
fee: int = 0,
|
||||||
time: datetime | None = None,
|
time: datetime | None = None,
|
||||||
|
expiry: datetime | None = None,
|
||||||
|
labels: list[str] | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
checking_id = checking_id or f"checking_{uuid4().hex[:8]}"
|
checking_id = checking_id or f"checking_{uuid4().hex[:8]}"
|
||||||
payment_hash = payment_hash or uuid4().hex
|
payment_hash = payment_hash or uuid4().hex
|
||||||
@@ -462,7 +494,9 @@ async def _create_payment(
|
|||||||
bolt11=f"bolt11-{checking_id}",
|
bolt11=f"bolt11-{checking_id}",
|
||||||
amount_msat=amount_msat,
|
amount_msat=amount_msat,
|
||||||
memo="memo",
|
memo="memo",
|
||||||
|
expiry=expiry,
|
||||||
fee=fee,
|
fee=fee,
|
||||||
|
labels=labels,
|
||||||
),
|
),
|
||||||
status=status,
|
status=status,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user