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
This commit is contained in:
parent
0422fcdc98
commit
667b8fee50
8 changed files with 47 additions and 53 deletions
|
|
@ -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()
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue