Fix DOGE fee calculation bug and improve UI consistency

- Fix negative DOGE processing fees from estimatesmartfee RPC issues
- Add abs() protection for both DOGE and Monero fee calculations
- Reduce transaction size estimate from 0.25KB to 0.15KB (more realistic)
- Change fee multiplier from 2x to 1.5x (less excessive)
- Set reasonable fallback fee of 0.5 DOGE when RPC fails
- Add negative feerate detection with proper error handling
- Fix crypto payment status color mapping (case insensitive lookup)
- Add missing status mappings for confirmed-overpay-complete and doublepay-refund-complete
- Improve comment system UI with dark mode styling and better layout
- Add CSS preloader to prevent trans-background image flashing
- Enhance user settings page with better button organization
- Remove "View Quote" button for cancelled crypto quotes
- Fix comment form styling and authentication flow
This commit is contained in:
Russell Ballestrini 2025-10-06 09:36:02 -04:00
parent c61811293e
commit f5d68ba267
10 changed files with 240 additions and 55 deletions

BIN
docs/state-machine.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

View file

@ -7,6 +7,22 @@
*
* * * * * */
/* Preload hover background images to prevent flash */
body::after {
content: "";
position: absolute;
width: 0;
height: 0;
overflow: hidden;
z-index: -1;
background-image:
url("/static/img/trans-blue.png"),
url("/static/img/trans-green.png"),
url("/static/img/trans-red.png"),
url("/static/img/trans-blue2.png"),
url("/static/img/trans-green2.png"),
url("/static/img/trans-red2.png");
}
body {
margin: 0px;
@ -411,6 +427,28 @@ input.mps-button-red {
box-sizing: border-box;
}
/* Red cancel button */
.mps-cancel-button-red {
background: var(--red-color);
color: white;
border-color: var(--red-color);
}
[data-theme="dark"] .mps-cancel-button-red {
background-color: #2d3748;
background-image: url("/static/img/trans-red.png");
border: 2px solid var(--red-color);
color: var(--red-color);
font-weight: bold;
}
[data-theme="dark"] .mps-cancel-button-red:hover {
background-color: #2d3748;
background-image: url("/static/img/trans-red2.png");
color: #ffffff;
border: 2px solid var(--red-color);
}
.comment-actions {
margin-bottom: 42px;
display: grid;
@ -438,6 +476,92 @@ input.mps-button-red {
box-sizing: border-box;
}
/* Light mode color styling */
.reply-link {
background: var(--green-color);
color: white;
border-color: var(--green-color);
}
.delete-link {
background: var(--red-color);
color: white;
border-color: var(--red-color);
}
.mps-comment-form-button {
background: var(--green-color);
color: white;
border-color: var(--green-color);
}
[data-theme="dark"] .reply-link,
[data-theme="dark"] .edit-link,
[data-theme="dark"] .moderate-link {
background-color: #2d3748;
background-image: url("/static/img/trans-blue.png");
border: 2px solid var(--blue-color);
color: var(--blue-color);
font-weight: bold;
}
[data-theme="dark"] .reply-link:hover,
[data-theme="dark"] .edit-link:hover,
[data-theme="dark"] .moderate-link:hover {
background-color: #2d3748;
background-image: url("/static/img/trans-blue2.png");
color: #ffffff;
border: 2px solid var(--blue-color);
}
/* Reply button - green */
[data-theme="dark"] .reply-link {
background-color: #2d3748;
background-image: url("/static/img/trans-green.png");
border: 2px solid var(--green-color);
color: var(--green-color);
font-weight: bold;
}
[data-theme="dark"] .reply-link:hover {
background-color: #2d3748;
background-image: url("/static/img/trans-green2.png");
color: #ffffff;
border: 2px solid var(--green-color);
}
/* Delete button - red */
[data-theme="dark"] .delete-link {
background-color: #2d3748;
background-image: url("/static/img/trans-red.png");
border: 2px solid var(--red-color);
color: var(--red-color);
font-weight: bold;
}
[data-theme="dark"] .delete-link:hover {
background-color: #2d3748;
background-image: url("/static/img/trans-red2.png");
color: #ffffff;
border: 2px solid var(--red-color);
}
/* Post comment button - green */
[data-theme="dark"] .mps-comment-form-button {
background-color: #2d3748;
background-image: url("/static/img/trans-green.png");
border: 2px solid var(--green-color);
color: var(--green-color);
font-weight: bold;
}
[data-theme="dark"] .mps-comment-form-button:hover {
background-color: #2d3748;
background-image: url("/static/img/trans-green2.png");
color: #ffffff;
border: 2px solid var(--green-color);
}
.payment-toggle-button {
display: inline-block;
padding: 6px 12px;
@ -1339,27 +1463,31 @@ div.message-ribbon {
/* Flash message styling for dark mode */
[data-theme="dark"] .alert-danger,
[data-theme="dark"] .alert-error {
color: #ff6b6b;
background-color: #2d1f20;
border-color: #ff6b6b;
color: var(--red-color);
background-color: #2d3748;
background-image: url("/static/img/trans-red2.png");
border-color: var(--red-color);
}
[data-theme="dark"] .alert-warning {
color: #ffd93d;
background-color: #2d2a1f;
background-color: #2d3748;
background-image: url("/static/img/trans-red2.png");
border-color: #ffd93d;
}
[data-theme="dark"] .alert-info,
[data-theme="dark"] .alert-notice {
color: var(--blue-color);
background-color: #1f252d;
background-color: #2d3748;
background-image: url("/static/img/trans-blue2.png");
border-color: var(--blue-color);
}
[data-theme="dark"] .alert-success {
color: var(--green-color);
background-color: #1f2d1f;
background-color: #2d3748;
background-image: url("/static/img/trans-green2.png");
border-color: var(--green-color);
}
@ -1715,6 +1843,25 @@ img.crypto-button-icon {
margin-bottom: 20px;
}
[data-theme="dark"] .original-comment-box {
background-color: #2d3748;
background-image: url("/static/img/trans-blue.png");
border-left: 4px solid var(--blue-color);
color: var(--text-primary);
}
/* Comment reply styling */
.comment-reply {
margin-left: 0px;
margin-bottom: 20px;
border-left: 2px solid #ddd;
padding-left: 10px;
}
[data-theme="dark"] .comment-reply {
border-left: 2px solid var(--blue-color);
}
.start-button-icon {
font-size: 20px;
}

View file

@ -3,7 +3,7 @@
{% block title %}Edit Comment{% endblock %}
{% block content %}
<div class="container">
<section class="one-column">
<h2>Edit Comment</h2>
<div class="original-comment original-comment-box">
@ -18,14 +18,16 @@
<form method="post" action="/comments/{{ comment.id }}/edit">
<div class="form-group">
<label for="comment_data">Edit Your Comment:</label>
<textarea name="data" id="comment_data" rows="6" cols="80" required placeholder="Edit your comment... (You can use Markdown formatting)">{{ comment.data }}</textarea>
<label for="comment-data">Edit Your Comment:</label>
<textarea name="data" id="comment-data" rows="6" style="width: 100%;" required placeholder="Edit your comment... (You can use Markdown formatting)">{{ comment.data }}</textarea>
</div>
<br/>
<div class="form-actions">
<a href="{{ product.absolute_url(request) }}" class="mps-cancel-button">Cancel</a>
<a href="{{ product.absolute_url(request) }}" class="mps-cancel-button mps-cancel-button-red">Cancel</a>
<input type="submit" value="Update Comment" class="mps-comment-form-button" />
</div>
</form>
</div>
</section>
{% endblock %}

View file

@ -3,7 +3,7 @@
{% block title %}Reply to Comment{% endblock %}
{% block content %}
<div class="container">
<section class="one-column">
<h2>Reply to Comment</h2>
<div class="original-comment original-comment-box">
@ -18,14 +18,16 @@
<form method="post" action="{{ request.route_url('comment_reply', comment_id=parent_comment.id) }}">
<div class="form-group">
<label for="comment_data">Your Reply:</label>
<textarea name="data" id="comment_data" rows="6" cols="80" required placeholder="Write your reply... (You can use Markdown formatting)"></textarea>
<label for="comment-data">Your Reply:</label>
<textarea name="data" id="comment-data" rows="6" style="width: 100%;" required placeholder="Write your reply... (You can use Markdown formatting)"></textarea>
</div>
<br/>
<div class="form-actions">
<a href="{{ product.absolute_url(request) }}" class="mps-cancel-button">Cancel</a>
<a href="{{ product.absolute_url(request) }}" class="mps-cancel-button mps-cancel-button-red">Cancel</a>
<input type="submit" value="Post Reply" class="mps-comment-form-button" />
</div>
</form>
</div>
</section>
{% endblock %}

View file

@ -30,7 +30,7 @@
{% block content -%}
<section class="two-column">
<section class="one-column">
<div class="product-images">
{% if "thumbnail1" in product.extensions %}

View file

@ -68,7 +68,7 @@
<a href="{{ request.route_url('crypto_quote', payment_id=payment.id) }}" class="mps-button mps-button-small mps-button-blue">
View Quote
</a>
{% elif payment.status.endswith('-refunded') or payment.status.endswith('-refunded-complete') or payment.status == 'cancelled' %}
{% elif payment.status.endswith('-refunded') or payment.status.endswith('-refunded-complete') %}
<a href="{{ request.route_url('crypto_quote', payment_id=payment.id) }}" class="mps-button mps-button-small mps-button-blue">
View Quote
</a>

View file

@ -47,7 +47,7 @@
</div>
<section class="product-right">
<br/>
<div class="well">
<h1>${{ '{:,.2f}'.format(product.price) }}</h1>
@ -59,8 +59,6 @@
<a href="{{ signed_get_object_url }}" class="product-download-button mps-button" download>&#11123 Download</a>
<br/>
<br/>
<hr/>
<br/>
{% endif %}
{% endif %}

View file

@ -1,6 +1,6 @@
{% macro render_comment(comment, shop, request, max_depth=5) %}
{% if comment.depth <= max_depth and (comment.approved or (request.user.authenticated and (shop.is_owner(request.user) or shop.is_editor(request.user)))) %}
<div id="comment-{{ comment.id }}" class="comment{% if comment.parent_id %} reply{% endif %}" style="margin-left: {{ comment.depth * 20 }}px; margin-bottom: 20px;{% if comment.parent_id %} border-left: 2px solid #ddd; padding-left: 10px;{% endif %}">
<div id="comment-{{ comment.id }}" class="comment{% if comment.parent_id %} reply comment-reply{% endif %}" style="margin-left: {{ comment.depth * 20 }}px; margin-bottom: 20px;">
<div class="comment-header">
<strong>{{ comment.user.name if comment.user else "Anonymous" }}</strong>
<span class="comment-date">{{ comment.ago_string }}</span>
@ -16,13 +16,14 @@
{{ comment.data_html | safe }}
</div>
{% if request.user.authenticated %}
<div class="comment-actions">
{% if request.user.authenticated and (request.user.id == comment.user_id or comment.can_user_moderate(request.user, shop)) %}
{% if request.user.id == comment.user_id or comment.can_user_moderate(request.user, shop) %}
<input type="submit" value="Delete" class="delete-link" onclick="return confirm('Are you sure you want to delete this comment?')" form="delete-{{ comment.id }}" />
<form id="delete-{{ comment.id }}" method="post" action="/comments/{{ comment.id }}/delete" class="hidden-form"></form>
{% endif %}
{% if request.user.authenticated and comment.can_user_moderate(request.user, shop) %}
{% if comment.can_user_moderate(request.user, shop) %}
{% if comment.approved %}
<input type="submit" value="Unapprove" class="moderate-link" form="unapprove-{{ comment.id }}" />
<form id="unapprove-{{ comment.id }}" method="post" action="/comments/{{ comment.id }}/unapprove" class="hidden-form"></form>
@ -32,17 +33,18 @@
{% endif %}
{% endif %}
{% if request.user.authenticated and (request.user.id == comment.user_id or comment.can_user_moderate(request.user, shop)) %}
{% if request.user.id == comment.user_id or comment.can_user_moderate(request.user, shop) %}
<a href="/comments/{{ comment.id }}/edit" class="edit-link">Edit</a>
{% endif %}
{% if request.user.authenticated and not comment.is_locked %}
{% if not comment.is_locked %}
{% set can_comment, error_msg = comment.can_user_comment(request.user, shop) %}
{% if can_comment %}
<a href="/comments/{{ comment.id }}/reply" class="reply-link">Reply</a>
{% endif %}
{% endif %}
</div>
{% endif %}
{% if comment.enabled_children %}
{% for child in comment.enabled_children %}
@ -57,7 +59,14 @@
<div class="comments-section">
{% if comments %}
<h3>Comments & Reviews ({{ product.public_comment_count }})</h3>
{% endif %}
<!-- Authentication prompt for non-authenticated users -->
{% if not request.user.authenticated %}
<p><a href="/join-or-log-in">Sign in</a> to leave a comment.</p>
{% endif %}
{% if comments %}
{% for comment in comments %}
{{ render_comment(comment, shop, request) }}
{% endfor %}
@ -78,20 +87,20 @@
<input type="hidden" name="parent_id" value="" />
<div>
<label for="comment_data">Comment:</label>
<textarea name="data" id="comment_data" rows="4" cols="50" required></textarea>
<label for="comment-data">Comment:</label>
<textarea name="data" id="comment-data" rows="4" style="width: 100%;" required></textarea>
</div>
<br/>
<div>
<input type="submit" value="Post Comment" class="mps-comment-form-button" />
<input type="submit" value="Send Comment" class="mps-comment-form-button" />
</div>
</form>
</div>
{% else %}
<p class="comment-error">{{ error_msg }}</p>
{% endif %}
{% else %}
<p><a href="/join-or-log-in">Sign in</a> to leave a comment.</p>
{% endif %}
</div>
{% endif %}

View file

@ -114,6 +114,7 @@
<section class="one-column">
<section class="well">
<h3>Account Actions</h3>
{% if request.is_saas_domain %}
<a href="/u/shops" class="product-edit-button mps-button">My Shops</a>
@ -129,8 +130,11 @@
<a href="/u/settings/crypto" class="product-edit-button mps-button">Crypto Payment Preferences</a>
<br/>
<a href="/u/addresses" class="product-edit-button mps-button">Shipping Addresses</a>
<br/>
{% endif %}
<a href="/" class="mps-button mps-button-blue">Back to Shop</a>
</section>
</section>

View file

@ -1142,11 +1142,13 @@ def estimate_monero_fee_for_quote(settings, shop_sweep_to_address, amount_picone
# Double the fee to cover both inbound (customer) and outbound (sweep) transactions
total_fee_piconero = estimated_fee_piconero * 2
return Decimal(total_fee_piconero) / Decimal("1000000000000") # Convert to XMR
return abs(
Decimal(total_fee_piconero) / Decimal("1000000000000")
) # Convert to XMR
except Exception as e:
# Fallback to hardcoded fee if RPC call fails, doubled for inbound + outbound
fallback_fee = float(settings.get("monero.fee_buffer", "0.0001")) * 2
return Decimal(str(fallback_fee))
return abs(Decimal(str(fallback_fee)))
def estimate_dogecoin_fee_for_quote(settings):
@ -1164,41 +1166,56 @@ def estimate_dogecoin_fee_for_quote(settings):
logger.info(f"Dogecoin estimatesmartfee result: {fee_estimate_result}")
if fee_estimate_result and "feerate" in fee_estimate_result:
# feerate is in DOGE per KB, estimate transaction size as ~0.25KB (more realistic)
# Typical DOGE transaction is ~225 bytes, not 500 bytes
# feerate is in DOGE per KB, estimate transaction size as ~0.15KB (realistic)
# Typical DOGE transaction is ~150 bytes for simple transfers
network_feerate = float(fee_estimate_result["feerate"])
# Cap fee rate at reasonable maximum (0.1 DOGE/KB = ~$0.025/KB)
# Dogecoin fees should never exceed this under normal conditions
max_reasonable_feerate = 0.1
# Ensure positive feerate - negative values indicate RPC issues
if network_feerate <= 0:
logger.warning(
f"Invalid negative/zero feerate {network_feerate} from estimatesmartfee, using fallback"
)
raise ValueError(f"Invalid feerate: {network_feerate}")
# Cap fee rate at reasonable maximum (100 DOGE/KB = ~$26/KB)
# This is very generous - normal DOGE fees are much lower
max_reasonable_feerate = 100.0
if network_feerate > max_reasonable_feerate:
logger.warning(
f"Network fee rate {network_feerate} DOGE/KB is excessive, capping at {max_reasonable_feerate} DOGE/KB"
)
network_feerate = max_reasonable_feerate
estimated_fee_doge = network_feerate * 0.25
# Double the fee to cover both inbound (customer) and outbound (sweep) transactions
total_fee_doge = estimated_fee_doge * 2
estimated_fee_doge = network_feerate * 0.15
# Add reasonable buffer for sweep transaction (not doubling)
total_fee_doge = estimated_fee_doge * 1.5
# Cap the total fee at a reasonable maximum (0.5 DOGE = ~$0.13)
# This should rarely be hit if RPC is working properly
max_reasonable_total_fee = 0.5
if total_fee_doge > max_reasonable_total_fee:
logger.warning(
f"Calculated DOGE fee {total_fee_doge} is excessive, capping at {max_reasonable_total_fee} DOGE"
)
total_fee_doge = max_reasonable_total_fee
logger.info(
f"Using dynamic DOGE fee: {network_feerate} DOGE/KB * 0.25KB * 2 = {total_fee_doge} DOGE"
f"Using dynamic DOGE fee: {network_feerate} DOGE/KB * 0.15KB * 1.5 = {total_fee_doge} DOGE"
)
return Decimal(str(total_fee_doge))
return abs(Decimal(str(total_fee_doge)))
else:
# Fallback to hardcoded fee if estimatesmartfee fails, doubled for inbound + outbound
# Reduce fallback from 0.01 to 0.002 DOGE (~$0.0006 vs $0.003)
fallback_fee = float(settings.get("dogecoin.fee_buffer", "0.002")) * 2
# Fallback to reasonable fee if estimatesmartfee fails - RPC should be working!
fallback_fee = float(settings.get("dogecoin.fee_buffer", "0.5"))
logger.warning(
f"Dogecoin estimatesmartfee failed, using fallback fee: {fallback_fee} DOGE"
f"Dogecoin estimatesmartfee failed (RPC issue?), using fallback fee: {fallback_fee} DOGE"
)
return Decimal(str(fallback_fee))
return abs(Decimal(str(fallback_fee)))
except Exception as e:
# Fallback to hardcoded fee if RPC call fails, doubled for inbound + outbound
fallback_fee = float(settings.get("dogecoin.fee_buffer", "0.002")) * 2
# Fallback to reasonable fee if RPC call fails - RPC should be working!
fallback_fee = float(settings.get("dogecoin.fee_buffer", "0.5"))
logger.error(
f"Dogecoin fee estimation error: {e}, using fallback fee: {fallback_fee} DOGE"
f"Dogecoin fee estimation error: {e} (RPC issue?), using fallback fee: {fallback_fee} DOGE"
)
return Decimal(str(fallback_fee))
return abs(Decimal(str(fallback_fee)))
def get_payment_status_info(status):
@ -1209,6 +1226,10 @@ def get_payment_status_info(status):
"confirmed": {"label": "✓ Confirmed", "color": "#28a745"},
"confirmed-complete": {"label": "✓ Confirmed (Complete)", "color": "#28a745"},
"confirmed-overpay": {"label": "✓ Confirmed (Overpaid)", "color": "#28a745"},
"confirmed-overpay-complete": {
"label": "✓ Confirmed (Overpaid)",
"color": "#28a745",
},
"expired": {"label": "Expired", "color": "#6c757d"},
"expired": {"label": "Expired", "color": "#6c757d"},
"latepay-refunded": {"label": "Late Payment - Refunded", "color": "#fd7e14"},
@ -1242,7 +1263,7 @@ def get_payment_status_info(status):
"label": "Duplicate Payment - Refunding",
"color": "#fd7e14",
},
"doublepay-refunded-complete": {
"doublepay-refund-complete": {
"label": "✓ Duplicate Payment - Refunded",
"color": "#fd7e14",
},
@ -1267,7 +1288,9 @@ def get_payment_status_info(status):
"color": "#dc3545",
},
}
return status_mapping.get(status, {"label": status.title(), "color": "#6c757d"})
return status_mapping.get(
status.lower(), {"label": status.title(), "color": "#6c757d"}
)
# TODO: COMMENT OUT WHEN DEBUGGING COMPLETE - TEMPORARY PRODUCTION DEBUG ROUTE