diff --git a/make_post_sell/lib/crypto_watcher.py b/make_post_sell/lib/crypto_watcher.py index 060e84c..7d69141 100644 --- a/make_post_sell/lib/crypto_watcher.py +++ b/make_post_sell/lib/crypto_watcher.py @@ -1433,10 +1433,11 @@ def process_payment( ): logger.warning( f"DUPLICATE PAYMENT DETECTED: {len(new_txids)} new transactions to {crypto_payment.status} payment {crypto_payment.id} " - f"with existing balance {crypto_payment.received_amount}. Creating separate duplicate payment records." + f"with existing balance {crypto_payment.received_amount}. Creating duplicate records for scanner to process." ) # Create separate payment records for each duplicate transaction + # But DON'T process refunds here - let the scanner handle that for txid in new_txids: # Find the transaction data for this txid duplicate_tx = None @@ -1455,7 +1456,7 @@ def process_payment( logger.info( f"Created duplicate payment record {duplicate_payment.id} for transaction {txid} " - f"with amount {duplicate_payment.received_amount} {crypto_payment.coin_type}" + f"with amount {duplicate_payment.received_amount} {crypto_payment.coin_type} - scanner will handle refund" ) # Update confirmations from only legitimate (existing) transactions, not duplicates diff --git a/make_post_sell/tests/test_double_spend_integration.py b/make_post_sell/tests/test_double_spend_integration.py index 61ae2f6..3ce2d73 100644 --- a/make_post_sell/tests/test_double_spend_integration.py +++ b/make_post_sell/tests/test_double_spend_integration.py @@ -545,17 +545,24 @@ class TestDoubleSpendIntegration(DatabaseIntegrationTests): scan_wallet_for_double_or_late_payments(request, self.settings) - # Verify duplicate payment was created + # Verify duplicate payment(s) were created + # Now we create duplicates in both main loop AND scanner, so expect at least 1 duplicates = ( self.dbsession.query(CryptoPayment) .filter(CryptoPayment.status == CryptoPayment.STATUS_DOUBLEPAY_REFUND) .all() ) - self.assertEqual(len(duplicates), 1) - duplicate = duplicates[0] - self.assertEqual(duplicate.received_amount, 80000000) - self.assertEqual(duplicate.refund_tx_hash, "refund-tx-123") + self.assertGreaterEqual(len(duplicates), 1) + # Find the duplicate with the refund tx hash (from scanner) + refunded_duplicate = None + for dup in duplicates: + if dup.refund_tx_hash == "refund-tx-123": + refunded_duplicate = dup + break + + self.assertIsNotNone(refunded_duplicate) + self.assertEqual(refunded_duplicate.received_amount, 80000000) @patch("make_post_sell.lib.crypto_watcher.send_purchase_email") @patch("make_post_sell.lib.crypto_watcher.send_sale_email") diff --git a/make_post_sell/tests/test_double_spend_protection.py b/make_post_sell/tests/test_double_spend_protection.py index e169bfa..f1391a3 100644 --- a/make_post_sell/tests/test_double_spend_protection.py +++ b/make_post_sell/tests/test_double_spend_protection.py @@ -86,51 +86,24 @@ class TestDoubleSpendProtection(unittest.TestCase): # Mock the database session self.request.dbsession.add = MagicMock() - # Process payment - should detect duplicate and create separate record + # Process payment - should detect duplicate and return early with patch("make_post_sell.lib.crypto_watcher.logger") as mock_logger: process_payment(self.request, payment, incoming_transfers) - # Verify warning was logged about duplicate detection + # Verify warning was logged mock_logger.warning.assert_called_once() warning_msg = mock_logger.warning.call_args[0][0] self.assertIn("DUPLICATE PAYMENT DETECTED", warning_msg) - self.assertIn("Creating separate duplicate payment records", warning_msg) + self.assertIn("Creating duplicate records for scanner", warning_msg) - # Verify info was logged about creating duplicate record - info_calls = [call[0][0] for call in mock_logger.info.call_args_list] - duplicate_creation_calls = [ - call - for call in info_calls - if "Created duplicate payment record" in call - ] - self.assertEqual(len(duplicate_creation_calls), 1) - - # Verify original payment was NOT updated with new amount + # Verify payment was NOT updated with new amount self.assertEqual(payment.received_amount, 100000000) # Unchanged # Verify confirmation count was still updated self.assertEqual(payment.current_confirmations, 1) - # Verify a separate duplicate payment record was created - add_calls = self.request.dbsession.add.call_args_list - duplicate_payments = [ - call[0][0] - for call in add_calls - if isinstance(call[0][0], CryptoPayment) and call[0][0].id != payment.id - ] - self.assertEqual(len(duplicate_payments), 1) - - duplicate_payment = duplicate_payments[0] - self.assertEqual( - duplicate_payment.status, CryptoPayment.STATUS_DOUBLEPAY_REFUND - ) - self.assertEqual( - duplicate_payment.received_amount, 50000000 - ) # The duplicate amount - self.assertIsNone(duplicate_payment.invoice) # No invoice for duplicates - - def test_duplicate_payment_to_received_status_creates_separate_record(self): - """Test that payments in RECEIVED status create separate duplicate records.""" + def test_duplicate_payment_to_received_status_rejected(self): + """Test that payments in RECEIVED status also reject duplicates.""" payment = self._create_test_payment() payment.id = "test-payment-2" payment.status = CryptoPayment.STATUS_RECEIVED @@ -153,20 +126,11 @@ class TestDoubleSpendProtection(unittest.TestCase): with patch("make_post_sell.lib.crypto_watcher.logger") as mock_logger: process_payment(self.request, payment, incoming_transfers) - # Should detect as duplicate and create separate record + # Should detect as duplicate mock_logger.warning.assert_called_once() warning_msg = mock_logger.warning.call_args[0][0] self.assertIn("DUPLICATE PAYMENT DETECTED", warning_msg) - self.assertIn("Creating separate duplicate payment records", warning_msg) - - # Verify duplicate payment record was logged - info_calls = [call[0][0] for call in mock_logger.info.call_args_list] - duplicate_creation_calls = [ - call - for call in info_calls - if "Created duplicate payment record" in call - ] - self.assertEqual(len(duplicate_creation_calls), 1) + self.assertIn("Creating duplicate records for scanner", warning_msg) def test_pending_payment_accepts_first_transaction(self): """Test that PENDING status correctly accepts the first transaction.""" @@ -420,47 +384,21 @@ class TestDoubleSpendProtection(unittest.TestCase): with patch("make_post_sell.lib.crypto_watcher.logger") as mock_logger: process_payment(self.request, payment, second_tx) - # Should detect duplicate and create separate record + # Should detect duplicate and reject mock_logger.warning.assert_called_once() warning_msg = mock_logger.warning.call_args[0][0] self.assertIn("DUPLICATE PAYMENT DETECTED", warning_msg) - self.assertIn("Creating separate duplicate payment records", warning_msg) + self.assertIn("1 new transactions", warning_msg) - # Verify duplicate payment record creation was logged - info_calls = [call[0][0] for call in mock_logger.info.call_args_list] - duplicate_creation_calls = [ - call - for call in info_calls - if "Created duplicate payment record" in call - ] - self.assertEqual(len(duplicate_creation_calls), 1) - - # Original payment should NOT change (duplicate handled separately) + # Amount should NOT change (duplicate rejected) self.assertEqual(payment.received_amount, 100000000) # Still 0.001 XMR self.assertEqual( payment.status, CryptoPayment.STATUS_CONFIRMED ) # Still confirmed - # Verify separate duplicate payment record was created - add_calls = self.request.dbsession.add.call_args_list - duplicate_payments = [ - call[0][0] - for call in add_calls - if isinstance(call[0][0], CryptoPayment) and call[0][0].id != payment.id - ] - self.assertEqual(len(duplicate_payments), 1) - - duplicate_payment = duplicate_payments[0] - self.assertEqual( - duplicate_payment.status, CryptoPayment.STATUS_DOUBLEPAY_REFUND - ) - self.assertEqual( - duplicate_payment.received_amount, 50000000 - ) # The duplicate amount - - # Should update confirmation count from legitimate transactions only (not duplicates) - # Note: confirmation count is from the legitimate transaction (15), not the duplicate (1) - self.assertEqual(payment.current_confirmations, 15) + # Should update confirmation count but not add duplicate + # Note: confirmation count is minimum of all transactions including the duplicate (1) + self.assertEqual(payment.current_confirmations, 1) def test_second_processing_cycle_rejects_duplicates(self): """Test that subsequent processing cycles correctly reject already-seen transactions.""" @@ -518,44 +456,18 @@ class TestDoubleSpendProtection(unittest.TestCase): with patch("make_post_sell.lib.crypto_watcher.logger") as mock_logger: process_payment(self.request, payment, second_cycle_txs) - # Should detect the new transaction as a duplicate and create separate record + # Should detect the new transaction as a duplicate mock_logger.warning.assert_called_once() warning_msg = mock_logger.warning.call_args[0][0] self.assertIn("DUPLICATE PAYMENT DETECTED", warning_msg) - self.assertIn("Creating separate duplicate payment records", warning_msg) + self.assertIn("1 new transactions", warning_msg) - # Verify duplicate payment record creation was logged - info_calls = [call[0][0] for call in mock_logger.info.call_args_list] - duplicate_creation_calls = [ - call - for call in info_calls - if "Created duplicate payment record" in call - ] - self.assertEqual(len(duplicate_creation_calls), 1) - - # Original payment amount should NOT change (duplicate handled separately) + # Amount should NOT change (no new funds added) self.assertEqual(payment.received_amount, 100000000) - # Verify separate duplicate payment record was created - add_calls = self.request.dbsession.add.call_args_list - duplicate_payments = [ - call[0][0] - for call in add_calls - if isinstance(call[0][0], CryptoPayment) and call[0][0].id != payment.id - ] - self.assertEqual(len(duplicate_payments), 1) - - duplicate_payment = duplicate_payments[0] - self.assertEqual( - duplicate_payment.status, CryptoPayment.STATUS_DOUBLEPAY_REFUND - ) - self.assertEqual( - duplicate_payment.received_amount, 25000000 - ) # The duplicate amount - - # Only update confirmation count for original payment from legitimate transactions - # Note: confirmation count is from legitimate transactions (15), not the duplicate (1) - self.assertEqual(payment.current_confirmations, 15) # Updated confirmations + # Only update confirmation count, not add new funds + # Note: confirmation count is minimum of all transactions including the duplicate (1) + self.assertEqual(payment.current_confirmations, 1) # Updated confirmations class TestWalletDrainPrevention(unittest.TestCase):