[perf] pending payments check (#3565)

Co-authored-by: dni  <office@dnilabs.com>
This commit is contained in:
Vlad Stan 2025-11-25 14:09:57 +02:00 committed by GitHub
parent 33e2fc2ea8
commit 0910687328
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 74 additions and 30 deletions

View file

@ -179,12 +179,18 @@ async def api_payments_daily_stats(
) )
async def api_payments_paginated( async def api_payments_paginated(
key_info: WalletTypeInfo = Depends(require_invoice_key), key_info: WalletTypeInfo = Depends(require_invoice_key),
recheck_pending: bool = Query(
False, description="Force check and update of pending payments."
),
filters: Filters = Depends(parse_filters(PaymentFilters)), filters: Filters = Depends(parse_filters(PaymentFilters)),
): ):
page = await get_payments_paginated( page = await get_payments_paginated(
wallet_id=key_info.wallet.id, wallet_id=key_info.wallet.id,
filters=filters, filters=filters,
) )
if not recheck_pending:
return page
for payment in page.data: for payment in page.data:
if payment.pending: if payment.pending:
await update_pending_payment(payment) await update_pending_payment(payment)

View file

@ -522,7 +522,7 @@ class Filter(BaseModel, Generic[TFilterModel]):
if field in model.__fields__: if field in model.__fields__:
compare_field = model.__fields__[field] compare_field = model.__fields__[field]
values: dict = {} values: dict = {}
if op in {Operator.EVERY, Operator.ANY}: if op in {Operator.EVERY, Operator.ANY, Operator.INCLUDE, Operator.EXCLUDE}:
raw_values = [v for rv in raw_values for v in rv.split(",")] raw_values = [v for rv in raw_values for v in rv.split(",")]
for index, raw_value in enumerate(raw_values): for index, raw_value in enumerate(raw_values):
@ -540,14 +540,17 @@ class Filter(BaseModel, Generic[TFilterModel]):
prefix = f"{self.table_name}." if self.table_name else "" prefix = f"{self.table_name}." if self.table_name else ""
stmt = [] stmt = []
for key in self.values.keys() if self.values else []: for key in self.values.keys() if self.values else []:
clean_key = key.split("__")[0] if self.model and self.model.__fields__[self.field].type_ == datetime:
if self.model and self.model.__fields__[clean_key].type_ == datetime:
placeholder = compat_timestamp_placeholder(key) placeholder = compat_timestamp_placeholder(key)
stmt.append(f"{prefix}{clean_key} {self.op.as_sql} {placeholder}") stmt.append(f"{prefix}{self.field} {self.op.as_sql} {placeholder}")
if self.op in {Operator.INCLUDE, Operator.EXCLUDE}:
stmt.append(f":{key}")
else: else:
stmt.append(f"{prefix}{clean_key} {self.op.as_sql} :{key}") stmt.append(f"{prefix}{self.field} {self.op.as_sql} :{key}")
if self.op == Operator.EVERY: if self.op in {Operator.INCLUDE, Operator.EXCLUDE}:
statement = f"{prefix}{self.field} {self.op.as_sql} ({', '.join(stmt)})"
elif self.op == Operator.EVERY:
statement = " AND ".join(stmt) statement = " AND ".join(stmt)
else: else:
statement = " OR ".join(stmt) statement = " OR ".join(stmt)

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,6 @@
window.app.component('lnbits-payment-list', { window.app.component('lnbits-payment-list', {
template: '#lnbits-payment-list', template: '#lnbits-payment-list',
props: ['update', 'lazy', 'wallet', 'paymentFilter'], props: ['wallet', 'paymentFilter'],
mixins: [window.windowMixin], mixins: [window.windowMixin],
data() { data() {
return { return {
@ -198,6 +198,7 @@ window.app.component('lnbits-payment-list', {
this.payments = response.data.data.map(obj => { this.payments = response.data.data.map(obj => {
return LNbits.map.payment(obj) return LNbits.map.payment(obj)
}) })
this.recheckPendingPayments()
}) })
.catch(err => { .catch(err => {
this.paymentsTable.loading = false this.paymentsTable.loading = false
@ -244,6 +245,45 @@ window.app.component('lnbits-payment-list', {
}) })
.catch(LNbits.utils.notifyApiError) .catch(LNbits.utils.notifyApiError)
}, },
recheckPendingPayments() {
const pendingPayments = this.payments.filter(p => p.status === 'pending')
if (pendingPayments.length === 0) return
const params = [
'recheck_pending=true',
'checking_id[in]=' + pendingPayments.map(p => p.checking_id).join(',')
].join('&')
LNbits.api
.getPayments(this.currentWallet, params)
.then(response => {
let updatedPayments = 0
response.data.data.forEach(updatedPayment => {
if (updatedPayment.status !== 'pending') {
const index = this.payments.findIndex(
p => p.checking_id === updatedPayment.checking_id
)
if (index !== -1) {
this.payments.splice(
index,
1,
LNbits.map.payment(updatedPayment)
)
updatedPayments += 1
}
}
})
if (updatedPayments > 0) {
Quasar.Notify.create({
type: 'positive',
message: this.$t('payment_successful')
})
}
})
.catch(err => {
console.warn(err)
})
},
showHoldInvoiceDialog(payment) { showHoldInvoiceDialog(payment) {
this.hodlInvoice.show = true this.hodlInvoice.show = true
this.hodlInvoice.preimage = '' this.hodlInvoice.preimage = ''
@ -423,23 +463,11 @@ window.app.component('lnbits-payment-list', {
this.fetchPayments() this.fetchPayments()
} }
}, },
lazy(newVal) {
if (newVal === true) this.fetchPayments()
},
update() {
this.fetchPayments()
},
'g.updatePayments'() { 'g.updatePayments'() {
this.fetchPayments() this.fetchPayments()
},
'g.wallet': {
handler(newWallet) {
this.fetchPayments()
},
deep: true
} }
}, },
created() { created() {
if (this.lazy === undefined) this.fetchPayments() this.fetchPayments()
} }
}) })

