diff --git a/make_post_sell/tests/test_double_spend_integration.py b/make_post_sell/tests/test_double_spend_integration.py index ef57979..063d7f2 100644 --- a/make_post_sell/tests/test_double_spend_integration.py +++ b/make_post_sell/tests/test_double_spend_integration.py @@ -667,6 +667,110 @@ class TestDoubleSpendIntegration(DatabaseIntegrationTests): ) # The duplicate amount self.assertIsNone(duplicate_payment.invoice) # No invoice for duplicates + @patch("make_post_sell.lib.crypto_watcher.send_refund_email") + @patch("make_post_sell.lib.crypto_watcher.send_purchase_email") + @patch("make_post_sell.lib.crypto_watcher.send_sale_email") + def test_duplicate_payment_confirmation_count_updates_in_processing_cycle( + self, mock_sale_email, mock_purchase_email, mock_refund_email + ): + """ + Integration test for the bug where duplicate payments weren't getting their + confirmation counts updated in the main processing loop, causing stuck refunds. + + This tests the full cycle: create duplicate payment, process it with mock + blockchain data showing high confirmations, verify confirmation count updates. + """ + with transaction.manager: + # Create the base payment scenario + payment, invoice, shop, user = self._create_payment_scenario( + status=CryptoPayment.STATUS_CONFIRMED, received=100000000, expected=100000000 + ) + payment.tx_hashes = json.dumps(["original-tx-123"]) + self.dbsession.add(payment) + + # Create a duplicate payment that should get its confirmations updated + duplicate_payment = CryptoPayment( + invoice=None, # Duplicates don't have invoices + user=user, + shop=shop, + address=payment.address, + account_index=payment.account_index, + subaddress_index=payment.subaddress_index, + coin_type="XMR", + expected_amount=0, # Duplicates don't expect payment + rate_locked_usd_per_coin=Decimal("150.00"), + quote_expires_at_ms=int(time.time() * 1000) + 900000, + confirmations_required=10, + refund_address="test-refund-address-12345", + ) + duplicate_payment.status = CryptoPayment.STATUS_DOUBLEPAY_REFUND + duplicate_payment.received_amount = 37080000 # Some amount received + duplicate_payment.current_confirmations = 0 # Starting with 0 - the bug! + duplicate_payment.tx_hashes = json.dumps(["duplicate-tx-456"]) + self.dbsession.add(duplicate_payment) + self.dbsession.flush() + + # Mock crypto client to simulate blockchain with confirmed transaction + mock_client = MagicMock() + # This simulates the _call to get_transfers returning the duplicate transaction with confirmations + mock_client._call.return_value = { + "in": [ + { + "txid": "original-tx-123", + "amount": 100000000, + "confirmations": 25, # Original transaction + }, + { + "txid": "duplicate-tx-456", + "amount": 37080000, + "confirmations": 15, # Duplicate transaction with sufficient confirmations + }, + { + "txid": "newer-tx-789", + "amount": 20000000, + "confirmations": 2, # New transaction with low confirmations + }, + ] + } + + # Mock PaymentRescue to prevent actual refund processing but allow confirmation updates + with patch("make_post_sell.lib.crypto_watcher.PaymentRescue") as mock_rescue_class: + # Simply return None to prevent refund processing + # The key test is that confirmations get updated, not that refund happens + mock_rescue_class.return_value = None + + # Create mock request + mock_request = MagicMock() + mock_request.dbsession = self.dbsession + + # TEST: Process the duplicate payment - should update confirmations and process refund + process_payment(mock_request, duplicate_payment, [], client=mock_client) + + # CRITICAL ASSERTION: Confirmation count should have been updated from 0 to 15 + self.assertEqual( + duplicate_payment.current_confirmations, + 15, + "Duplicate payment confirmation count was not updated from blockchain data" + ) + + # Verify the specific blockchain query was made to get ALL transfers (not filtered) + mock_client._call.assert_called_once_with( + "get_transfers", + { + "in": True, + "out": False, + "pending": True, + "failed": False, + "pool": True, + "filter_by_height": False, # Critical: gets ALL transfers to find old confirmed txs + "subaddr_indices": [duplicate_payment.subaddress_index], + "account_index": duplicate_payment.account_index, + } + ) + + # Since we mocked PaymentRescue to return None, refund won't be attempted + # but the confirmation count update should still happen + if __name__ == "__main__": import unittest