diff --git a/make_post_sell/lib/crypto_watcher/__init__.py b/make_post_sell/lib/crypto_watcher/__init__.py index a297676..a36752a 100644 --- a/make_post_sell/lib/crypto_watcher/__init__.py +++ b/make_post_sell/lib/crypto_watcher/__init__.py @@ -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, diff --git a/make_post_sell/models/crypto_payment.py b/make_post_sell/models/crypto_payment.py index 2715b7a..c58c1d5 100644 --- a/make_post_sell/models/crypto_payment.py +++ b/make_post_sell/models/crypto_payment.py @@ -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 diff --git a/make_post_sell/request_methods.py b/make_post_sell/request_methods.py index bf169da..0ed0dad 100644 --- a/make_post_sell/request_methods.py +++ b/make_post_sell/request_methods.py @@ -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) diff --git a/make_post_sell/tests/test_crypto_watcher.py b/make_post_sell/tests/test_crypto_watcher.py index 3dfbf44..56a5764 100644 --- a/make_post_sell/tests/test_crypto_watcher.py +++ b/make_post_sell/tests/test_crypto_watcher.py @@ -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