4.3 KiB
4.3 KiB
MPS-15: 21-Day Free Trial
Summary
New shops get a 21-day free trial. Trial includes 1 shop, 1 seat, all features. After 21 days the shop enters a grace period, then becomes read-only until a plan is chosen.
Model Changes
Shop model (models/shop.py)
Add columns:
trial_started_timestamp = Column(BigInteger, nullable=True)
trial_ended = Column(Boolean, default=False)
plan_active = Column(Boolean, default=False)
trial_started_timestamp: set to current time (ms) on shop creationtrial_ended: flipped to True when trial expires (background check or request-time check)plan_active: True when a paid plan is active (billing integration, future ticket)
Migration
- Add 3 columns to
mps_shopwith idempotent guards trial_started_timestampnullable (existing shops get NULL = pre-trial era, treated as paid)trial_endedserver_default="0"plan_activeserver_default="0"
Properties
TRIAL_DURATION_MS = 21 * 24 * 60 * 60 * 1000 # 21 days in milliseconds
@property
def trial_expiry_timestamp(self):
if self.trial_started_timestamp is None:
return None
return self.trial_started_timestamp + self.TRIAL_DURATION_MS
@property
def is_trial_active(self):
if self.plan_active:
return False # paid plan supersedes trial
if self.trial_started_timestamp is None:
return False # pre-trial shop (existing shops)
now = int(time.time() * 1000)
return now < self.trial_expiry_timestamp
@property
def is_trial_expired(self):
if self.plan_active or self.trial_started_timestamp is None:
return False
now = int(time.time() * 1000)
return now >= self.trial_expiry_timestamp
@property
def trial_days_remaining(self):
if not self.is_trial_active:
return 0
remaining_ms = self.trial_expiry_timestamp - int(time.time() * 1000)
return max(0, remaining_ms // (24 * 60 * 60 * 1000))
@property
def is_active(self):
"""Shop can operate: either paid plan or active trial."""
return self.plan_active or self.is_trial_active
Trial Enforcement
What trial shops CAN do (all features)
- Create products, upload files, set prices
- Accept payments (all 5 methods)
- Use watch mode, analytics, subscriptions, gift cards
- Create 2 dev/stage shops (per MPS-14)
- Full settings access
What happens when trial expires
- Shop becomes read-only: products visible, downloads work for existing purchases
- New purchases blocked (checkout disabled)
- Product creation/editing disabled
- Settings page shows "Trial expired — choose a plan to continue"
- Flash message on every page: "Your 21-day trial has expired. Choose a plan to keep selling."
Enforcement points
Request-time check (middleware or request method):
def shop_trial_check(request):
shop = request.shop
if shop and shop.is_trial_expired and not shop.plan_active:
# Allow read-only routes, block write routes
...
Write routes to block when trial expired:
- Product create/edit/delete
- Checkout completion (all 3 paths: Stripe, PayPal, Adyen)
- Gift card purchase
- Settings changes (except choosing a plan)
Read routes to allow:
- Product view, shop view, search
- Cart view (but not checkout)
- Settings view (read-only, plan selection enabled)
- Download (for existing purchases)
Trial Banner
In base.j2, show trial status for shop owners:
{% if request.shop and request.shop.is_trial_active and request.user in request.shop.owners %}
<div class="trial-banner">
Free trial: {{ request.shop.trial_days_remaining }} days remaining.
<a href="/s/{{ request.shop.id }}/settings#plan">Choose a plan</a>
</div>
{% endif %}
{% if request.shop and request.shop.is_trial_expired and not request.shop.plan_active %}
<div class="trial-banner trial-expired">
Your 21-day trial has expired.
<a href="/s/{{ request.shop.id }}/settings#plan">Choose a plan to keep selling</a>
</div>
{% endif %}
Shop Creation Changes
In views/shop.py shop_new():
- Set
shop.trial_started_timestamp = int(time.time() * 1000)on creation - Existing shops (NULL timestamp) are grandfathered as paid
Tests
- Unit: trial properties (is_trial_active, is_trial_expired, trial_days_remaining, is_active)
- Integration: trial shop with real DB, verify expiry behavior
- Functional: create shop, verify trial banner, verify trial countdown