modified: make_post_sell/lib/crypto_watcher/__init__.py
modified: make_post_sell/models/crypto_payment.py modified: make_post_sell/request_methods.py modified: make_post_sell/tests/test_crypto_watcher.py
This commit is contained in:
parent
da18fe2323
commit
8d382c7f31
4 changed files with 233 additions and 134 deletions
|
|
@ -203,6 +203,7 @@ 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
|
||||
"""
|
||||
|
||||
# Define local get_coin_config for this function
|
||||
def get_coin_config(coin_type):
|
||||
configs = {
|
||||
|
|
@ -1090,6 +1091,8 @@ def auto_sweep_payment_xmr(client, crypto_payment: CryptoPayment, dbsession=None
|
|||
crypto_payment.swept_tx_hash = tx_hash
|
||||
crypto_payment.swept_timestamp = now_timestamp()
|
||||
crypto_payment.swept_network_fee = fee
|
||||
# Update status to confirmed-complete after successful sweep
|
||||
crypto_payment.status = CryptoPayment.STATUS_CONFIRMED_COMPLETE
|
||||
if dbsession:
|
||||
dbsession.add(crypto_payment)
|
||||
log.sweep_operation(
|
||||
|
|
@ -1232,6 +1235,8 @@ def auto_sweep_payment_doge(client, crypto_payment: CryptoPayment, dbsession=Non
|
|||
crypto_payment.swept_timestamp = now_timestamp()
|
||||
# Note: DOGE RPC doesn't return fee info easily, so we leave it None
|
||||
crypto_payment.swept_network_fee = None
|
||||
# Update status to confirmed-complete after successful sweep
|
||||
crypto_payment.status = CryptoPayment.STATUS_CONFIRMED_COMPLETE
|
||||
if dbsession:
|
||||
dbsession.add(crypto_payment)
|
||||
|
||||
|
|
@ -1681,6 +1686,11 @@ def process_payment(
|
|||
"""
|
||||
import json
|
||||
|
||||
log.payment_info(
|
||||
crypto_payment,
|
||||
f"[DEBUG] Entering process_payment - status: {crypto_payment.status}, has_invoice: {crypto_payment.invoice is not None}, incoming_transfers: {len(incoming_transfers)}",
|
||||
)
|
||||
|
||||
now_ms = int(time.time() * 1000)
|
||||
|
||||
# Initialize payment rescue with correct client for this coin type
|
||||
|
|
@ -2297,13 +2307,37 @@ def process_payment(
|
|||
delete_invoice_for_terminal_state(env_request.dbsession, crypto_payment)
|
||||
return
|
||||
|
||||
# No funds seen yet
|
||||
if not incoming_transfers:
|
||||
# Early check for confirmed payments that just need sweeping
|
||||
if (
|
||||
not incoming_transfers
|
||||
and crypto_payment.status == CryptoPayment.STATUS_CONFIRMED
|
||||
and client
|
||||
and crypto_payment.shop_sweep_to_address
|
||||
and not crypto_payment.is_swept
|
||||
):
|
||||
log.payment_info(crypto_payment, "Confirmed payment needs sweep - processing")
|
||||
try:
|
||||
sweep_success = auto_sweep_payment(
|
||||
client, crypto_payment, env_request.dbsession
|
||||
)
|
||||
if sweep_success and crypto_payment.is_swept:
|
||||
log.payment_info(crypto_payment, "Swept successfully")
|
||||
except Exception as e:
|
||||
log.payment_error(
|
||||
crypto_payment, f"Auto-sweep failed for confirmed payment: {e}"
|
||||
)
|
||||
crypto_payment.updated_timestamp = now_ms
|
||||
env_request.dbsession.add(crypto_payment)
|
||||
return
|
||||
|
||||
total_recv, txids, min_confs = summarize_txs(incoming_transfers)
|
||||
# Calculate transfer summary - even if empty
|
||||
if incoming_transfers:
|
||||
total_recv, txids, min_confs = summarize_txs(incoming_transfers)
|
||||
else:
|
||||
# No new transfers - use existing payment data
|
||||
total_recv = 0
|
||||
txids = []
|
||||
min_confs = 0
|
||||
|
||||
# For duplicate detection, we need separate confirmation calculations
|
||||
|
||||
|
|
@ -2480,121 +2514,111 @@ def process_payment(
|
|||
f"with amount {duplicate_payment.received_amount} {crypto_payment.coin_type} - scanner will handle refund",
|
||||
)
|
||||
|
||||
# Note: No recalculation needed - new_sum and new_txids are already set correctly
|
||||
# based on the duplicate detection logic above
|
||||
# Note: No recalculation needed - new_sum and new_txids are already set correctly
|
||||
# based on the duplicate detection logic above
|
||||
|
||||
# Update received_amount with the (possibly recalculated) new_sum
|
||||
crypto_payment.received_amount = (crypto_payment.received_amount or 0) + new_sum
|
||||
# Update received_amount with the (possibly recalculated) new_sum
|
||||
crypto_payment.received_amount = (crypto_payment.received_amount or 0) + new_sum
|
||||
|
||||
# Update tx_hashes with new transactions
|
||||
merged = list(dict.fromkeys(list(existing) + new_txids))
|
||||
crypto_payment.tx_hashes = json.dumps(merged)
|
||||
# Update tx_hashes with new transactions
|
||||
merged = list(dict.fromkeys(list(existing) + new_txids))
|
||||
crypto_payment.tx_hashes = json.dumps(merged)
|
||||
|
||||
# Update confirmations from only legitimate (existing) transactions, not duplicates
|
||||
legitimate_transfers = []
|
||||
seen = set(existing)
|
||||
for tx in incoming_transfers:
|
||||
txid = tx.get("txid") or tx.get("transaction_id")
|
||||
if txid and txid in seen:
|
||||
legitimate_transfers.append(tx)
|
||||
# Update confirmations from only legitimate (existing) transactions, not duplicates
|
||||
legitimate_transfers = []
|
||||
seen = set(existing)
|
||||
for tx in incoming_transfers:
|
||||
txid = tx.get("txid") or tx.get("transaction_id")
|
||||
if txid and txid in seen:
|
||||
legitimate_transfers.append(tx)
|
||||
|
||||
if legitimate_transfers:
|
||||
_, _, legitimate_min_confs = summarize_txs(legitimate_transfers)
|
||||
crypto_payment.current_confirmations = legitimate_min_confs
|
||||
else:
|
||||
crypto_payment.current_confirmations = min_confs
|
||||
if legitimate_transfers:
|
||||
_, _, legitimate_min_confs = summarize_txs(legitimate_transfers)
|
||||
crypto_payment.current_confirmations = legitimate_min_confs
|
||||
else:
|
||||
crypto_payment.current_confirmations = min_confs
|
||||
|
||||
crypto_payment.updated_timestamp = now_ms
|
||||
crypto_payment.updated_timestamp = now_ms
|
||||
|
||||
# Update status from pending to received if this is the first amount received
|
||||
if crypto_payment.status == CryptoPayment.STATUS_PENDING and new_sum > 0:
|
||||
# Update status from pending to received if this is the first amount received
|
||||
if crypto_payment.status == CryptoPayment.STATUS_PENDING and new_sum > 0:
|
||||
log.state_transition(
|
||||
crypto_payment,
|
||||
"PENDING",
|
||||
"RECEIVED",
|
||||
f"amount {crypto_payment.received_amount}",
|
||||
)
|
||||
crypto_payment.status = CryptoPayment.STATUS_RECEIVED
|
||||
|
||||
# Check if original payment should move to confirmed status
|
||||
if (
|
||||
crypto_payment.received_amount >= crypto_payment.expected_amount
|
||||
and crypto_payment.current_confirmations
|
||||
>= int(crypto_payment.confirmations_required)
|
||||
and crypto_payment.status == CryptoPayment.STATUS_RECEIVED
|
||||
):
|
||||
# ALWAYS finalize invoice when payment moves to confirmed
|
||||
if crypto_payment.invoice:
|
||||
log.payment_info(
|
||||
crypto_payment,
|
||||
f"Payment confirmed, finalizing invoice {crypto_payment.invoice.id}",
|
||||
)
|
||||
finalize_invoice(env_request, crypto_payment)
|
||||
|
||||
# Update status to confirmed
|
||||
if crypto_payment.received_amount > crypto_payment.expected_amount:
|
||||
log.state_transition(
|
||||
crypto_payment,
|
||||
"PENDING",
|
||||
"RECEIVED",
|
||||
f"amount {crypto_payment.received_amount}",
|
||||
"CONFIRMED_OVERPAY",
|
||||
f"received {crypto_payment.received_amount}, expected {crypto_payment.expected_amount}",
|
||||
)
|
||||
crypto_payment.status = CryptoPayment.STATUS_RECEIVED
|
||||
crypto_payment.status = CryptoPayment.STATUS_CONFIRMED_OVERPAY
|
||||
else:
|
||||
log.state_transition(
|
||||
crypto_payment,
|
||||
"RECEIVED",
|
||||
"CONFIRMED",
|
||||
f"{crypto_payment.current_confirmations} confirmations",
|
||||
)
|
||||
crypto_payment.status = CryptoPayment.STATUS_CONFIRMED
|
||||
|
||||
# Check if original payment should move to confirmed status
|
||||
if (
|
||||
crypto_payment.received_amount >= crypto_payment.expected_amount
|
||||
and crypto_payment.current_confirmations
|
||||
>= int(crypto_payment.confirmations_required)
|
||||
and crypto_payment.status == CryptoPayment.STATUS_RECEIVED
|
||||
):
|
||||
# ALWAYS finalize invoice when payment moves to confirmed
|
||||
if crypto_payment.invoice:
|
||||
log.payment_info(
|
||||
crypto_payment,
|
||||
f"Payment confirmed, finalizing invoice {crypto_payment.invoice.id}",
|
||||
)
|
||||
finalize_invoice(env_request, crypto_payment)
|
||||
# Send confirmation emails if not already sent (independent of invoice finalization)
|
||||
if crypto_payment.invoice and crypto_payment.invoice.user:
|
||||
# Create a request wrapper with shop's domain context for emails
|
||||
email_request = create_shop_context_request(env_request, crypto_payment)
|
||||
|
||||
# Update status to confirmed
|
||||
if crypto_payment.received_amount > crypto_payment.expected_amount:
|
||||
log.state_transition(
|
||||
crypto_payment,
|
||||
"RECEIVED",
|
||||
"CONFIRMED_OVERPAY",
|
||||
f"received {crypto_payment.received_amount}, expected {crypto_payment.expected_amount}",
|
||||
)
|
||||
crypto_payment.status = CryptoPayment.STATUS_CONFIRMED_OVERPAY
|
||||
else:
|
||||
log.state_transition(
|
||||
crypto_payment,
|
||||
"RECEIVED",
|
||||
"CONFIRMED",
|
||||
f"{crypto_payment.current_confirmations} confirmations",
|
||||
)
|
||||
crypto_payment.status = CryptoPayment.STATUS_CONFIRMED
|
||||
# Send purchase confirmation email if not already sent
|
||||
if not crypto_payment.purchase_email_sent:
|
||||
try:
|
||||
send_purchase_email(
|
||||
email_request,
|
||||
crypto_payment.invoice.user.email,
|
||||
[item.product for item in crypto_payment.invoice.line_items],
|
||||
crypto_payment.invoice.total,
|
||||
)
|
||||
crypto_payment.purchase_email_sent = True
|
||||
log.payment_info(crypto_payment, "Purchase confirmation email sent")
|
||||
except Exception as e:
|
||||
log.payment_error(
|
||||
crypto_payment, f"Failed to send purchase email: {e}"
|
||||
)
|
||||
|
||||
# Send confirmation emails if not already sent (independent of invoice finalization)
|
||||
if crypto_payment.invoice and crypto_payment.invoice.user:
|
||||
# Create a request wrapper with shop's domain context for emails
|
||||
email_request = create_shop_context_request(env_request, crypto_payment)
|
||||
|
||||
# Send purchase confirmation email if not already sent
|
||||
if not crypto_payment.purchase_email_sent:
|
||||
try:
|
||||
send_purchase_email(
|
||||
email_request,
|
||||
crypto_payment.invoice.user.email,
|
||||
[
|
||||
item.product
|
||||
for item in crypto_payment.invoice.line_items
|
||||
],
|
||||
crypto_payment.invoice.total,
|
||||
)
|
||||
crypto_payment.purchase_email_sent = True
|
||||
log.payment_info(
|
||||
crypto_payment, "Purchase confirmation email sent"
|
||||
)
|
||||
except Exception as e:
|
||||
log.payment_error(
|
||||
crypto_payment, f"Failed to send purchase email: {e}"
|
||||
)
|
||||
|
||||
# Send sales notification email if not already sent
|
||||
if not crypto_payment.sales_email_sent:
|
||||
try:
|
||||
send_sale_email(
|
||||
email_request,
|
||||
crypto_payment.invoice.shop,
|
||||
[
|
||||
item.product
|
||||
for item in crypto_payment.invoice.line_items
|
||||
],
|
||||
crypto_payment.invoice.total,
|
||||
)
|
||||
crypto_payment.sales_email_sent = True
|
||||
log.payment_info(
|
||||
crypto_payment, "Sales notification email sent"
|
||||
)
|
||||
except Exception as e:
|
||||
log.payment_error(
|
||||
crypto_payment, f"Failed to send sales email: {e}"
|
||||
)
|
||||
# Send sales notification email if not already sent
|
||||
if not crypto_payment.sales_email_sent:
|
||||
try:
|
||||
send_sale_email(
|
||||
email_request,
|
||||
crypto_payment.invoice.shop,
|
||||
[item.product for item in crypto_payment.invoice.line_items],
|
||||
crypto_payment.invoice.total,
|
||||
)
|
||||
crypto_payment.sales_email_sent = True
|
||||
log.payment_info(crypto_payment, "Sales notification email sent")
|
||||
except Exception as e:
|
||||
log.payment_error(
|
||||
crypto_payment, f"Failed to send sales email: {e}"
|
||||
)
|
||||
|
||||
# ALSO handle already-confirmed payments that go through duplicate detection path
|
||||
# (They need emails and sweep processing too)
|
||||
|
|
@ -2983,6 +3007,25 @@ def process_payment(
|
|||
|
||||
# Note: Auto-sweep is now handled immediately when payment is confirmed (based on RPC confirmation data)
|
||||
|
||||
# Check if this confirmed payment needs sweeping (handles case where payment was already confirmed)
|
||||
if (
|
||||
client
|
||||
and crypto_payment.status == CryptoPayment.STATUS_CONFIRMED
|
||||
and crypto_payment.shop_sweep_to_address
|
||||
and not crypto_payment.is_swept
|
||||
):
|
||||
log.payment_info(crypto_payment, "Confirmed payment needs sweep - attempting")
|
||||
try:
|
||||
sweep_success = auto_sweep_payment(
|
||||
client, crypto_payment, env_request.dbsession
|
||||
)
|
||||
if sweep_success and crypto_payment.is_swept:
|
||||
log.payment_info(crypto_payment, "Swept successfully")
|
||||
except Exception as e:
|
||||
log.payment_error(
|
||||
crypto_payment, f"Auto-sweep failed for confirmed payment: {e}"
|
||||
)
|
||||
|
||||
env_request.dbsession.add(crypto_payment)
|
||||
|
||||
|
||||
|
|
@ -3483,7 +3526,9 @@ def scan_wallet_for_double_or_late_payments(request, settings):
|
|||
f"Scanning account {account_index} for transfers with min_height >= {scan_position}"
|
||||
)
|
||||
else:
|
||||
log.processing_cycle(f"Scanning ALL transfers for account {account_index} (no height filter)")
|
||||
log.processing_cycle(
|
||||
f"Scanning ALL transfers for account {account_index} (no height filter)"
|
||||
)
|
||||
|
||||
# Get transfers from the wallet using bounded query
|
||||
log.processing_cycle(
|
||||
|
|
@ -3579,7 +3624,8 @@ def scan_wallet_for_double_or_late_payments(request, settings):
|
|||
if payment:
|
||||
should_process = _should_process_late_payment(payment, tx)
|
||||
log.payment_info(
|
||||
payment, f"should_process_late_payment: {should_process}"
|
||||
payment,
|
||||
f"should_process_late_payment: {should_process}",
|
||||
)
|
||||
if should_process:
|
||||
late_payments_found += 1
|
||||
|
|
@ -3596,7 +3642,7 @@ def scan_wallet_for_double_or_late_payments(request, settings):
|
|||
# Check if duplicate payment record already exists for this transaction
|
||||
existing_duplicate = (
|
||||
db.query(CryptoPayment)
|
||||
.filter(
|
||||
.filter(
|
||||
CryptoPayment.coin_type == coin_type,
|
||||
CryptoPayment.account_index == account_idx,
|
||||
CryptoPayment.subaddress_index == subaddr_idx,
|
||||
|
|
@ -3651,7 +3697,9 @@ def scan_wallet_for_double_or_late_payments(request, settings):
|
|||
f"{tx.get('confirmations', 0)} confirmations, height {tx.get('height', 0)}",
|
||||
)
|
||||
incoming = [tx]
|
||||
process_payment(request, payment, incoming, client=client)
|
||||
process_payment(
|
||||
request, payment, incoming, client=client
|
||||
)
|
||||
|
||||
# Update scan position for THIS processor only
|
||||
if max_height > 0 and max_height > scan_position:
|
||||
|
|
@ -3977,6 +4025,10 @@ def run_once(env, interval):
|
|||
|
||||
# Skip if payment status changed (another process may have handled it)
|
||||
db.refresh(crypto_payment)
|
||||
log.payment_info(
|
||||
crypto_payment,
|
||||
f"[DEBUG] Checking status: {crypto_payment.status} in all_monitored_statuses: {crypto_payment.status in all_monitored_statuses}",
|
||||
)
|
||||
if crypto_payment.status not in all_monitored_statuses:
|
||||
log.payment_info(
|
||||
crypto_payment,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ class CryptoPayment(RBase, Base):
|
|||
STATUS_PENDING = "pending"
|
||||
STATUS_RECEIVED = "received"
|
||||
STATUS_CONFIRMED = "confirmed"
|
||||
STATUS_CONFIRMED_COMPLETE = (
|
||||
"confirmed-complete" # Confirmed and swept to cold storage
|
||||
)
|
||||
STATUS_CONFIRMED_OVERPAY = "confirmed-overpay"
|
||||
STATUS_CONFIRMED_OVERPAY_REFUNDED = "confirmed-overpay-refunded"
|
||||
STATUS_CONFIRMED_OVERPAY_REFUNDED_COMPLETE = "confirmed-overpay-refunded-complete"
|
||||
|
|
@ -90,7 +93,7 @@ class CryptoPayment(RBase, Base):
|
|||
ACTIVE_STATUSES = [
|
||||
STATUS_PENDING,
|
||||
STATUS_RECEIVED,
|
||||
STATUS_CONFIRMED,
|
||||
STATUS_CONFIRMED, # Still active until swept
|
||||
STATUS_CONFIRMED_OVERPAY,
|
||||
STATUS_DOUBLEPAY_REFUNDED, # Double payment that needs refund processing
|
||||
]
|
||||
|
|
@ -107,7 +110,7 @@ class CryptoPayment(RBase, Base):
|
|||
# Terminal statuses that should not be processed (have no outgoing transitions)
|
||||
TERMINAL_STATUSES = [
|
||||
# Successful terminal states
|
||||
STATUS_CONFIRMED, # Normal successful payment
|
||||
STATUS_CONFIRMED_COMPLETE, # Normal successful payment that has been swept
|
||||
STATUS_CONFIRMED_OVERPAY_REFUNDED_COMPLETE, # Overpaid, refund complete
|
||||
# Failed terminal states
|
||||
STATUS_EXPIRED, # Payment window expired
|
||||
|
|
|
|||
|
|
@ -210,7 +210,9 @@ def includeme(config):
|
|||
return False
|
||||
|
||||
try:
|
||||
from make_post_sell.lib.crypto_watcher.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 +227,9 @@ def includeme(config):
|
|||
return False
|
||||
|
||||
try:
|
||||
from make_post_sell.lib.crypto_watcher.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)
|
||||
|
|
|
|||
|
|
@ -1928,7 +1928,7 @@ class PassiveMonitoringTests(unittest.TestCase):
|
|||
|
||||
# Verify process_payment was called for cancelled payment
|
||||
mock_process.assert_called_once()
|
||||
|
||||
|
||||
# Verify scan position was updated
|
||||
self.assertEqual(mock_processor.last_scan_semaphore, "height:3512194")
|
||||
self.mock_dbsession.add.assert_called_with(mock_processor)
|
||||
|
|
@ -2859,7 +2859,9 @@ class RefundTypeTests(unittest.TestCase):
|
|||
|
||||
def test_underpayment_refund_with_fee(self):
|
||||
"""Test underpayment refund applies 9% restocking fee."""
|
||||
from make_post_sell.lib.crypto_watcher.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)
|
||||
|
|
@ -2896,7 +2898,9 @@ 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_watcher.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)
|
||||
|
|
@ -2915,7 +2919,9 @@ 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_watcher.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)
|
||||
|
|
@ -2958,7 +2964,9 @@ 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_watcher.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)
|
||||
|
|
@ -2995,7 +3003,9 @@ 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_watcher.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)
|
||||
|
|
@ -3014,7 +3024,9 @@ 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_watcher.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)
|
||||
|
|
@ -3044,7 +3056,9 @@ 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_watcher.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"))
|
||||
|
|
@ -3055,7 +3069,9 @@ 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_watcher.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
|
||||
|
||||
|
|
@ -3073,7 +3089,9 @@ 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_watcher.crypto_payment_rescue import PaymentRescue
|
||||
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import (
|
||||
PaymentRescue,
|
||||
)
|
||||
|
||||
# Create DOGE payment
|
||||
doge_payment = MagicMock()
|
||||
|
|
@ -3112,7 +3130,9 @@ 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_watcher.crypto_payment_rescue import PaymentRescue
|
||||
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import (
|
||||
PaymentRescue,
|
||||
)
|
||||
|
||||
# Create XMR payment
|
||||
xmr_payment = MagicMock()
|
||||
|
|
@ -3165,7 +3185,9 @@ 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_watcher.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 = {
|
||||
|
|
@ -3222,7 +3244,9 @@ 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_watcher.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()
|
||||
|
|
@ -3248,7 +3272,9 @@ 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_watcher.crypto_payment_rescue import PaymentRescue
|
||||
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import (
|
||||
PaymentRescue,
|
||||
)
|
||||
|
||||
# Create DOGE payment
|
||||
doge_payment = MagicMock()
|
||||
|
|
@ -3288,7 +3314,9 @@ 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_watcher.crypto_payment_rescue import PaymentRescue
|
||||
from make_post_sell.lib.crypto_watcher.crypto_payment_rescue import (
|
||||
PaymentRescue,
|
||||
)
|
||||
|
||||
# Create XMR payment
|
||||
xmr_payment = MagicMock()
|
||||
|
|
@ -3452,7 +3480,9 @@ 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_watcher.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
|
||||
|
|
@ -3535,7 +3565,9 @@ 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_watcher.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
|
||||
|
|
@ -3590,7 +3622,9 @@ 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_watcher.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
|
||||
|
|
@ -3763,7 +3797,9 @@ 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_watcher.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 = {
|
||||
|
|
@ -3857,7 +3893,9 @@ 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_watcher.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 = {
|
||||
|
|
@ -4066,7 +4104,9 @@ 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_watcher.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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue