feat: separate "respond" countdown on PENDING/COUNTERED offers
The offer page already showed a pay-by countdown once the offer was ACCEPTED. The negotiation window (pre-acceptance) had a deadline server-side (offer.expires_timestamp) but no countdown in the UI — buyers and sellers had to guess how long they had to respond. offer_page view now exposes respond_deadline_human (ago.human) and respond_deadline_timestamp_ms alongside the existing pay deadline pair. offer.j2 surfaces it in two places: - "Your turn" panel: "Respond in 5 days, 12 hours, or this offer auto-expires." (the user is the current_party, can act). - "Waiting on the other party" panel: "They have in 5 days, 12 hours to respond, or this offer auto-expires." (the other party owes the next move). Both render through the same [data-pay-deadline] attribute the existing ticker scans — the surrounding copy disambiguates respond-vs-pay. One countdown shape, two semantic uses, depending on state. Regression test test_pending_offer_renders_respond_countdown locks in the markup (PENDING offer, seller view, "Respond" + the regex for the prose ago.human() output).
This commit is contained in:
parent
fedd6abe35
commit
762462a16c
3 changed files with 76 additions and 14 deletions
|
|
@ -62,6 +62,10 @@
|
|||
<section class="offer-actions well">
|
||||
<h3 class="type-title">Your turn</h3>
|
||||
|
||||
{% if respond_deadline_human %}
|
||||
<p class="offer-respond-deadline-note">Respond <strong data-pay-deadline="{{ respond_deadline_timestamp_ms }}">{{ respond_deadline_human }}</strong>, or this offer auto-expires.</p>
|
||||
{% endif %}
|
||||
|
||||
{# Terminal actions (accept / decline / withdraw) carry a confirm
|
||||
prompt. offer.js bails if the prompt was cancelled (it checks
|
||||
event.defaultPrevented), so the AJAX path respects it too. #}
|
||||
|
|
@ -93,7 +97,11 @@
|
|||
</section>
|
||||
{% elif is_open %}
|
||||
<section class="well">
|
||||
<p><em>Waiting on the other party.</em></p>
|
||||
<p><em>Waiting on the other party.</em>
|
||||
{% if respond_deadline_human %}
|
||||
They have <strong data-pay-deadline="{{ respond_deadline_timestamp_ms }}">{{ respond_deadline_human }}</strong> to respond, or this offer auto-expires.
|
||||
{% endif %}
|
||||
</p>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
|
|
|
|||
|
|
@ -6932,6 +6932,51 @@ class TestOfferCheckout(_AuthenticatedBase):
|
|||
# Seller never sees the buyer's pay-now form (they cannot pay).
|
||||
self.assertNotIn(f"/o/{offer_id}/checkout", body)
|
||||
|
||||
def test_pending_offer_renders_respond_countdown(self):
|
||||
"""A PENDING / COUNTERED offer carries a 'respond by' countdown
|
||||
(separate from the post-acceptance pay countdown). Both render
|
||||
via the same [data-pay-deadline] ticker — the surrounding copy
|
||||
tells the user what to do.
|
||||
"""
|
||||
from ..models.offer import (
|
||||
MpsOffer, OFFER_STATE_PENDING, now_timestamp,
|
||||
)
|
||||
from ..models.product import Product
|
||||
from ..models.user import get_or_create_user_by_email
|
||||
|
||||
shop = self._create_shop_helper(user_creds=self.user1_creds)
|
||||
shop.offer_enabled = True
|
||||
product = Product(title="Negotiable", description="...")
|
||||
product.shop = shop
|
||||
product.price_in_cents = 10000
|
||||
product.is_physical = False
|
||||
product.is_sellable = True
|
||||
product.pricing_mode = 3
|
||||
self.dbsession.add(product)
|
||||
self.dbsession.flush()
|
||||
|
||||
buyer = get_or_create_user_by_email(self.dbsession, "test2@example.com")
|
||||
offer = MpsOffer(
|
||||
product=product, shop=shop, buyer=buyer,
|
||||
amount_in_cents=5000,
|
||||
expires_timestamp=now_timestamp() + 3 * 24 * 3600 * 1000,
|
||||
)
|
||||
offer.state = OFFER_STATE_PENDING
|
||||
self.dbsession.add(offer)
|
||||
self.dbsession.flush()
|
||||
offer_id = offer.uuid_str
|
||||
transaction.commit()
|
||||
|
||||
# Seller (user1, already logged in from _create_shop_helper) sees
|
||||
# "Your turn" with a respond-by countdown.
|
||||
body = self.testapp.get(f"/o/{offer_id}", status=200).body.decode()
|
||||
self.assertIn("Your turn", body)
|
||||
self.assertIn("Respond", body)
|
||||
self.assertRegex(
|
||||
body,
|
||||
r'<strong data-pay-deadline="\d+">in \d+ \w+',
|
||||
)
|
||||
|
||||
def test_accepted_offer_renders_pay_countdown_for_buyer(self):
|
||||
"""The buyer's accepted-offer view shows the pay-by deadline as
|
||||
an `ago.human()` delta ("in 23 hours, 59 minutes") in a
|
||||
|
|
|
|||
|
|
@ -239,23 +239,32 @@ def offer_page(request):
|
|||
and actor_party == offer.current_party
|
||||
)
|
||||
|
||||
deadline = offer.acceptance_pay_deadline_ms
|
||||
if deadline is not None:
|
||||
import ago
|
||||
from datetime import datetime, timezone
|
||||
deadline_dt = datetime.fromtimestamp(deadline / 1000, tz=timezone.utc)
|
||||
# Human delta form ("in 23 hours, 13 minutes") instead of a UTC
|
||||
# wall-clock string. JS refines this to second precision on
|
||||
# capable browsers; this is the no-JS fallback.
|
||||
ctx["pay_deadline_human"] = ago.human(
|
||||
deadline_dt, precision=2,
|
||||
import ago
|
||||
from datetime import datetime, timezone
|
||||
|
||||
def _human(ms):
|
||||
if ms is None:
|
||||
return None
|
||||
return ago.human(
|
||||
datetime.fromtimestamp(ms / 1000, tz=timezone.utc),
|
||||
precision=2,
|
||||
past_tense="expired {} ago",
|
||||
future_tense="in {}",
|
||||
)
|
||||
ctx["pay_deadline_timestamp_ms"] = deadline
|
||||
|
||||
# Post-acceptance pay window (ACCEPTED → must pay before this).
|
||||
pay_deadline = offer.acceptance_pay_deadline_ms
|
||||
ctx["pay_deadline_human"] = _human(pay_deadline)
|
||||
ctx["pay_deadline_timestamp_ms"] = pay_deadline
|
||||
|
||||
# Pre-acceptance negotiation window (PENDING/COUNTERED → must
|
||||
# accept/counter/decline before this, else the offer auto-expires).
|
||||
if offer.is_open:
|
||||
ctx["respond_deadline_human"] = _human(offer.expires_timestamp)
|
||||
ctx["respond_deadline_timestamp_ms"] = offer.expires_timestamp
|
||||
else:
|
||||
ctx["pay_deadline_human"] = None
|
||||
ctx["pay_deadline_timestamp_ms"] = None
|
||||
ctx["respond_deadline_human"] = None
|
||||
ctx["respond_deadline_timestamp_ms"] = None
|
||||
return ctx
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue