From 667b8fee5014ec47927250733b23d896538136e4 Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Wed, 1 Oct 2025 12:48:49 -0400 Subject: [PATCH] Reorganize crypto watcher code into dedicated package - Create make_post_sell/lib/crypto_watcher/ directory structure - Move crypto_watcher.py to crypto_watcher/__init__.py - Move crypto_payment_rescue.py to crypto_watcher/ - Move crypto_clients.py to crypto_watcher/ - Update all imports to use the new package structure - Fix relative imports within the crypto_watcher package - Update imports in request_methods.py, views, and all tests - Fix late payment misidentification (pending vs confirmed status) - Update terminology: 'late payment' only for expired/cancelled states - All 141 tests passing --- .../__init__.py} | 22 +++----- .../{ => crypto_watcher}/crypto_clients.py | 0 .../crypto_payment_rescue.py | 6 +-- make_post_sell/request_methods.py | 8 +-- make_post_sell/tests/test_crypto_watcher.py | 54 +++++++++---------- make_post_sell/tests/test_integration.py | 6 +-- make_post_sell/views/crypto.py | 2 +- make_post_sell/views/crypto_processor.py | 2 +- 8 files changed, 47 insertions(+), 53 deletions(-) rename make_post_sell/lib/{crypto_watcher.py => crypto_watcher/__init__.py} (99%) rename make_post_sell/lib/{ => crypto_watcher}/crypto_clients.py (100%) rename make_post_sell/lib/{ => crypto_watcher}/crypto_payment_rescue.py (98%) diff --git a/make_post_sell/lib/crypto_watcher.py b/make_post_sell/lib/crypto_watcher/__init__.py similarity index 99% rename from make_post_sell/lib/crypto_watcher.py rename to make_post_sell/lib/crypto_watcher/__init__.py index 0899e68..43ad03c 100644 --- a/make_post_sell/lib/crypto_watcher.py +++ b/make_post_sell/lib/crypto_watcher/__init__.py @@ -3,6 +3,7 @@ import json import sys import time import logging +import uuid from typing import List from decimal import Decimal from urllib.parse import urlparse @@ -10,18 +11,19 @@ from urllib.parse import urlparse from pyramid.paster import bootstrap, setup_logging from .crypto_clients import get_client_from_settings, get_dogecoin_client_from_settings -from ..models.crypto_payment import CryptoPayment -from ..models.invoice import Invoice, delete_invoice_by_id -from .mail import ( +from ...models.crypto_payment import CryptoPayment +from ...models.crypto_processor import CryptoProcessor +from ...models.invoice import Invoice, delete_invoice_by_id +from ..mail import ( send_purchase_email, send_sale_email, send_refund_email, send_no_refund_shop_notification, ) -from ..models.inventory import get_inventory_by_product_and_shop_location +from ...models.inventory import get_inventory_by_product_and_shop_location from .crypto_payment_rescue import PaymentRescue -from ..models.meta import now_timestamp -from ..models.user_crypto_refund_address import UserCryptoRefundAddress +from ...models.meta import now_timestamp +from ...models.user_crypto_refund_address import UserCryptoRefundAddress logger = logging.getLogger(__name__) @@ -200,11 +202,6 @@ 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 """ - import uuid - import time - import json - from ..models.crypto_payment import CryptoPayment - # Define local get_coin_config for this function def get_coin_config(coin_type): configs = { @@ -3411,9 +3408,6 @@ def scan_wallet_for_double_or_late_payments(request, settings): log.processing_cycle("Starting wallet scan for double or late payments") db = request.dbsession - # Import here to avoid circular imports - from ..models.crypto_processor import CryptoProcessor - # Get all active crypto processors (one per shop/coin combination) processors = db.query(CryptoProcessor).filter(CryptoProcessor.enabled == True).all() diff --git a/make_post_sell/lib/crypto_clients.py b/make_post_sell/lib/crypto_watcher/crypto_clients.py similarity index 100% rename from make_post_sell/lib/crypto_clients.py rename to make_post_sell/lib/crypto_watcher/crypto_clients.py diff --git a/make_post_sell/lib/crypto_payment_rescue.py b/make_post_sell/lib/crypto_watcher/crypto_payment_rescue.py similarity index 98% rename from make_post_sell/lib/crypto_payment_rescue.py rename to make_post_sell/lib/crypto_watcher/crypto_payment_rescue.py index eb2f8c7..87c7983 100644 --- a/make_post_sell/lib/crypto_payment_rescue.py +++ b/make_post_sell/lib/crypto_watcher/crypto_payment_rescue.py @@ -9,7 +9,7 @@ Handles: import logging from decimal import Decimal -from ..models.user_crypto_refund_address import get_user_crypto_refund_address +from ...models.user_crypto_refund_address import get_user_crypto_refund_address logger = logging.getLogger(__name__) @@ -174,7 +174,7 @@ class PaymentRescue: # Get coin type and atomic units for proper logging coin_type = payment.coin_type if payment else "XMR" - from .crypto_watcher import get_coin_config + from . import get_coin_config coin_config = get_coin_config(coin_type) atomic_units = int(coin_config["atomic_units"]) @@ -202,7 +202,7 @@ class PaymentRescue: logger.info(f"Refund reason: {refund_details['reason']}") # Check if incoming payment has enough confirmations before allowing refund - from .crypto_watcher import get_coin_config + from . import get_coin_config coin_config = get_coin_config(payment.coin_type if payment else "XMR") required_confirmations = coin_config.get("confirmations_required", 10) diff --git a/make_post_sell/request_methods.py b/make_post_sell/request_methods.py index 2a9f393..bf169da 100644 --- a/make_post_sell/request_methods.py +++ b/make_post_sell/request_methods.py @@ -210,7 +210,7 @@ def includeme(config): return False try: - from make_post_sell.lib.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 +225,7 @@ def includeme(config): return False try: - from make_post_sell.lib.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) @@ -251,7 +251,7 @@ def includeme(config): return False try: - from make_post_sell.lib.crypto_clients import ( + from make_post_sell.lib.crypto_watcher.crypto_clients import ( get_dogecoin_client_from_settings, ) @@ -268,7 +268,7 @@ def includeme(config): return False try: - from make_post_sell.lib.crypto_clients import ( + from make_post_sell.lib.crypto_watcher.crypto_clients import ( get_dogecoin_client_from_settings, ) diff --git a/make_post_sell/tests/test_crypto_watcher.py b/make_post_sell/tests/test_crypto_watcher.py index 13a8515..618f649 100644 --- a/make_post_sell/tests/test_crypto_watcher.py +++ b/make_post_sell/tests/test_crypto_watcher.py @@ -38,7 +38,7 @@ from ..lib.crypto_watcher import ( sweep_restocking_fee, COIN_CONFIGS, ) -from ..lib.crypto_clients import MockMoneroClient, MockDogecoinClient +from ..lib.crypto_watcher.crypto_clients import MockMoneroClient, MockDogecoinClient class CryptoWatcherUnitTests(unittest.TestCase): @@ -2822,14 +2822,14 @@ class RefundTypeTests(unittest.TestCase): def test_underpayment_refund_with_fee(self): """Test underpayment refund applies 9% restocking fee.""" - from make_post_sell.lib.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) # Mock refund address lookup with patch( - "make_post_sell.lib.crypto_payment_rescue.get_user_crypto_refund_address" + "make_post_sell.lib.crypto_watcher.crypto_payment_rescue.get_user_crypto_refund_address" ) as mock_get_addr: mock_refund_record = MagicMock() mock_refund_record.address = "refund-address-123" @@ -2859,7 +2859,7 @@ 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_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) @@ -2878,14 +2878,14 @@ 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_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) # Mock refund address lookup with patch( - "make_post_sell.lib.crypto_payment_rescue.get_user_crypto_refund_address" + "make_post_sell.lib.crypto_watcher.crypto_payment_rescue.get_user_crypto_refund_address" ) as mock_get_addr: mock_refund_record = MagicMock() mock_refund_record.address = "refund-address-456" @@ -2921,14 +2921,14 @@ 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_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) # Mock refund address lookup with patch( - "make_post_sell.lib.crypto_payment_rescue.get_user_crypto_refund_address" + "make_post_sell.lib.crypto_watcher.crypto_payment_rescue.get_user_crypto_refund_address" ) as mock_get_addr: mock_refund_record = MagicMock() mock_refund_record.address = "refund-address-789" @@ -2958,14 +2958,14 @@ 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_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) # Mock no refund address found with patch( - "make_post_sell.lib.crypto_payment_rescue.get_user_crypto_refund_address" + "make_post_sell.lib.crypto_watcher.crypto_payment_rescue.get_user_crypto_refund_address" ) as mock_get_addr: mock_get_addr.return_value = None # No refund address @@ -2977,14 +2977,14 @@ 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_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) # Mock refund address lookup with patch( - "make_post_sell.lib.crypto_payment_rescue.get_user_crypto_refund_address" + "make_post_sell.lib.crypto_watcher.crypto_payment_rescue.get_user_crypto_refund_address" ) as mock_get_addr: mock_refund_record = MagicMock() mock_refund_record.address = "refund-address-123" @@ -3007,7 +3007,7 @@ 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_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")) @@ -3018,7 +3018,7 @@ 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_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 @@ -3036,7 +3036,7 @@ 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_payment_rescue import PaymentRescue + from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import PaymentRescue # Create DOGE payment doge_payment = MagicMock() @@ -3075,7 +3075,7 @@ 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_payment_rescue import PaymentRescue + from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import PaymentRescue # Create XMR payment xmr_payment = MagicMock() @@ -3128,7 +3128,7 @@ 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_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 = { @@ -3163,7 +3163,7 @@ class RefundTypeTests(unittest.TestCase): def test_calculate_refund_amount_precision(self): """Test refund amount calculation with high precision decimals.""" - from make_post_sell.lib.crypto_payment_rescue import ( + from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import ( calculate_refund_amount, RESTOCKING_FEE_PERCENT, ) @@ -3185,7 +3185,7 @@ 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_payment_rescue import PaymentRescue + from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import PaymentRescue # Create payment with unsupported coin unsupported_payment = MagicMock() @@ -3211,7 +3211,7 @@ 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_payment_rescue import PaymentRescue + from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import PaymentRescue # Create DOGE payment doge_payment = MagicMock() @@ -3251,7 +3251,7 @@ 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_payment_rescue import PaymentRescue + from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import PaymentRescue # Create XMR payment xmr_payment = MagicMock() @@ -3415,7 +3415,7 @@ 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_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 @@ -3498,7 +3498,7 @@ 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_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 @@ -3553,7 +3553,7 @@ 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_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 @@ -3726,7 +3726,7 @@ 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_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 = { @@ -3820,7 +3820,7 @@ 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_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 = { @@ -4029,7 +4029,7 @@ 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_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 diff --git a/make_post_sell/tests/test_integration.py b/make_post_sell/tests/test_integration.py index 84fa611..46b1f87 100644 --- a/make_post_sell/tests/test_integration.py +++ b/make_post_sell/tests/test_integration.py @@ -2224,7 +2224,7 @@ class DogecoinPaymentIntegration(DatabaseIntegrationTests): def test_dogecoin_unique_address_generation_integration(self): """Test that Dogecoin generates unique addresses for each payment.""" - from ..lib.crypto_clients import MockDogecoinClient + from ..lib.crypto_watcher.crypto_clients import MockDogecoinClient from ..models.crypto_processor import CryptoProcessor # Create test data @@ -2295,7 +2295,7 @@ class DogecoinPaymentIntegration(DatabaseIntegrationTests): def test_dogecoin_address_labeling_integration(self): """Test that Dogecoin address generation includes proper labeling.""" - from ..lib.crypto_clients import MockDogecoinClient + from ..lib.crypto_watcher.crypto_clients import MockDogecoinClient from ..models.crypto_processor import CryptoProcessor # Create test data @@ -2357,7 +2357,7 @@ class DogecoinPaymentIntegration(DatabaseIntegrationTests): def test_monero_enhanced_shop_labeling_integration(self): """Test that Monero subaddresses include shop ID in labels for better tracking.""" - from ..lib.crypto_clients import MockMoneroClient + from ..lib.crypto_watcher.crypto_clients import MockMoneroClient from ..models.crypto_processor import CryptoProcessor # Create test data for multiple shops diff --git a/make_post_sell/views/crypto.py b/make_post_sell/views/crypto.py index 37e493a..b3c7ed0 100644 --- a/make_post_sell/views/crypto.py +++ b/make_post_sell/views/crypto.py @@ -8,7 +8,7 @@ from ..models.cart import get_cart_by_id from ..models.invoice import Invoice, delete_invoice_by_id from ..models.crypto_payment import CryptoPayment -from ..lib.crypto_clients import ( +from ..lib.crypto_watcher.crypto_clients import ( get_client_from_settings, get_dogecoin_client_from_settings, ) diff --git a/make_post_sell/views/crypto_processor.py b/make_post_sell/views/crypto_processor.py index 88bcbc4..cc5ba9d 100644 --- a/make_post_sell/views/crypto_processor.py +++ b/make_post_sell/views/crypto_processor.py @@ -94,7 +94,7 @@ def crypto_processor_settings(request): if coin_type == "XMR": # Create new Monero account via RPC try: - from ..lib.crypto_clients import get_client_from_settings + from ..lib.crypto_watcher.crypto_clients import get_client_from_settings client = get_client_from_settings(request.registry.settings)