View file

@ -402,7 +402,7 @@ window.PageWallet = {
) )
.then(response => { .then(response => {
dismissPaymentMsg() dismissPaymentMsg()
this.updatePayments = !this.updatePayments this.g.updatePayments = !this.g.updatePayments
this.parse.show = false this.parse.show = false
if (response.data.status == 'success') { if (response.data.status == 'success') {
Quasar.Notify.create({ Quasar.Notify.create({

View file

@ -18,12 +18,7 @@
//Needed for Vue to create the app on first load (although called on every page, its only loaded once) //Needed for Vue to create the app on first load (although called on every page, its only loaded once)
window.app = Vue.createApp({ window.app = Vue.createApp({
el: '#vue', el: '#vue',
mixins: [window.windowMixin], mixins: [window.windowMixin]
data() {
return {
updatePayments: false
}
}
}) })
</script> </script>
{%- endmacro %} {%- endmacro %}

View file

@ -172,7 +172,6 @@
<q-card class="wallet-card"> <q-card class="wallet-card">
<q-card-section> <q-card-section>
<lnbits-payment-list <lnbits-payment-list
:update="updatePayments"
:expand-details="expandDetails" :expand-details="expandDetails"
:payment-filter="paymentFilter" :payment-filter="paymentFilter"
></lnbits-payment-list> ></lnbits-payment-list>

View file

@ -418,9 +418,22 @@ async def test_get_payments_paginated(client, inkey_fresh_headers_to, fake_payme
) )
assert response.status_code == 200 assert response.status_code == 200
paginated = response.json() paginated = response.json()
assert len(paginated["data"]) == 2 data = paginated["data"]
assert len(data) == 2
assert paginated["total"] == len(fake_data) assert paginated["total"] == len(fake_data)
checking_id_list = [payment["checking_id"] for payment in data]
params = {"checking_id[in]": ",".join(checking_id_list)}
response = await client.get(
"/api/v1/payments/paginated",
params=params,
headers=inkey_fresh_headers_to,
)
data = response.json()["data"]
assert len(data) == 2
for payment in data:
assert payment["checking_id"] in checking_id_list
@pytest.mark.anyio @pytest.mark.anyio
async def test_get_payments_history(client, inkey_fresh_headers_to, fake_payments): async def test_get_payments_history(client, inkey_fresh_headers_to, fake_payments):