[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(
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)),
):
page = await get_payments_paginated(
wallet_id=key_info.wallet.id,
filters=filters,
)
if not recheck_pending:
return page
for payment in page.data:
if payment.pending:
await update_pending_payment(payment)

View file

@ -522,7 +522,7 @@ class Filter(BaseModel, Generic[TFilterModel]):
if field in model.__fields__:
compare_field = model.__fields__[field]
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(",")]
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 ""
stmt = []
for key in self.values.keys() if self.values else []:
clean_key = key.split("__")[0]
if self.model and self.model.__fields__[clean_key].type_ == datetime:
if self.model and self.model.__fields__[self.field].type_ == datetime:
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:
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)
else:
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', {
template: '#lnbits-payment-list',
props: ['update', 'lazy', 'wallet', 'paymentFilter'],
props: ['wallet', 'paymentFilter'],
mixins: [window.windowMixin],
data() {
return {
@ -198,6 +198,7 @@ window.app.component('lnbits-payment-list', {
this.payments = response.data.data.map(obj => {
return LNbits.map.payment(obj)
})
this.recheckPendingPayments()
})
.catch(err => {
this.paymentsTable.loading = false
@ -244,6 +245,45 @@ window.app.component('lnbits-payment-list', {
})
.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) {
this.hodlInvoice.show = true
this.hodlInvoice.preimage = ''
@ -423,23 +463,11 @@ window.app.component('lnbits-payment-list', {
this.fetchPayments()
}
},
lazy(newVal) {
if (newVal === true) this.fetchPayments()
},
update() {
this.fetchPayments()
},
'g.updatePayments'() {
this.fetchPayments()
},
'g.wallet': {
handler(newWallet) {
this.fetchPayments()
},
deep: true
}
},
created() {
if (this.lazy === undefined) this.fetchPayments()
this.fetchPayments()
}
})

View file

@ -402,7 +402,7 @@ window.PageWallet = {
)
.then(response => {
dismissPaymentMsg()
this.updatePayments = !this.updatePayments
this.g.updatePayments = !this.g.updatePayments
this.parse.show = false
if (response.data.status == 'success') {
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)
window.app = Vue.createApp({
el: '#vue',
mixins: [window.windowMixin],
data() {
return {
updatePayments: false
}
}
mixins: [window.windowMixin]
})
</script>
{%- endmacro %}

View file

@ -172,7 +172,6 @@
<q-card class="wallet-card">
<q-card-section>
<lnbits-payment-list
:update="updatePayments"
:expand-details="expandDetails"
:payment-filter="paymentFilter"
></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
paginated = response.json()
assert len(paginated["data"]) == 2
data = paginated["data"]
assert len(data) == 2
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
async def test_get_payments_history(client, inkey_fresh_headers_to, fake_payments):