diff --git a/Makefile b/Makefile index ba0a29a..8eb4329 100644 --- a/Makefile +++ b/Makefile @@ -58,6 +58,8 @@ help: @echo " make dogecoin-node - Start Dogecoin daemon (pruned mode)" @echo " make dogecoin-node-full - Start Dogecoin daemon (full blockchain)" @echo " make dogecoin-status - Check sync status and wallet info" + @echo " make dogecoin-check-fee - Check current transaction fee setting" + @echo " make dogecoin-fix-fee - Fix high transaction fee (sets to 0.001 DOGE)" @echo "" @echo "WALLET MANAGEMENT:" @echo " make sweep-check - Check hot wallet balances (dev tool)" @@ -180,9 +182,10 @@ http: venv @echo "Starting simple HTTP server on port 8000..." $(PYTHON) -m http.server 8000 -# Run the crypto watcher service for monitoring Monero payments. +# Run the crypto watcher service for monitoring crypto payments. crypto-watcher: venv config @echo "Starting crypto payment watcher..." + @echo "💡 TIP: If Dogecoin refunds fail, check fee: make dogecoin-check-fee" $(VENV_DIR)/bin/crypto_watcher $(DATA_DIR)/$(CONFIG_FILE) # Run the crypto watcher once (for testing or manual processing). @@ -564,6 +567,12 @@ dogecoin-config: check-dogecoin @echo "" >> $(HOME)/.dogecoin/dogecoin.conf @echo "# Hot wallet for Make Post Sell" >> $(HOME)/.dogecoin/dogecoin.conf @echo "wallet=make_post_sell_hot_wallet" >> $(HOME)/.dogecoin/dogecoin.conf + @echo "" >> $(HOME)/.dogecoin/dogecoin.conf + @echo "# Transaction fee settings" >> $(HOME)/.dogecoin/dogecoin.conf + @echo "# Set reasonable fee for multi-output transactions (sendmany)" >> $(HOME)/.dogecoin/dogecoin.conf + @echo "paytxfee=0.001" >> $(HOME)/.dogecoin/dogecoin.conf + @echo "# Minimum fee per KB (prevents 0-fee transactions)" >> $(HOME)/.dogecoin/dogecoin.conf + @echo "mintxfee=0.001" >> $(HOME)/.dogecoin/dogecoin.conf @echo "" @echo "✓ Configuration created at: $(HOME)/.dogecoin/dogecoin.conf" @echo "" @@ -621,6 +630,37 @@ dogecoin-node-full: check-dogecoin -rpcallowip=127.0.0.1 \ -server=1 +# Check and fix Dogecoin transaction fee +dogecoin-check-fee: check-dogecoin + @echo "=== Checking Dogecoin Transaction Fee ===" + @if ! dogecoin-cli getwalletinfo >/dev/null 2>&1; then \ + echo "❌ Dogecoin daemon not running"; \ + echo "Start with: make dogecoin-node"; \ + exit 1; \ + fi + @fee=$$(dogecoin-cli getwalletinfo | grep paytxfee | awk '{print $$2}' | tr -d ','); \ + echo "Current transaction fee: $$fee DOGE"; \ + if [ "$$(echo "$$fee > 0.001" | bc -l)" = "1" ]; then \ + echo "⚠️ WARNING: Transaction fee is too high!"; \ + echo "This may cause sendmany (multi-output transactions) to fail."; \ + echo "Run 'make dogecoin-fix-fee' to set reasonable fee."; \ + else \ + echo "✓ Transaction fee is reasonable"; \ + fi + +# Set reasonable Dogecoin transaction fee +dogecoin-fix-fee: check-dogecoin + @echo "=== Setting Dogecoin Transaction Fee ===" + @if ! dogecoin-cli getwalletinfo >/dev/null 2>&1; then \ + echo "❌ Dogecoin daemon not running"; \ + echo "Start with: make dogecoin-node"; \ + exit 1; \ + fi + @echo "Setting transaction fee to 0.001 DOGE..." + @dogecoin-cli settxfee 0.001 + @echo "✓ Transaction fee updated" + @echo "New fee: $$(dogecoin-cli getwalletinfo | grep paytxfee | awk '{print $$2}' | tr -d ',') DOGE" + # Check Dogecoin sync status and wallet info dogecoin-status: check-dogecoin @echo "=== Dogecoin Node Status ===" diff --git a/make_post_sell/lib/crypto_watcher/__init__.py b/make_post_sell/lib/crypto_watcher/__init__.py index 0d4c251..1890efd 100644 --- a/make_post_sell/lib/crypto_watcher/__init__.py +++ b/make_post_sell/lib/crypto_watcher/__init__.py @@ -864,14 +864,16 @@ def auto_sweep_payment_xmr(client, crypto_payment: CryptoPayment, dbsession=None # Get dynamic fee estimate by doing a test transfer with do_not_relay=true try: # Estimate fee by doing a test transfer without broadcasting - # This gives accurate fee based on actual transaction size + # Use 96% of amount to leave room for fee in the same subaddress + test_amount_piconero = int(payment_amount_piconero * Decimal("0.96")) + test_transfer_result = client._call( "transfer", { "destinations": [ { "address": crypto_payment.shop_sweep_to_address, - "amount": payment_amount_piconero, + "amount": test_amount_piconero, } ], "account_index": crypto_payment.account_index, @@ -881,18 +883,24 @@ def auto_sweep_payment_xmr(client, crypto_payment: CryptoPayment, dbsession=None "get_tx_metadata": True, }, ) - estimated_fee_piconero = int( + dynamic_fee_piconero = int( test_transfer_result.get("fee", 100000000000) ) # Fallback to ~0.0001 XMR + # Add 10% margin to the dynamic fee estimate to ensure sweep always works + estimated_fee_piconero = int(dynamic_fee_piconero * Decimal("1.1")) log.payment_info( crypto_payment, - f"Dynamic fee estimate: {estimated_fee_piconero / atomic_units} XMR (single transaction)", + f"Dynamic fee estimate: {dynamic_fee_piconero / atomic_units} XMR, using {estimated_fee_piconero / atomic_units} XMR (with 10% margin)", ) except Exception as e: - # Fallback to hardcoded fee if RPC call fails - estimated_fee_piconero = int(Decimal("0.0001") * atomic_units) + # Fallback based on historical data: actual fees ~0.0000306 XMR + # Use 3x multiplier for safety margin + estimated_fee_piconero = int( + Decimal("0.0000918") * atomic_units + ) # ~91,800,000 piconero (3x typical fee) log.error_with_context( - "Failed to get dynamic fee estimate, using fallback", e + f"Failed to get dynamic fee estimate, using 3x typical fee as fallback: {e}", + e, ) # Calculate transfer amount: payment minus fee and reserve for pending refunds @@ -2758,85 +2766,98 @@ def process_payment( env_request.dbsession, crypto_payment ) - # EARLY DETECTION: Check for overpayments on existing received payments + # Check if received payment has enough confirmations to become confirmed elif ( - payment_rescue - and crypto_payment.invoice - and crypto_payment.invoice.user - and crypto_payment.status - == CryptoPayment.STATUS_RECEIVED # Only check received status - and received_amount_int - > expected_amount_int # Check current received amount for overpayment + crypto_payment.status == CryptoPayment.STATUS_RECEIVED + and received_amount_int >= expected_amount_int # Has enough amount and early_min_confs >= int( crypto_payment.confirmations_required - ) # Wait for required confirmations + ) # Has enough confirmations ): - # Overpayment detected - check if it exceeds threshold - coin_config = get_coin_config(crypto_payment.coin_type) - atomic_units = coin_config["atomic_units"] - received_crypto = Decimal(received_amount_int) / atomic_units - expected_crypto = Decimal(expected_amount_int) / atomic_units - - log.payment_info( - crypto_payment, - f"EARLY DETECTION - Potential overpayment: " - f"received {received_crypto} {crypto_payment.coin_type}, expected {expected_crypto} {crypto_payment.coin_type} " - f"({early_min_confs}/{crypto_payment.confirmations_required} confirmations) - checking threshold", - ) - - # Check if overpayment exceeds 5% threshold using PaymentRescue logic - refund_details = payment_rescue.handle_overpayment( - crypto_payment, - expected_crypto, - received_crypto, - crypto_payment.invoice.user, - ) - - if refund_details: - # Overpayment exceeds threshold - process refund + # Check for exact payment first + if received_amount_int == expected_amount_int: log.payment_info( crypto_payment, - f"EARLY DETECTION - Overpayment threshold exceeded: {refund_details}", - ) - - # Mark as confirmed with overpayment detected (refund pending) - crypto_payment.status = CryptoPayment.STATUS_CONFIRMED_OVERPAY - crypto_payment.refund_reason = refund_details["reason"] - - # Try refund immediately since we have enough confirmations - result = payment_rescue.execute_refund( - refund_details, crypto_payment - ) - if result["success"]: - log.payment_info( - crypto_payment, f"Excess refunded: TX {result['tx_hash']}" - ) - # Mark as confirmed with overpayment refunded - crypto_payment.status = ( - CryptoPayment.STATUS_CONFIRMED_OVERPAY_REFUNDED - ) - crypto_payment.refund_tx_hash = result["tx_hash"] - crypto_payment.refund_confirmations = 0 # Just sent - - # Finalize invoice since core payment amount is sufficient - finalize_invoice(env_request, crypto_payment, send_emails=True) - - # Note: Restocking fee will be swept when refund is fully confirmed - # to avoid double-sweeping before refund transaction is safely confirmed - else: - log.payment_error( - crypto_payment, - f"Refund failed for overpayment: {result['error']}", - ) - else: - # Overpayment within acceptable threshold - just confirm normally - log.payment_info( - crypto_payment, - "EARLY DETECTION - Overpayment within 5% threshold - confirming normally", + f"Exact payment confirmed with {early_min_confs}/{crypto_payment.confirmations_required} confirmations", ) crypto_payment.status = CryptoPayment.STATUS_CONFIRMED finalize_invoice(env_request, crypto_payment, send_emails=True) + # EARLY DETECTION: Check for overpayments on existing received payments + elif ( + payment_rescue + and crypto_payment.invoice + and crypto_payment.invoice.user + and received_amount_int > expected_amount_int # Overpayment + ): + # Overpayment detected - check if it exceeds threshold + coin_config = get_coin_config(crypto_payment.coin_type) + atomic_units = coin_config["atomic_units"] + received_crypto = Decimal(received_amount_int) / atomic_units + expected_crypto = Decimal(expected_amount_int) / atomic_units + + log.payment_info( + crypto_payment, + f"EARLY DETECTION - Potential overpayment: " + f"received {received_crypto} {crypto_payment.coin_type}, expected {expected_crypto} {crypto_payment.coin_type} " + f"({early_min_confs}/{crypto_payment.confirmations_required} confirmations) - checking threshold", + ) + + # Check if overpayment exceeds 5% threshold using PaymentRescue logic + refund_details = payment_rescue.handle_overpayment( + crypto_payment, + expected_crypto, + received_crypto, + crypto_payment.invoice.user, + ) + + if refund_details: + # Overpayment exceeds threshold - process refund + log.payment_info( + crypto_payment, + f"EARLY DETECTION - Overpayment threshold exceeded: {refund_details}", + ) + + # Mark as confirmed with overpayment detected (refund pending) + crypto_payment.status = CryptoPayment.STATUS_CONFIRMED_OVERPAY + crypto_payment.refund_reason = refund_details["reason"] + + # Try refund immediately since we have enough confirmations + result = payment_rescue.execute_refund( + refund_details, crypto_payment + ) + if result["success"]: + log.payment_info( + crypto_payment, + f"Excess refunded: TX {result['tx_hash']}", + ) + # Mark as confirmed with overpayment refunded + crypto_payment.status = ( + CryptoPayment.STATUS_CONFIRMED_OVERPAY_REFUNDED + ) + crypto_payment.refund_tx_hash = result["tx_hash"] + crypto_payment.refund_confirmations = 0 # Just sent + + # Finalize invoice since core payment amount is sufficient + finalize_invoice( + env_request, crypto_payment, send_emails=True + ) + + # Note: Restocking fee will be swept when refund is fully confirmed + # to avoid double-sweeping before refund transaction is safely confirmed + else: + log.payment_error( + crypto_payment, + f"Refund failed for overpayment: {result['error']}", + ) + else: + # Overpayment within acceptable threshold - just confirm normally + log.payment_info( + crypto_payment, + "EARLY DETECTION - Overpayment within 5% threshold - confirming normally", + ) + crypto_payment.status = CryptoPayment.STATUS_CONFIRMED + finalize_invoice(env_request, crypto_payment, send_emails=True) # Check for overpayment (exact match or overpaid) - for first payment elif ( diff --git a/make_post_sell/lib/crypto_watcher/crypto_clients.py b/make_post_sell/lib/crypto_watcher/crypto_clients.py index 9702e30..cc7b3c6 100644 --- a/make_post_sell/lib/crypto_watcher/crypto_clients.py +++ b/make_post_sell/lib/crypto_watcher/crypto_clients.py @@ -300,9 +300,22 @@ class DogecoinClient: """Send Dogecoin to an address. Returns transaction ID.""" return self._call("sendtoaddress", [address, amount, comment]) - def sendmany(self, from_label: str, addresses_amounts: Dict[str, float]) -> str: - """Send to multiple addresses at once. More efficient for sweeping.""" - return self._call("sendmany", [from_label, addresses_amounts]) + def sendmany(self, from_label: str, addresses_amounts: Dict[str, float], minconf: int = 1, comment: str = "") -> str: + """Send to multiple addresses at once. More efficient for sweeping. + + Args: + from_label: Account label (use "" for default account) + addresses_amounts: Dict mapping addresses to amounts + minconf: Minimum confirmations (default: 1) + comment: Transaction comment (optional) + """ + # Build params list - Dogecoin expects specific parameter order + params = [from_label, addresses_amounts] + if minconf != 1 or comment: + params.append(minconf) + if comment: + params.append(comment) + return self._call("sendmany", params) # Blockchain Info diff --git a/make_post_sell/lib/crypto_watcher/crypto_payment_rescue.py b/make_post_sell/lib/crypto_watcher/crypto_payment_rescue.py index 87c7983..48e559b 100644 --- a/make_post_sell/lib/crypto_watcher/crypto_payment_rescue.py +++ b/make_post_sell/lib/crypto_watcher/crypto_payment_rescue.py @@ -119,6 +119,7 @@ class PaymentRescue: "excess_amount": excess_amount, "refund_amount": refund_amount, "fee_amount": excess_amount - refund_amount, + "payment_amount": expected_amount, # Shop should get the actual payment too! "reason": f"Overpayment exceeds {int(OVERPAYMENT_THRESHOLD_PERCENT * 100)}% threshold: received {received_amount} but expected {expected_amount}", } @@ -158,10 +159,14 @@ class PaymentRescue: def execute_refund(self, refund_details, payment=None): """ Execute the actual refund transaction. + + Now implements multi-output transactions: + - Customer gets refund minus fee + - Shop owner gets the fee portion Args: refund_details: dict with refund information - payment: CryptoPayment object (needed for account_index) + payment: CryptoPayment object (needed for account_index and shop_sweep_to_address) Returns: dict with transaction details or raises exception @@ -171,6 +176,9 @@ class PaymentRescue: logger = logging.getLogger(__name__) refund_amount_coin = refund_details["refund_amount"] + fee_amount_coin = refund_details["fee_amount"] + # For overpayments, also include the actual payment amount + payment_amount_coin = refund_details.get("payment_amount", Decimal("0")) # Get coin type and atomic units for proper logging coin_type = payment.coin_type if payment else "XMR" @@ -179,6 +187,8 @@ class PaymentRescue: coin_config = get_coin_config(coin_type) atomic_units = int(coin_config["atomic_units"]) refund_amount_atomic = int(refund_amount_coin * atomic_units) + fee_amount_atomic = int(fee_amount_coin * atomic_units) + payment_amount_atomic = int(payment_amount_coin * atomic_units) atomic_unit_name = ( "piconero" @@ -186,11 +196,22 @@ class PaymentRescue: else "koinu" if coin_type == "DOGE" else "atomic units" ) + # Get shop sweep address - we'll handle missing address with a clear error + shop_sweep_address = payment.shop_sweep_to_address if payment else None + if payment: logger.info(f"Attempting to refund: {payment}") logger.info( f"Refund amount: {refund_amount_coin} {coin_type} ({refund_amount_atomic} {atomic_unit_name})" ) + logger.info( + f"Fee amount to shop: {fee_amount_coin} {coin_type} ({fee_amount_atomic} {atomic_unit_name})" + ) + if payment_amount_coin > 0: + logger.info( + f"Payment amount to shop: {payment_amount_coin} {coin_type} ({payment_amount_atomic} {atomic_unit_name})" + ) + logger.info(f"Shop sweep address: {shop_sweep_address}") logger.info(f"Refund address: {refund_details['refund_address']}") logger.info(f"Refund reason: {refund_details['reason']}") else: @@ -242,6 +263,7 @@ class PaymentRescue: logger.info("Checking Dogecoin wallet balance") balance_result = self.crypto_client.getbalance() logger.info(f"Dogecoin wallet balance: {balance_result} DOGE") + if balance_result < refund_amount_coin: logger.warning( @@ -253,14 +275,43 @@ class PaymentRescue: try: # Create coin-specific refund transaction if coin_type == "XMR": - # Monero uses the transfer RPC with atomic units (piconero) + # Monero always uses multi-output transfer + if not shop_sweep_address: + raise ValueError("Shop sweep address is required for refunds") + + # Build destinations array + destinations = [ + { + "address": refund_details["refund_address"], + "amount": refund_amount_atomic, + } + ] + + # For overpayments, combine payment amount and fee into single shop output + shop_amount_atomic = fee_amount_atomic + if payment_amount_atomic > 0: + shop_amount_atomic += payment_amount_atomic + + destinations.append({ + "address": shop_sweep_address, + "amount": shop_amount_atomic, + }) + + if payment_amount_atomic > 0: + logger.info( + f"Monero multi-output transfer: " + f"{refund_amount_atomic} piconero to customer, " + f"{shop_amount_atomic} piconero to shop (payment: {payment_amount_atomic} + fee: {fee_amount_atomic})" + ) + else: + logger.info( + f"Monero multi-output transfer: " + f"{refund_amount_atomic} piconero to customer, " + f"{fee_amount_atomic} piconero to shop" + ) + transfer_params = { - "destinations": [ - { - "address": refund_details["refund_address"], - "amount": refund_amount_atomic, - } - ], + "destinations": destinations, "account_index": account_index, "get_tx_key": True, "do_not_relay": False, @@ -270,23 +321,148 @@ class PaymentRescue: tx_result = self.crypto_client._call("transfer", transfer_params) elif coin_type == "DOGE": - # Dogecoin uses sendtoaddress RPC with coin amounts (not atomic units) + # Dogecoin always uses sendmany for multi-output + if not shop_sweep_address: + raise ValueError("Shop sweep address is required for refunds") + # Round to 8 decimal places to match DOGE precision requirements refund_amount_doge = round(float(refund_amount_coin), 8) - logger.info( - f"Calling Dogecoin sendtoaddress: {refund_amount_doge} DOGE to {refund_details['refund_address']}" - ) - logger.info( - f"Dogecoin client config - URL: {self.crypto_client.rpc_url}, User: {self.crypto_client.rpc_user}" - ) - logger.info( - f"About to call sendtoaddress with params: address={refund_details['refund_address']}, amount={refund_amount_doge}" - ) - tx_hash = self.crypto_client.sendtoaddress( - refund_details["refund_address"], - refund_amount_doge, - f"Overpayment refund for {refund_details['payment_id']}", - ) + fee_amount_doge = round(float(fee_amount_coin), 8) + payment_amount_doge = round(float(payment_amount_coin), 8) if payment_amount_coin > 0 else 0 + + # Initial outputs - will be adjusted below if needed + outputs = {} + + # For overpayments, combine payment amount and fee into single shop output + shop_amount_doge = fee_amount_doge + if payment_amount_doge > 0: + shop_amount_doge = round(payment_amount_doge + fee_amount_doge, 8) + + # Account for network fee by reducing amounts proportionally + # Dogecoin sendmany adds fee on top of outputs, so we need to leave room + total_output = refund_amount_doge + shop_amount_doge + # Use conservative fee estimate to ensure we have enough room + # This accounts for fee calculation differences and rounding + estimated_fee = 0.01 # Very conservative to avoid insufficient funds errors + + # Check if we need to adjust for fees + if total_output + estimated_fee > balance_result: + # Calculate how much we need to reduce + shortage = (total_output + estimated_fee) - balance_result + logger.info(f"Need to reduce outputs by {shortage} DOGE to account for network fee") + + # Reduce both amounts proportionally + refund_ratio = refund_amount_doge / total_output + shop_ratio = shop_amount_doge / total_output + + # Reduce and round conservatively (round down to avoid exceeding balance) + import math + refund_reduction = shortage * refund_ratio + shop_reduction = shortage * shop_ratio + + # Round down to 3 decimal places for cleaner amounts + refund_amount_doge = math.floor((refund_amount_doge - refund_reduction) * 1000) / 1000 + shop_amount_doge = math.floor((shop_amount_doge - shop_reduction) * 1000) / 1000 + + logger.info(f"Adjusted amounts - Customer: {refund_amount_doge} DOGE, Shop: {shop_amount_doge} DOGE") + + # Build final outputs with adjusted amounts + outputs[refund_details["refund_address"]] = refund_amount_doge + outputs[shop_sweep_address] = shop_amount_doge + + if payment_amount_doge > 0: + logger.info( + f"Calling Dogecoin sendmany with multi-output: " + f"{refund_amount_doge} DOGE to customer, " + f"{shop_amount_doge} DOGE to shop (payment: {payment_amount_doge} + fee: {fee_amount_doge})" + ) + else: + logger.info( + f"Calling Dogecoin sendmany with multi-output: " + f"{refund_amount_doge} DOGE to customer, " + f"{fee_amount_doge} DOGE to shop" + ) + + # First check if we have enough balance including fees + # Already accounted for in shop_amount_doge adjustment above + total_needed = refund_amount_doge + shop_amount_doge + + if balance_result < total_needed: + logger.error( + f"Insufficient balance for refund: " + f"have {balance_result} DOGE, need {total_needed} DOGE" + ) + raise ValueError(f"Insufficient balance: have {balance_result}, need {total_needed}") + + # Get the account label for the payment address + # First, try to get account from the address + from_account = "" + try: + # Try newer method first (getaddressinfo) + addr_info = self.crypto_client._call("getaddressinfo", [payment.address]) + if addr_info and "labels" in addr_info and addr_info["labels"]: + from_account = addr_info["labels"][0] + logger.info(f"Using account label from getaddressinfo: {from_account}") + except Exception as e: + # Fall back to older method (getaccount) + try: + from_account = self.crypto_client._call("getaccount", [payment.address]) + if from_account: + logger.info(f"Using account from getaccount: {from_account}") + except Exception as e2: + # Last resort: try to find in unspent outputs + try: + unspent = self.crypto_client._call("listunspent", []) + for utxo in unspent: + if utxo.get("address") == payment.address: + from_account = utxo.get("account", "") + if from_account: + logger.info(f"Using account from unspent output: {from_account}") + break + except Exception as e3: + logger.warning(f"Could not get account for address: {e}, {e2}, {e3}") + from_account = "" + + # sendmany(fromaccount, {address:amount,...}, minconf=1) + # Don't include comment - it causes issues with many Dogecoin versions + + # Ensure outputs are properly formatted - sometimes decimal precision issues cause 500 errors + # Convert outputs to ensure they're plain floats, not Decimal objects + clean_outputs = {} + for addr, amount in outputs.items(): + clean_outputs[addr] = float(amount) + + # If account has special characters, try different approaches + logger.info(f"Attempting sendmany with account='{from_account}', outputs={clean_outputs}") + + # Try different sendmany approaches until one works + tx_hash = None + attempts = [ + # 1. Try with the actual account first (where the funds are) + ("actual account", lambda: self.crypto_client.sendmany(from_account, clean_outputs, 1)), + # 2. Try with empty account (default) + ("empty account", lambda: self.crypto_client.sendmany("", clean_outputs, 1)), + # 3. Try actual account without minconf + ("actual account without minconf", lambda: self.crypto_client.sendmany(from_account, clean_outputs)), + # 4. Try with explicit default account "*" + ("default account '*'", lambda: self.crypto_client.sendmany("*", clean_outputs, 1)), + ] + + last_error = None + for attempt_name, attempt_func in attempts: + try: + logger.info(f"Trying sendmany with {attempt_name}") + tx_hash = attempt_func() + logger.info(f"Success with {attempt_name}: {tx_hash}") + break + except Exception as e: + last_error = e + logger.warning(f"Failed with {attempt_name}: {e}") + continue + + if not tx_hash and last_error: + raise last_error + tx_result = {"tx_hash": tx_hash} else: diff --git a/make_post_sell/models/invoice.py b/make_post_sell/models/invoice.py index e980eb1..eb499d0 100644 --- a/make_post_sell/models/invoice.py +++ b/make_post_sell/models/invoice.py @@ -288,7 +288,7 @@ class Invoice(RBase, Base): @property def is_paid(self): """Check if this invoice has been successfully paid.""" - return self.payment_status in ["confirmed", "confirmed_overpaid", "paid"] + return self.payment_status in ["confirmed", "confirmed-overpay", "confirmed-complete", "paid"] def get_invoice_by_id(dbsession, invoice_id): @@ -354,7 +354,7 @@ def delete_invoice_by_id(dbsession, invoice_id): } # Guard: Don't delete successful payments - those are legitimate transactions - successful_statuses = ["confirmed", "confirmed-overpaid"] + successful_statuses = ["confirmed", "confirmed-overpay", "confirmed-complete"] if crypto_payment.status in successful_statuses: return { "success": False, diff --git a/make_post_sell/templates/crypto_checkout.j2 b/make_post_sell/templates/crypto_checkout.j2 index 7d46777..fceba94 100644 --- a/make_post_sell/templates/crypto_checkout.j2 +++ b/make_post_sell/templates/crypto_checkout.j2 @@ -58,7 +58,7 @@
Total Amount: {{ '%.12f' % amount_crypto }} {{ coin_symbol }}
Expected {{ smallest_unit_name }}: {{ '{:,}'.format(expected_smallest_units) }}
Payment ID: {{ payment_id }}
Invoice ID: {{ invoice_id }}
{% endif %} {% else %} @@ -67,7 +67,7 @@Total Amount: {{ '%.8f' % amount_crypto }} {{ coin_symbol }}
Expected {{ smallest_unit_name }}: {{ '{:,}'.format(expected_smallest_units) }}
Payment ID: {{ payment_id }}
Invoice ID: {{ invoice_id }}
{% endif %} {% endif %} @@ -191,7 +191,7 @@ const currentStatus = statusEl ? statusEl.textContent.trim() : ''; // Use smart redirect URL for confirmed states, crypto-quotes for terminal states - if (currentStatus === 'confirmed' || currentStatus === 'confirmed-overpaid') { + if (currentStatus === 'confirmed' || currentStatus === 'confirmed-complete' || currentStatus === 'confirmed-overpay') { invoiceLink.href = redirectUrl || '/u/purchases'; // Determine button text based on redirect URL if (redirectUrl && redirectUrl.includes('/p/')) { @@ -250,7 +250,7 @@ if (expiresEl) { expiresEl.parentNode.style.display = 'none'; } - } else if (lastKnownStatus && (lastKnownStatus === 'confirmed' || lastKnownStatus === 'confirmed-overpaid')) { + } else if (lastKnownStatus && (lastKnownStatus === 'confirmed' || lastKnownStatus === 'confirmed-complete' || lastKnownStatus === 'confirmed-overpay')) { // Payment confirmed - replace with invoice link replaceButtonsWithInvoiceLink(); @@ -377,7 +377,7 @@ console.log(`Status changed from ${lastKnownStatus} to ${currentStatus}`); // Show confirmations section when payment is received or later - if (currentStatus === 'received' || currentStatus === 'confirmed' || currentStatus === 'confirmed-overpaid') { + if (currentStatus === 'received' || currentStatus === 'confirmed' || currentStatus === 'confirmed-complete' || currentStatus === 'confirmed-overpay') { if (confirmationsEl) { confirmationsEl.style.display = 'block'; } @@ -399,7 +399,7 @@ if (expiresEl && expiresEl.parentNode) { expiresEl.parentNode.style.display = 'none'; } - } else if (currentStatus === 'confirmed' || currentStatus === 'confirmed-overpaid') { + } else if (currentStatus === 'confirmed' || currentStatus === 'confirmed-complete' || currentStatus === 'confirmed-overpay') { // Payment confirmed - replace with smart redirect link replaceButtonsWithInvoiceLink(data.redirect_url); @@ -410,7 +410,7 @@ // Redirect only if status changed from non-confirmed to confirmed (via polling) // Don't redirect if user browsed directly to already-confirmed quote - if (lastKnownStatus && lastKnownStatus !== 'confirmed' && lastKnownStatus !== 'confirmed-overpaid') { + if (lastKnownStatus && lastKnownStatus !== 'confirmed' && lastKnownStatus !== 'confirmed-complete' && lastKnownStatus !== 'confirmed-overpay') { setTimeout(() => { // Use smart redirect URL if available, otherwise fallback to purchases page window.location.href = data.redirect_url || '/u/purchases'; @@ -470,7 +470,7 @@ // Stop polling if we've reached a terminal status or confirmed status const currentStatus = statusEl ? statusEl.textContent.trim() : ''; - if (terminalStatuses.includes(currentStatus) || currentStatus === 'confirmed') { + if (terminalStatuses.includes(currentStatus) || currentStatus === 'confirmed' || currentStatus === 'confirmed-complete') { if (pollInterval) { clearInterval(pollInterval); pollInterval = null; diff --git a/make_post_sell/templates/crypto_quotes_history.j2 b/make_post_sell/templates/crypto_quotes_history.j2 index 4557baa..118f495 100644 --- a/make_post_sell/templates/crypto_quotes_history.j2 +++ b/make_post_sell/templates/crypto_quotes_history.j2 @@ -58,7 +58,7 @@ {% endif %}