diff --git a/make_post_sell/models/cart.py b/make_post_sell/models/cart.py
index 8dcc254..c29f265 100644
--- a/make_post_sell/models/cart.py
+++ b/make_post_sell/models/cart.py
@@ -412,6 +412,14 @@ class Cart(RBase, Base):
return True
return False
+ @property
+ def requires_stripe_payment(self):
+ """Return True if Stripe can legally process this cart, else False.
+ Stripe enforces a hard floor of $0.50 USD — below that, Stripe rejects the charge.
+ Use DOGE or XMR for purchases under $0.50.
+ """
+ return self.total_in_cents >= 50
+
@property
def is_not_public(self):
return not self.public
diff --git a/make_post_sell/models/invoice.py b/make_post_sell/models/invoice.py
index 2e6a6fd..e65d385 100644
--- a/make_post_sell/models/invoice.py
+++ b/make_post_sell/models/invoice.py
@@ -260,6 +260,14 @@ class Invoice(RBase, Base):
return True
return False
+ @property
+ def requires_stripe_payment(self):
+ """Return True if Stripe can legally process this invoice, else False.
+ Stripe enforces a hard floor of $0.50 USD — below that, Stripe rejects the charge.
+ Use DOGE or XMR for purchases under $0.50.
+ """
+ return self.total_in_cents >= 50
+
@property
def human_created_timestamp(self):
"""Return the created timestamp in a human-readable format."""
diff --git a/make_post_sell/models/shop.py b/make_post_sell/models/shop.py
index b6406e4..40734ff 100644
--- a/make_post_sell/models/shop.py
+++ b/make_post_sell/models/shop.py
@@ -151,6 +151,9 @@ class Shop(RBase, Base):
# Grayscale mode: render entire site in black and white
grayscale_mode = Column(Boolean, default=False)
+ # Public sales stats: show sales counts and revenue on the shop page (off by default)
+ public_sales_stats = Column(Boolean, default=False)
+
# Color filter: 0=off, 1=grayscale, 2=red, 3=green, 4=blue,
# 5=red+green, 6=red+blue, 7=green+blue
color_filter = Column(BigInteger, default=0)
diff --git a/make_post_sell/scripts/alembic/versions/1a2b3c4d5e6f_add_public_sales_stats_to_shop.py b/make_post_sell/scripts/alembic/versions/1a2b3c4d5e6f_add_public_sales_stats_to_shop.py
new file mode 100644
index 0000000..b58f7ed
--- /dev/null
+++ b/make_post_sell/scripts/alembic/versions/1a2b3c4d5e6f_add_public_sales_stats_to_shop.py
@@ -0,0 +1,37 @@
+"""add public_sales_stats to shop
+
+Revision ID: 1a2b3c4d5e6f
+Revises: 0ee5654cfe7d
+Create Date: 2026-04-10 00:00:00.000000
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '1a2b3c4d5e6f'
+down_revision = '0ee5654cfe7d'
+branch_labels = None
+depends_on = None
+
+
+def _column_exists(table, column):
+ from alembic import op as _op
+ from sqlalchemy import inspect
+ bind = _op.get_bind()
+ insp = inspect(bind)
+ return column in [c["name"] for c in insp.get_columns(table)]
+
+
+def upgrade():
+ if not _column_exists('mps_shop', 'public_sales_stats'):
+ op.add_column(
+ 'mps_shop',
+ sa.Column('public_sales_stats', sa.Boolean(), nullable=False, server_default='0'),
+ )
+
+
+def downgrade():
+ if _column_exists('mps_shop', 'public_sales_stats'):
+ op.drop_column('mps_shop', 'public_sales_stats')
diff --git a/make_post_sell/templates/shop.j2 b/make_post_sell/templates/shop.j2
index b4484a1..e69d615 100644
--- a/make_post_sell/templates/shop.j2
+++ b/make_post_sell/templates/shop.j2
@@ -2,6 +2,14 @@
{% block content -%}
+{% if sales_count is defined %}
+
+ {{ sales_count }} sale{{ 's' if sales_count != 1 else '' }}
+ ·
+ ${{ '%.2f'|format(sales_revenue) }} earned
+
+{% endif %}
+
{% if products %}
{% for product in products %}
diff --git a/make_post_sell/views/cart.py b/make_post_sell/views/cart.py
index a930df5..78bc623 100644
--- a/make_post_sell/views/cart.py
+++ b/make_post_sell/views/cart.py
@@ -591,6 +591,16 @@ def cart_checkout(request):
and not request.monero_enabled
and not request.dogecoin_enabled
)
+
+ # Stripe has a hard $0.50 minimum — block carts below that when only Stripe is available
+ if cart.total_in_cents > 0 and not cart.requires_stripe_payment and only_stripe_enabled:
+ msg = (
+ "Stripe requires a minimum of $0.50. Use DOGE or XMR for purchases under $0.50.",
+ "error",
+ )
+ request.session.flash(msg)
+ return HTTPFound("/cart")
+
if cart.requires_payment and only_stripe_enabled and stripe_user_shop is None:
msg = ("Please enter your payment information.", "info")
request.session.flash(msg)
@@ -709,6 +719,21 @@ def cart_complete_checkout(request):
request.session.flash(msg)
return HTTPFound("/cart")
+ # Block Stripe for carts under Stripe's $0.50 minimum when only Stripe is available
+ _only_stripe_at_checkout = (
+ request.stripe_enabled
+ and not request.paypal_enabled
+ and not request.monero_enabled
+ and not request.dogecoin_enabled
+ )
+ if cart.total_in_cents > 0 and not cart.requires_stripe_payment and _only_stripe_at_checkout:
+ msg = (
+ "Stripe requires a minimum of $0.50. Use DOGE or XMR for purchases under $0.50.",
+ "error",
+ )
+ request.session.flash(msg)
+ return HTTPFound("/cart")
+
# If Stripe is enabled and payment is required, ensure a Stripe customer exists
if (
cart.requires_payment
@@ -754,7 +779,7 @@ def cart_complete_checkout(request):
for invoice in invoices:
shop = invoice.shop
- if invoice.requires_payment:
+ if invoice.requires_stripe_payment:
stripe_user_shop = shop.stripe_user_shop(request.user)
if stripe_user_shop is None:
msg = (
diff --git a/make_post_sell/views/shop.py b/make_post_sell/views/shop.py
index 8ff7e48..77c31a6 100644
--- a/make_post_sell/views/shop.py
+++ b/make_post_sell/views/shop.py
@@ -230,9 +230,19 @@ def shop(request):
if "slug" not in request.matchdict:
return HTTPFound(f"/s/{shop.id}/{shop.slug}")
- return {
- "products": get_products_from_a_shop(shop),
- }
+ ctx = {"products": get_products_from_a_shop(shop)}
+
+ if shop.public_sales_stats:
+ from ..models.invoice import Invoice
+ from sqlalchemy import func as sa_func
+
+ invoices = shop.invoices
+ ctx["sales_count"] = invoices.count()
+ ctx["sales_revenue"] = invoices.with_entities(
+ sa_func.coalesce(sa_func.sum(Invoice.total_in_cents), 0)
+ ).scalar() / 100.0
+
+ return ctx
@view_config(route_name="shop_products", renderer="shop_products.j2")
@@ -832,6 +842,20 @@ def shop_settings(request):
)
)
+ # Handle public sales stats setting
+ public_sales_stats_value = request.params.get("public_sales_stats")
+ if public_sales_stats_value is not None:
+ public_sales_stats = int(public_sales_stats_value) == 1
+ if shop.public_sales_stats != public_sales_stats:
+ shop.public_sales_stats = public_sales_stats
+ status = "enabled" if public_sales_stats else "disabled"
+ request.session.flash(
+ (
+ f"Public sales stats are now {status}",
+ "success",
+ )
+ )
+
# Handle stripe settings form
if form_section == "stripe-settings":
# Handle disable action