diff --git a/make_post_sell/lib/crypto_watcher/__init__.py b/make_post_sell/lib/crypto_watcher/__init__.py index 3d14713..ad3e11d 100644 --- a/make_post_sell/lib/crypto_watcher/__init__.py +++ b/make_post_sell/lib/crypto_watcher/__init__.py @@ -1275,6 +1275,36 @@ class ShopContextRequestWrapper: """Proxy app attribute from original request.""" return getattr(self._original_request, "app", {}) + @property + def shop(self): + """The shop this wrapped request is scoped to. Used by + derivative properties (e.g. shop_cdn_endpoint) that would + otherwise return None — env_request from the watcher loop + doesn't carry a request.shop.""" + return self._shop + + @property + def shop_cdn_endpoint(self): + """Mirror of the regular `request.shop_cdn_endpoint` reified + method (see request_methods.py:add_shop_cdn_endpoint). Used by + send_purchase_email + send_sale_email to build product + thumbnail URLs. Without this, the proxy fell through to None + and the email's rendered as `None//...`.""" + shop = self._shop + # BYOB shop with an explicit CDN endpoint wins. + if shop is not None and getattr(shop, "has_primary_s3", False): + byob = shop.primary_s3_cdn_endpoint + if byob: + return byob + # Otherwise fall back to the MPS-default public CDN. + app = getattr(self._original_request, "app", None) + if app is None: + return None + try: + return app["bucket.secure_uploads.get_endpoint"] + except Exception: + return None + def create_shop_context_request(env_request, crypto_payment: CryptoPayment): """Create a request wrapper with shop domain context for email generation.""" diff --git a/make_post_sell/lib/mail.py b/make_post_sell/lib/mail.py index a561225..22dda81 100644 --- a/make_post_sell/lib/mail.py +++ b/make_post_sell/lib/mail.py @@ -632,10 +632,38 @@ def send_auction_outbid_email(request, to_email, auction): title=auction.product.title, high=f"{auction.current_high:.2f}", auction_url=auction_url, + thumbnail=_product_thumbnail_html(request, auction.product), ) send_pyramid_email(request, to_email, subject, text, html) +def _product_thumbnail_html(request, product): + """Render a tokenized thumbnail tag for inclusion in a + transactional email body, or empty string if the product has no + thumbnail1 extension uploaded. + + Sized for 184px max — same dimensions as the purchase/sale emails + use, so the recipient sees a consistent image card across the whole + email surface. Returns "" (not None) so format calls can splice + without conditional logic. + """ + if product is None or "thumbnail1" not in getattr(product, "extensions", []): + return "" + cdn = getattr(request, "shop_cdn_endpoint", None) + if not cdn: + return "" + return ( + '' + ).format( + cdn=cdn, + path=product.s3_path, + ts=product.updated_timestamp, + ) + + def send_offer_received_email(request, to_email, offer): """Notify a shop owner that a new offer arrived for review.""" offer_url = f"{request.host_url}/o/{offer.uuid_str}" @@ -649,6 +677,7 @@ def send_offer_received_email(request, to_email, offer): amount=f"{offer.current_amount:.2f}", title=offer.product.title, offer_url=offer_url, + thumbnail=_product_thumbnail_html(request, offer.product), ) send_pyramid_email(request, to_email, subject, text, html) @@ -666,6 +695,7 @@ def send_offer_accepted_email(request, to_email, offer): amount=f"{offer.current_amount:.2f}", title=offer.product.title, offer_url=offer_url, + thumbnail=_product_thumbnail_html(request, offer.product), ) send_pyramid_email(request, to_email, subject, text, html) @@ -680,6 +710,7 @@ def send_offer_countered_email(request, to_email, offer): amount=f"{offer.current_amount:.2f}", title=offer.product.title, offer_url=offer_url, + thumbnail=_product_thumbnail_html(request, offer.product), ) send_pyramid_email( request, to_email, subject, @@ -698,6 +729,7 @@ def send_offer_declined_email(request, to_email, offer): amount=f"{offer.current_amount:.2f}", title=offer.product.title, offer_url=offer_url, + thumbnail=_product_thumbnail_html(request, offer.product), ) send_pyramid_email( request, to_email, subject, @@ -716,6 +748,7 @@ def send_offer_withdrawn_email(request, to_email, offer): amount=f"{offer.current_amount:.2f}", title=offer.product.title, offer_url=offer_url, + thumbnail=_product_thumbnail_html(request, offer.product), ) send_pyramid_email( request, to_email, subject, @@ -734,6 +767,7 @@ def send_offer_buyer_cancelled_email(request, to_email, offer): amount=f"{offer.current_amount:.2f}", title=offer.product.title, offer_url=offer_url, + thumbnail=_product_thumbnail_html(request, offer.product), ) send_pyramid_email( request, to_email, subject, diff --git a/make_post_sell/lib/mail_messages.py b/make_post_sell/lib/mail_messages.py index e4cb6c3..a574fd7 100644 --- a/make_post_sell/lib/mail_messages.py +++ b/make_post_sell/lib/mail_messages.py @@ -360,6 +360,7 @@ AUCTION_OUTBID_HTML = """

You have been outbid

Someone outbid you on {title}.

+{thumbnail}

Current high: ${high}.

Place a new bid

@@ -381,6 +382,7 @@ OFFER_RECEIVED_HTML = """

New offer received

You received a new offer of ${amount} for {title}.

+{thumbnail}

View offer

@@ -401,6 +403,7 @@ OFFER_ACCEPTED_HTML = """

Offer accepted

Your offer of ${amount} for {title} was accepted.

+{thumbnail}

Pay now

@@ -421,6 +424,7 @@ OFFER_COUNTERED_HTML = """

Counter offer received

The other party countered with ${amount} for {title}.

+{thumbnail}

Review and respond

@@ -441,6 +445,7 @@ OFFER_DECLINED_HTML = """

Offer declined

Your offer of ${amount} for {title} was declined.

+{thumbnail}

View details

@@ -461,6 +466,7 @@ OFFER_WITHDRAWN_HTML = """

Offer withdrawn

The buyer withdrew their offer of ${amount} for {title}.

+{thumbnail}

View details

@@ -482,6 +488,7 @@ OFFER_BUYER_CANCELLED_HTML = """

Buyer cancelled accepted offer

The buyer cancelled their accepted offer of ${amount} for {title}.

+{thumbnail}

You accepted this offer earlier but the buyer backed out before paying.

View details