v0.2.0:16 — Launch-special discount codes + marketing bullets
Major feature release.
Featured (launch-special) discount codes:
- New 'featured' flag on discount_codes (migration 0017). When true,
the buy page renders a diagonal LAUNCH SPECIAL ribbon + slashed
original price + new price for every applicable tier. Purchase
endpoint auto-applies the discount for buyers who don't type a
code. Operator-typed codes still win.
- find_applicable_featured_discount repo helper: most-specific match
(policy > product > global), tiebreak by created_at.
- GET /v1/products/<slug>/policies now returns featured_discount per
policy with the post-discount price computed server-side. SDK
consumers + the dynamic pricing page get this for free.
Marketing bullets on policies:
- metadata.marketing_bullets — operator-controlled copy that renders
as additional checkmarks above the entitlement bullets on both the
admin grid tier card and the buy page tier. For things like 'Up
to 5 products' or 'BTCPay integration' that aren't real
entitlement gates.
- Authored via textarea on draft + edit policy forms.
UI:
- 'Most popular' checkbox now on the draft tier card (was edit-only).
- Discount codes tab grouped by product (matching Licenses /
Subscriptions tabs). Each code row gets a 'featured' badge when
flagged.
All 87 tests still pass. Migration is additive, no SDK changes,
backwards-compatible.
This commit is contained in:
@@ -47,6 +47,12 @@ pub struct CreateDiscountCodeReq {
|
||||
pub referrer_label: Option<String>,
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
/// Mark this as a "launch special" — publicly displayed on the buy
|
||||
/// page with a diagonal LAUNCH SPECIAL ribbon + original price
|
||||
/// struck through. Auto-applies for buyers who don't type any
|
||||
/// code. Operator-typed codes still win when the buyer pastes one.
|
||||
#[serde(default)]
|
||||
pub featured: bool,
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
@@ -117,6 +123,7 @@ pub async fn create(
|
||||
policy_id.as_deref(),
|
||||
req.referrer_label.as_deref(),
|
||||
&req.description,
|
||||
req.featured,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -197,6 +204,10 @@ pub struct UpdateDiscountCodeReq {
|
||||
pub description: Option<String>,
|
||||
#[serde(default, deserialize_with = "deser_double_option", skip_serializing_if = "Option::is_none")]
|
||||
pub referrer_label: Option<Option<String>>,
|
||||
/// Toggle the launch-special public-display flag. `Some(true)` to
|
||||
/// promote, `Some(false)` to demote, omit to leave alone.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub featured: Option<bool>,
|
||||
}
|
||||
|
||||
/// Helper for `Option<Option<T>>` with serde — distinguishes "not present in
|
||||
@@ -227,6 +238,7 @@ pub async fn update(
|
||||
req.expires_at.as_ref().map(|opt| opt.as_deref()),
|
||||
req.description.as_deref(),
|
||||
req.referrer_label.as_ref().map(|opt| opt.as_deref()),
|
||||
req.featured,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user