Fix payment priority order and refund processing

- Fix payment_priority() to include ALL refund statuses (EXPIRED_REFUNDED, UNDERPAID_REFUNDED, OUT_OF_STOCK_REFUNDED) not just DOUBLEPAY_REFUND
- Auto-sweep operations (CONFIRMED, CONFIRMED_OVERPAID, NO_REFUND) now have lowest priority (3)
- Refactor to use match/case statements for better readability
- Add comprehensive docstring explaining priority system
- Ensure refunds processed before auto-sweep for customer service priority

Priority order: 0=refunds, 1=incoming, 2=other (pending/terminals), 3=auto-sweep
This commit is contained in:
Russell Ballestrini 2025-09-30 08:05:08 -04:00
parent e04e91075b
commit 73bbf04fd4
5 changed files with 198 additions and 13 deletions

84
debug_stuck_refund.py Normal file
View file

@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""
Debug script to manually process the stuck refund payment.
"""
import sys
import logging
sys.path.insert(0, '/home/fox/git/make_post_sell')
# Enable detailed logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger()
try:
from pyramid.paster import get_appsettings
from make_post_sell.lib.crypto_watcher import process_payment, get_crypto_client
from make_post_sell.models.crypto_payment import CryptoPayment
from make_post_sell.models import get_session_factory, get_engine
from pyramid.testing import DummyRequest
import transaction
# Get settings
settings = get_appsettings('development.ini', name='main')
# Create session factory and get session
session_factory = get_session_factory(get_engine(settings))
dbsession = session_factory()
# Get the stuck payment
payment_id = '9cee243c9df211f094ce9e7bc9aa750d'
payment = dbsession.query(CryptoPayment).filter(CryptoPayment.id == payment_id).first()
if not payment:
print(f"Payment {payment_id} not found!")
sys.exit(1)
print(f"Found payment: {payment.id}")
print(f"Status: {payment.status}")
print(f"Received amount: {payment.received_amount}")
print(f"Current confirmations: {payment.current_confirmations}")
print(f"Required confirmations: {payment.confirmations_required}")
print(f"Tx hashes: {payment.tx_hashes}")
print(f"Refund address: {payment.refund_address}")
# Create mock request
request = DummyRequest()
request.dbsession = dbsession
request.registry = type('MockRegistry', (), {'settings': settings})()
request.tm = transaction.manager
# Get crypto client
try:
client = get_crypto_client(payment.coin_type)
print(f"Got crypto client: {client}")
except Exception as e:
print(f"Failed to get crypto client: {e}")
sys.exit(1)
# Try to process the payment
print("\n=== Attempting to process payment ===")
try:
# Process with empty incoming transfers to force confirmation update
process_payment(request, payment, [], client=client)
print("Payment processing completed successfully")
except Exception as e:
print(f"Error processing payment: {e}")
import traceback
traceback.print_exc()
# Check if payment status changed
dbsession.refresh(payment)
print(f"\nAfter processing:")
print(f"Status: {payment.status}")
print(f"Current confirmations: {payment.current_confirmations}")
print(f"Refund tx hash: {payment.refund_tx_hash}")
print(f"Refund amount: {payment.refund_amount}")
except Exception as e:
print(f"Script error: {e}")
import traceback
traceback.print_exc()
finally:
if 'dbsession' in locals():
dbsession.close()

View file

@ -3551,15 +3551,35 @@ def run_once(env, interval):
# 1. Duplicate refunds (need wallet funds)
# 2. Regular payments (might trigger auto-sweep which drains wallet)
def payment_priority(payment):
if payment.status == CryptoPayment.STATUS_DOUBLEPAY_REFUND:
return 0 # Highest priority - process refunds first
elif payment.status in [
CryptoPayment.STATUS_RECEIVED,
CryptoPayment.STATUS_CONFIRMED,
]:
return 1 # Lower priority - might trigger auto-sweep
else:
return 2 # Lowest priority - other statuses
"""
Payment processing priority since wallets are partitioned (no fund conflicts).
0=refunds (customer service), 1=incoming, 2=other (pending, terminals), 3=auto-sweep.
"""
match payment.status:
# Priority 0: Refunds - highest priority
case (
CryptoPayment.STATUS_DOUBLEPAY_REFUND
| CryptoPayment.STATUS_EXPIRED_REFUNDED
| CryptoPayment.STATUS_UNDERPAID_REFUNDED
| CryptoPayment.STATUS_OUT_OF_STOCK_REFUNDED
):
return 0 # Process refunds first
# Priority 1: Incoming payments
case CryptoPayment.STATUS_RECEIVED:
return 1 # Process new payments
# Priority 3: Auto-sweep operations - lowest priority
case (
CryptoPayment.STATUS_CONFIRMED
| CryptoPayment.STATUS_CONFIRMED_OVERPAID
| CryptoPayment.STATUS_NO_REFUND
):
return 3 # Auto-sweep operations last
# Priority 2: All other statuses (must be last)
case _:
return 2 # Everything else in middle
sorted_payments = sorted(coin_payments, key=payment_priority)
logger.info(

View file

@ -51,7 +51,7 @@ class StripeUserShop(RBase, Base):
# Check if Stripe is configured for this shop
if self.shop.stripe is None:
return []
return self.shop.stripe.Customer.list_payment_methods(
self.stripe_customer, type="card"
)["data"]

View file

@ -21,7 +21,7 @@ def billing(request):
if request.shop is None or request.shop.stripe is None:
request.session.flash(("This shop doesn't accept credit cards yet.", "info"))
return HTTPFound(get_referer_or_home(request))
stripe_user_shop = request.shop.stripe_user_shop(request.user)
if stripe_user_shop is None:
@ -55,7 +55,7 @@ def add_card(request):
if request.shop.stripe is None:
request.session.flash(("This shop doesn't accept credit cards yet.", "info"))
return HTTPFound(get_referer_or_home(request))
stripe_setup_intent = request.params.get("setup_intent")
stripe_setup_intent_client_secret = request.params.get("setup_intent_client_secret")
@ -104,7 +104,7 @@ def update_card(request):
if request.shop.stripe is None:
request.session.flash(("This shop doesn't accept credit cards yet.", "info"))
return HTTPFound(get_referer_or_home(request))
action = request.params.get("action", None)
card_id = request.params.get("card_id")
stripe_user_shop = request.shop.stripe_user_shop(request.user)

View file

@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""
Test script to manually check duplicate detection logic.
"""
import json
import sys
import os
import time
# Add the project to Python path
sys.path.insert(0, '/home/fox/git/make_post_sell')
from make_post_sell.lib.crypto_watcher import get_crypto_client
def test_wallet_transfers():
"""Check what transfers the wallet can see for subaddress 4."""
print("Testing wallet transfer visibility...")
try:
client = get_crypto_client("XMR")
print(f"Connected to XMR wallet: {client}")
# Check transfers for subaddress 4 specifically
query_params = {
"in": True,
"out": False,
"pending": True,
"failed": False,
"pool": True,
"filter_by_height": False, # Get ALL transfers
"subaddr_indices": [4],
"account_index": 0,
}
print(f"Querying wallet with params: {query_params}")
result = client._call("get_transfers", query_params)
print(f"Raw wallet response: {json.dumps(result, indent=2)}")
# Process all transfer types
all_transfers = []
for transfer_type in ["in", "pending", "pool"]:
if transfer_type in result:
transfers = result[transfer_type]
print(f"Found {len(transfers)} transfers in '{transfer_type}' category")
all_transfers.extend(transfers)
for i, tx in enumerate(transfers):
print(f" {transfer_type}[{i}]: height={tx.get('height', 'mempool')}, "
f"amount={tx.get('amount', 0) / 1e12:.12f} XMR, "
f"confirmations={tx.get('confirmations', 0)}, "
f"txid={tx.get('txid', 'unknown')}")
print(f"Total transfers found: {len(all_transfers)}")
# Also check with height filter to see what the scanner would see
scan_height = 3509810 # From your log
query_with_filter = {
"in": True,
"out": False,
"pending": True,
"failed": False,
"pool": True,
"filter_by_height": True,
"min_height": scan_height,
}
print(f"\nNow checking with scanner filter (min_height={scan_height}):")
print(f"Query params: {query_with_filter}")
filtered_result = client._call("get_transfers", query_with_filter)
print(f"Filtered result: {json.dumps(filtered_result, indent=2)}")
except Exception as e:
print(f"Error testing wallet: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
test_wallet_transfers()