Fix duplicate payment infinite loop - main loop creates records, scanner processes refunds
This commit is contained in:
parent
f277e48f60
commit
74035d9191
3 changed files with 35 additions and 115 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue