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:
Grant
2026-05-11 12:47:45 -05:00
parent 2789d1da1f
commit 4334a9f044
9 changed files with 633 additions and 72 deletions
@@ -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?;