modified: make_post_sell/lib/crypto_watcher/__init__.py

modified:   make_post_sell/models/crypto_payment.py
	modified:   make_post_sell/request_methods.py
	modified:   make_post_sell/tests/test_crypto_watcher.py
This commit is contained in:
Russell Ballestrini 2025-10-01 15:55:34 -04:00
parent da18fe2323
commit 8d382c7f31
4 changed files with 233 additions and 134 deletions

View file

@ -203,6 +203,7 @@ def _create_duplicate_payment(original_payment, tx, coin_type, dbsession=None):
- For XMR: tx.amount is in atomic units (piconero) from monero-wallet-rpc
- Each duplicate gets a unique ID and can be refunded independently
"""
# Define local get_coin_config for this function
def get_coin_config(coin_type):
configs = {
@ -1090,6 +1091,8 @@ def auto_sweep_payment_xmr(client, crypto_payment: CryptoPayment, dbsession=None
crypto_payment.swept_tx_hash = tx_hash
crypto_payment.swept_timestamp = now_timestamp()
crypto_payment.swept_network_fee = fee
# Update status to confirmed-complete after successful sweep
crypto_payment.status = CryptoPayment.STATUS_CONFIRMED_COMPLETE
if dbsession:
dbsession.add(crypto_payment)
log.sweep_operation(
@ -1232,6 +1235,8 @@ def auto_sweep_payment_doge(client, crypto_payment: CryptoPayment, dbsession=Non
crypto_payment.swept_timestamp = now_timestamp()
# Note: DOGE RPC doesn't return fee info easily, so we leave it None
crypto_payment.swept_network_fee = None
# Update status to confirmed-complete after successful sweep
crypto_payment.status = CryptoPayment.STATUS_CONFIRMED_COMPLETE
if dbsession:
dbsession.add(crypto_payment)
@ -1681,6 +1686,11 @@ def process_payment(
"""
import json
log.payment_info(
crypto_payment,
f"[DEBUG] Entering process_payment - status: {crypto_payment.status}, has_invoice: {crypto_payment.invoice is not None}, incoming_transfers: {len(incoming_transfers)}",
)
now_ms = int(time.time() * 1000)
# Initialize payment rescue with correct client for this coin type
@ -2297,13 +2307,37 @@ def process_payment(
delete_invoice_for_terminal_state(env_request.dbsession, crypto_payment)
return
# No funds seen yet
if not incoming_transfers:
# Early check for confirmed payments that just need sweeping
if (
not incoming_transfers
and crypto_payment.status == CryptoPayment.STATUS_CONFIRMED
and client
and crypto_payment.shop_sweep_to_address
and not crypto_payment.is_swept
):
log.payment_info(crypto_payment, "Confirmed payment needs sweep - processing")
try:
sweep_success = auto_sweep_payment(
client, crypto_payment, env_request.dbsession
)
if sweep_success and crypto_payment.is_swept:
log.payment_info(crypto_payment, "Swept successfully")
except Exception as e:
log.payment_error(
crypto_payment, f"Auto-sweep failed for confirmed payment: {e}"
)
crypto_payment.updated_timestamp = now_ms
env_request.dbsession.add(crypto_payment)
return
total_recv, txids, min_confs = summarize_txs(incoming_transfers)
# Calculate transfer summary - even if empty
if incoming_transfers:
total_recv, txids, min_confs = summarize_txs(incoming_transfers)
else:
# No new transfers - use existing payment data
total_recv = 0
txids = []
min_confs = 0
# For duplicate detection, we need separate confirmation calculations
@ -2480,121 +2514,111 @@ def process_payment(
f"with amount {duplicate_payment.received_amount} {crypto_payment.coin_type} - scanner will handle refund",
)
# Note: No recalculation needed - new_sum and new_txids are already set correctly
# based on the duplicate detection logic above
# Note: No recalculation needed - new_sum and new_txids are already set correctly
# based on the duplicate detection logic above
# Update received_amount with the (possibly recalculated) new_sum
crypto_payment.received_amount = (crypto_payment.received_amount or 0) + new_sum
# Update received_amount with the (possibly recalculated) new_sum
crypto_payment.received_amount = (crypto_payment.received_amount or 0) + new_sum
# Update tx_hashes with new transactions
merged = list(dict.fromkeys(list(existing) + new_txids))
crypto_payment.tx_hashes = json.dumps(merged)
# Update tx_hashes with new transactions
merged = list(dict.fromkeys(list(existing) + new_txids))
crypto_payment.tx_hashes = json.dumps(merged)
# Update confirmations from only legitimate (existing) transactions, not duplicates
legitimate_transfers = []
seen = set(existing)
for tx in incoming_transfers:
txid = tx.get("txid") or tx.get("transaction_id")
if txid and txid in seen:
legitimate_transfers.append(tx)
# Update confirmations from only legitimate (existing) transactions, not duplicates
legitimate_transfers = []
seen = set(existing)
for tx in incoming_transfers:
txid = tx.get("txid") or tx.get("transaction_id")
if txid and txid in seen:
legitimate_transfers.append(tx)
if legitimate_transfers:
_, _, legitimate_min_confs = summarize_txs(legitimate_transfers)
crypto_payment.current_confirmations = legitimate_min_confs
else:
crypto_payment.current_confirmations = min_confs
if legitimate_transfers:
_, _, legitimate_min_confs = summarize_txs(legitimate_transfers)
crypto_payment.current_confirmations = legitimate_min_confs
else:
crypto_payment.current_confirmations = min_confs
crypto_payment.updated_timestamp = now_ms
crypto_payment.updated_timestamp = now_ms
# Update status from pending to received if this is the first amount received
if crypto_payment.status == CryptoPayment.STATUS_PENDING and new_sum > 0:
# Update status from pending to received if this is the first amount received
if crypto_payment.status == CryptoPayment.STATUS_PENDING and new_sum > 0:
log.state_transition(
crypto_payment,
"PENDING",
"RECEIVED",
f"amount {crypto_payment.received_amount}",
)
crypto_payment.status = CryptoPayment.STATUS_RECEIVED
# Check if original payment should move to confirmed status
if (
crypto_payment.received_amount >= crypto_payment.expected_amount
and crypto_payment.current_confirmations
>= int(crypto_payment.confirmations_required)
and crypto_payment.status == CryptoPayment.STATUS_RECEIVED
):
# ALWAYS finalize invoice when payment moves to confirmed
if crypto_payment.invoice:
log.payment_info(
crypto_payment,
f"Payment confirmed, finalizing invoice {crypto_payment.invoice.id}",
)
finalize_invoice(env_request, crypto_payment)
# Update status to confirmed
if crypto_payment.received_amount > crypto_payment.expected_amount:
log.state_transition(
crypto_payment,
"PENDING",
"RECEIVED",
f"amount {crypto_payment.received_amount}",
"CONFIRMED_OVERPAY",
f"received {crypto_payment.received_amount}, expected {crypto_payment.expected_amount}",
)
crypto_payment.status = CryptoPayment.STATUS_RECEIVED
crypto_payment.status = CryptoPayment.STATUS_CONFIRMED_OVERPAY
else:
log.state_transition(
crypto_payment,
"RECEIVED",
"CONFIRMED",
f"{crypto_payment.current_confirmations} confirmations",
)
crypto_payment.status = CryptoPayment.STATUS_CONFIRMED
# Check if original payment should move to confirmed status
if (
crypto_payment.received_amount >= crypto_payment.expected_amount
and crypto_payment.current_confirmations
>= int(crypto_payment.confirmations_required)
and crypto_payment.status == CryptoPayment.STATUS_RECEIVED
):
# ALWAYS finalize invoice when payment moves to confirmed
if crypto_payment.invoice:
log.payment_info(
crypto_payment,
f"Payment confirmed, finalizing invoice {crypto_payment.invoice.id}",
)
finalize_invoice(env_request, crypto_payment)
# Send confirmation emails if not already sent (independent of invoice finalization)
if crypto_payment.invoice and crypto_payment.invoice.user:
# Create a request wrapper with shop's domain context for emails
email_request = create_shop_context_request(env_request, crypto_payment)
# Update status to confirmed
if crypto_payment.received_amount > crypto_payment.expected_amount:
log.state_transition(
crypto_payment,
"RECEIVED",
"CONFIRMED_OVERPAY",
f"received {crypto_payment.received_amount}, expected {crypto_payment.expected_amount}",
)
crypto_payment.status = CryptoPayment.STATUS_CONFIRMED_OVERPAY
else:
log.state_transition(
crypto_payment,
"RECEIVED",
"CONFIRMED",
f"{crypto_payment.current_confirmations} confirmations",
)
crypto_payment.status = CryptoPayment.STATUS_CONFIRMED
# Send purchase confirmation email if not already sent
if not crypto_payment.purchase_email_sent:
try:
send_purchase_email(
email_request,
crypto_payment.invoice.user.email,
[item.product for item in crypto_payment.invoice.line_items],
crypto_payment.invoice.total,
)
crypto_payment.purchase_email_sent = True
log.payment_info(crypto_payment, "Purchase confirmation email sent")
except Exception as e:
log.payment_error(
crypto_payment, f"Failed to send purchase email: {e}"
)
# Send confirmation emails if not already sent (independent of invoice finalization)
if crypto_payment.invoice and crypto_payment.invoice.user:
# Create a request wrapper with shop's domain context for emails
email_request = create_shop_context_request(env_request, crypto_payment)
# Send purchase confirmation email if not already sent
if not crypto_payment.purchase_email_sent:
try:
send_purchase_email(
email_request,
crypto_payment.invoice.user.email,
[
item.product
for item in crypto_payment.invoice.line_items
],
crypto_payment.invoice.total,
)
crypto_payment.purchase_email_sent = True
log.payment_info(
crypto_payment, "Purchase confirmation email sent"
)
except Exception as e:
log.payment_error(
crypto_payment, f"Failed to send purchase email: {e}"
)
# Send sales notification email if not already sent
if not crypto_payment.sales_email_sent:
try:
send_sale_email(
email_request,
crypto_payment.invoice.shop,
[
item.product
for item in crypto_payment.invoice.line_items
],
crypto_payment.invoice.total,
)
crypto_payment.sales_email_sent = True
log.payment_info(
crypto_payment, "Sales notification email sent"
)
except Exception as e:
log.payment_error(
crypto_payment, f"Failed to send sales email: {e}"
)
# Send sales notification email if not already sent
if not crypto_payment.sales_email_sent:
try:
send_sale_email(
email_request,
crypto_payment.invoice.shop,
[item.product for item in crypto_payment.invoice.line_items],
crypto_payment.invoice.total,
)
crypto_payment.sales_email_sent = True
log.payment_info(crypto_payment, "Sales notification email sent")
except Exception as e:
log.payment_error(
crypto_payment, f"Failed to send sales email: {e}"
)
# ALSO handle already-confirmed payments that go through duplicate detection path
# (They need emails and sweep processing too)
@ -2983,6 +3007,25 @@ def process_payment(
# Note: Auto-sweep is now handled immediately when payment is confirmed (based on RPC confirmation data)
# Check if this confirmed payment needs sweeping (handles case where payment was already confirmed)
if (
client
and crypto_payment.status == CryptoPayment.STATUS_CONFIRMED
and crypto_payment.shop_sweep_to_address
and not crypto_payment.is_swept
):
log.payment_info(crypto_payment, "Confirmed payment needs sweep - attempting")
try:
sweep_success = auto_sweep_payment(
client, crypto_payment, env_request.dbsession
)
if sweep_success and crypto_payment.is_swept:
log.payment_info(crypto_payment, "Swept successfully")
except Exception as e:
log.payment_error(
crypto_payment, f"Auto-sweep failed for confirmed payment: {e}"
)
env_request.dbsession.add(crypto_payment)
@ -3483,7 +3526,9 @@ def scan_wallet_for_double_or_late_payments(request, settings):
f"Scanning account {account_index} for transfers with min_height >= {scan_position}"
)
else:
log.processing_cycle(f"Scanning ALL transfers for account {account_index} (no height filter)")
log.processing_cycle(
f"Scanning ALL transfers for account {account_index} (no height filter)"
)
# Get transfers from the wallet using bounded query
log.processing_cycle(
@ -3579,7 +3624,8 @@ def scan_wallet_for_double_or_late_payments(request, settings):
if payment:
should_process = _should_process_late_payment(payment, tx)
log.payment_info(
payment, f"should_process_late_payment: {should_process}"
payment,
f"should_process_late_payment: {should_process}",
)
if should_process:
late_payments_found += 1
@ -3596,7 +3642,7 @@ def scan_wallet_for_double_or_late_payments(request, settings):
# Check if duplicate payment record already exists for this transaction
existing_duplicate = (
db.query(CryptoPayment)
.filter(
.filter(
CryptoPayment.coin_type == coin_type,
CryptoPayment.account_index == account_idx,
CryptoPayment.subaddress_index == subaddr_idx,
@ -3651,7 +3697,9 @@ def scan_wallet_for_double_or_late_payments(request, settings):
f"{tx.get('confirmations', 0)} confirmations, height {tx.get('height', 0)}",
)
incoming = [tx]
process_payment(request, payment, incoming, client=client)
process_payment(
request, payment, incoming, client=client
)
# Update scan position for THIS processor only
if max_height > 0 and max_height > scan_position:
@ -3977,6 +4025,10 @@ def run_once(env, interval):
# Skip if payment status changed (another process may have handled it)
db.refresh(crypto_payment)
log.payment_info(
crypto_payment,
f"[DEBUG] Checking status: {crypto_payment.status} in all_monitored_statuses: {crypto_payment.status in all_monitored_statuses}",
)
if crypto_payment.status not in all_monitored_statuses:
log.payment_info(
crypto_payment,

View file

@ -26,6 +26,9 @@ class CryptoPayment(RBase, Base):
STATUS_PENDING = "pending"
STATUS_RECEIVED = "received"
STATUS_CONFIRMED = "confirmed"
STATUS_CONFIRMED_COMPLETE = (
"confirmed-complete" # Confirmed and swept to cold storage
)
STATUS_CONFIRMED_OVERPAY = "confirmed-overpay"
STATUS_CONFIRMED_OVERPAY_REFUNDED = "confirmed-overpay-refunded"
STATUS_CONFIRMED_OVERPAY_REFUNDED_COMPLETE = "confirmed-overpay-refunded-complete"
@ -90,7 +93,7 @@ class CryptoPayment(RBase, Base):
ACTIVE_STATUSES = [
STATUS_PENDING,
STATUS_RECEIVED,
STATUS_CONFIRMED,
STATUS_CONFIRMED, # Still active until swept
STATUS_CONFIRMED_OVERPAY,
STATUS_DOUBLEPAY_REFUNDED, # Double payment that needs refund processing
]
@ -107,7 +110,7 @@ class CryptoPayment(RBase, Base):
# Terminal statuses that should not be processed (have no outgoing transitions)
TERMINAL_STATUSES = [
# Successful terminal states
STATUS_CONFIRMED, # Normal successful payment
STATUS_CONFIRMED_COMPLETE, # Normal successful payment that has been swept
STATUS_CONFIRMED_OVERPAY_REFUNDED_COMPLETE, # Overpaid, refund complete
# Failed terminal states
STATUS_EXPIRED, # Payment window expired

View file

@ -210,7 +210,9 @@ def includeme(config):
return False
try:
from make_post_sell.lib.crypto_watcher.crypto_clients import get_client_from_settings
from make_post_sell.lib.crypto_watcher.crypto_clients import (
get_client_from_settings,
)
client = get_client_from_settings(request.registry.settings)
# Try to get blockchain height as a simple health check
@ -225,7 +227,9 @@ def includeme(config):
return False
try:
from make_post_sell.lib.crypto_watcher.crypto_clients import get_client_from_settings
from make_post_sell.lib.crypto_watcher.crypto_clients import (
get_client_from_settings,
)
client = get_client_from_settings(request.registry.settings)
# Check if wallet is synced (ready for payment processing)

View file

@ -1928,7 +1928,7 @@ class PassiveMonitoringTests(unittest.TestCase):
# Verify process_payment was called for cancelled payment
mock_process.assert_called_once()
# Verify scan position was updated
self.assertEqual(mock_processor.last_scan_semaphore, "height:3512194")
self.mock_dbsession.add.assert_called_with(mock_processor)
@ -2859,7 +2859,9 @@ class RefundTypeTests(unittest.TestCase):
def test_underpayment_refund_with_fee(self):
"""Test underpayment refund applies 9% restocking fee."""
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import PaymentRescue
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import (
PaymentRescue,
)
mock_client = MagicMock()
rescue = PaymentRescue(self.mock_dbsession, mock_client)
@ -2896,7 +2898,9 @@ class RefundTypeTests(unittest.TestCase):
def test_overpayment_refund_within_threshold(self):
"""Test overpayment within 5% threshold requires no refund."""
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import PaymentRescue
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import (
PaymentRescue,
)
mock_client = MagicMock()
rescue = PaymentRescue(self.mock_dbsession, mock_client)
@ -2915,7 +2919,9 @@ class RefundTypeTests(unittest.TestCase):
def test_overpayment_refund_above_threshold(self):
"""Test overpayment above 5% threshold triggers refund with 9% fee."""
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import PaymentRescue
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import (
PaymentRescue,
)
mock_client = MagicMock()
rescue = PaymentRescue(self.mock_dbsession, mock_client)
@ -2958,7 +2964,9 @@ class RefundTypeTests(unittest.TestCase):
def test_expired_payment_refund_with_fee(self):
"""Test expired payment refund applies 9% restocking fee."""
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import PaymentRescue
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import (
PaymentRescue,
)
mock_client = MagicMock()
rescue = PaymentRescue(self.mock_dbsession, mock_client)
@ -2995,7 +3003,9 @@ class RefundTypeTests(unittest.TestCase):
def test_refund_without_address_returns_none(self):
"""Test that refund returns None when no refund address is configured."""
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import PaymentRescue
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import (
PaymentRescue,
)
mock_client = MagicMock()
rescue = PaymentRescue(self.mock_dbsession, mock_client)
@ -3014,7 +3024,9 @@ class RefundTypeTests(unittest.TestCase):
def test_zero_refund_amount_returns_none(self):
"""Test that refund returns None when calculated refund amount is zero or negative."""
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import PaymentRescue
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import (
PaymentRescue,
)
mock_client = MagicMock()
rescue = PaymentRescue(self.mock_dbsession, mock_client)
@ -3044,7 +3056,9 @@ class RefundTypeTests(unittest.TestCase):
# This should still be positive: 0.001 * 0.91 = 0.00091
# So let's test the calculate_refund_amount function directly
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import calculate_refund_amount
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import (
calculate_refund_amount,
)
# Test edge case where fee would exceed amount (shouldn't happen with 9% but test anyway)
zero_result = calculate_refund_amount(Decimal("0"), Decimal("0.09"))
@ -3055,7 +3069,9 @@ class RefundTypeTests(unittest.TestCase):
# Out of stock refunds are handled directly in crypto_watcher.py, not PaymentRescue
# But we can test the principle by checking that full refunds have no fee deduction
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import calculate_refund_amount
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import (
calculate_refund_amount,
)
received_amount = Decimal("3.5") # 3.5 XMR received
@ -3073,7 +3089,9 @@ class RefundTypeTests(unittest.TestCase):
def test_doge_refund_execution_uses_correct_rpc(self):
"""Test DOGE refund execution uses sendtoaddress RPC."""
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import PaymentRescue
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import (
PaymentRescue,
)
# Create DOGE payment
doge_payment = MagicMock()
@ -3112,7 +3130,9 @@ class RefundTypeTests(unittest.TestCase):
def test_xmr_refund_execution_uses_correct_rpc(self):
"""Test XMR refund execution uses transfer RPC with atomic units."""
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import PaymentRescue
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import (
PaymentRescue,
)
# Create XMR payment
xmr_payment = MagicMock()
@ -3165,7 +3185,9 @@ class RefundTypeTests(unittest.TestCase):
@patch("make_post_sell.lib.crypto_watcher.get_coin_config")
def test_insufficient_confirmations_delays_refund(self, mock_get_coin_config):
"""Test refund is delayed when incoming payment lacks confirmations."""
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import PaymentRescue
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import (
PaymentRescue,
)
# Mock coin config to require 10 confirmations
mock_get_coin_config.return_value = {
@ -3222,7 +3244,9 @@ class RefundTypeTests(unittest.TestCase):
def test_unsupported_coin_refund_execution(self):
"""Test unsupported coin type raises appropriate error in refund execution."""
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import PaymentRescue
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import (
PaymentRescue,
)
# Create payment with unsupported coin
unsupported_payment = MagicMock()
@ -3248,7 +3272,9 @@ class RefundTypeTests(unittest.TestCase):
def test_doge_refund_amount_precision_handling(self):
"""Test DOGE refund execution rounds amounts to 8 decimal places."""
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import PaymentRescue
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import (
PaymentRescue,
)
# Create DOGE payment
doge_payment = MagicMock()
@ -3288,7 +3314,9 @@ class RefundTypeTests(unittest.TestCase):
def test_xmr_refund_amount_precision_handling(self):
"""Test XMR refund execution handles high precision amounts correctly."""
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import PaymentRescue
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import (
PaymentRescue,
)
# Create XMR payment
xmr_payment = MagicMock()
@ -3452,7 +3480,9 @@ class PaymentConfirmationOrderTests(unittest.TestCase):
):
"""Test overpayment: 1) finalize, 2) refund excess, 3) auto-sweep invoice amount. Fee sweep happens later."""
from make_post_sell.lib.crypto_watcher import process_confirmed_payment
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import PaymentRescue
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import (
PaymentRescue,
)
# Mock coin config
mock_get_coin_config.return_value = {"atomic_units": 100000000} # DOGE
@ -3535,7 +3565,9 @@ class PaymentConfirmationOrderTests(unittest.TestCase):
):
"""Test overpayment refund failure prevents auto-sweep (preserves customer funds)."""
from make_post_sell.lib.crypto_watcher import process_confirmed_payment
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import PaymentRescue
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import (
PaymentRescue,
)
# Mock coin config
mock_get_coin_config.return_value = {"atomic_units": 100000000} # DOGE
@ -3590,7 +3622,9 @@ class PaymentConfirmationOrderTests(unittest.TestCase):
):
"""Test overpayment within 5% threshold: normal confirmation + full auto-sweep."""
from make_post_sell.lib.crypto_watcher import process_confirmed_payment
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import PaymentRescue
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import (
PaymentRescue,
)
# Mock coin config
mock_get_coin_config.return_value = {"atomic_units": 100000000} # DOGE
@ -3763,7 +3797,9 @@ class PaymentConfirmationOrderTests(unittest.TestCase):
):
"""Test XMR overpayment: 1) finalize, 2) refund excess, 3) auto-sweep invoice amount. Fee sweep happens later."""
from make_post_sell.lib.crypto_watcher import process_confirmed_payment
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import PaymentRescue
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import (
PaymentRescue,
)
# Mock coin config for XMR
mock_get_coin_config.return_value = {
@ -3857,7 +3893,9 @@ class PaymentConfirmationOrderTests(unittest.TestCase):
):
"""Test XMR overpayment refund failure prevents auto-sweep (preserves customer funds)."""
from make_post_sell.lib.crypto_watcher import process_confirmed_payment
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import PaymentRescue
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import (
PaymentRescue,
)
# Mock coin config for XMR
mock_get_coin_config.return_value = {
@ -4066,7 +4104,9 @@ class PaymentConfirmationOrderTests(unittest.TestCase):
):
"""Test function is idempotent - customer gets product even if refund fails, can retry."""
from make_post_sell.lib.crypto_watcher import process_confirmed_payment
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import PaymentRescue
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import (
PaymentRescue,
)
from make_post_sell.models.crypto_payment import CryptoPayment
# Mock coin config