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
+78 -6
View File
@@ -2064,6 +2064,13 @@ pub async fn list_audit(
// ---------- Discount codes ----------
fn row_to_discount_code(row: sqlx::sqlite::SqliteRow) -> DiscountCode {
// `featured` lands in migration 0017. Same fallback pattern as
// tier_rank / archived_at — try_get + ok().flatten() so pre-0017
// databases (theoretical) don't crash here.
let featured: bool = row
.try_get::<i64, _>("featured")
.map(|v| v != 0)
.unwrap_or(false);
DiscountCode {
id: row.get("id"),
code: row.get("code"),
@@ -2077,6 +2084,7 @@ fn row_to_discount_code(row: sqlx::sqlite::SqliteRow) -> DiscountCode {
referrer_label: row.get("referrer_label"),
description: row.get("description"),
active: row.get::<i64, _>("active") != 0,
featured,
created_at: row.get("created_at"),
updated_at: row.get("updated_at"),
}
@@ -2122,6 +2130,7 @@ pub async fn create_discount_code(
applies_to_policy_id,
referrer_label,
description,
false, // not featured by default — backwards-compat for callers
)
.await
}
@@ -2148,6 +2157,7 @@ pub async fn create_discount_code_with_currency(
applies_to_policy_id: Option<&str>,
referrer_label: Option<&str>,
description: &str,
featured: bool,
) -> AppResult<DiscountCode> {
if !matches!(
kind,
@@ -2203,8 +2213,8 @@ pub async fn create_discount_code_with_currency(
"INSERT INTO discount_codes
(id, code, kind, amount, discount_currency, max_uses, used_count, expires_at,
applies_to_product_id, applies_to_policy_id, referrer_label,
description, active, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, 1, ?, ?)",
description, active, featured, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, 1, ?, ?, ?)",
)
.bind(&id)
.bind(&normalized)
@@ -2217,6 +2227,7 @@ pub async fn create_discount_code_with_currency(
.bind(applies_to_policy_id)
.bind(referrer_label)
.bind(description)
.bind(featured as i64)
.bind(&now)
.bind(&now)
.execute(pool)
@@ -2239,7 +2250,7 @@ pub async fn get_discount_code_by_id(
let row = sqlx::query(
"SELECT id, code, kind, amount, max_uses, used_count, expires_at,
applies_to_product_id, applies_to_policy_id, referrer_label,
description, active, created_at, updated_at
description, active, featured, created_at, updated_at
FROM discount_codes WHERE id = ?",
)
.bind(id)
@@ -2256,7 +2267,7 @@ pub async fn get_discount_code_by_code(
let row = sqlx::query(
"SELECT id, code, kind, amount, max_uses, used_count, expires_at,
applies_to_product_id, applies_to_policy_id, referrer_label,
description, active, created_at, updated_at
description, active, featured, created_at, updated_at
FROM discount_codes WHERE code = ?",
)
.bind(&normalized)
@@ -2272,18 +2283,72 @@ pub async fn list_discount_codes(
let q = if only_active {
"SELECT id, code, kind, amount, max_uses, used_count, expires_at,
applies_to_product_id, applies_to_policy_id, referrer_label,
description, active, created_at, updated_at
description, active, featured, created_at, updated_at
FROM discount_codes WHERE active = 1 ORDER BY created_at DESC"
} else {
"SELECT id, code, kind, amount, max_uses, used_count, expires_at,
applies_to_product_id, applies_to_policy_id, referrer_label,
description, active, created_at, updated_at
description, active, featured, created_at, updated_at
FROM discount_codes ORDER BY created_at DESC"
};
let rows = sqlx::query(q).fetch_all(pool).await?;
Ok(rows.into_iter().map(row_to_discount_code).collect())
}
/// Find the most-applicable active featured discount for a given
/// (product_id, policy_id) pair, if any. Returns `None` if no featured
/// code applies, or if all matching codes are expired / exhausted /
/// inactive. Precedence: policy-specific > product-specific > global,
/// then by created_at ascending (operator-set priority).
///
/// Used by:
/// - `GET /v1/products/<slug>/policies` to surface launch-special
/// prices on the public buy page + dynamic pricing page.
/// - `POST /v1/purchase` to auto-apply the featured code when the
/// buyer doesn't type one.
pub async fn find_applicable_featured_discount(
pool: &SqlitePool,
product_id: &str,
policy_id: &str,
) -> AppResult<Option<DiscountCode>> {
let now = Utc::now().to_rfc3339();
// The SQL filters by featured + active + not-expired +
// remaining-uses, scopes to either the policy, the product, or
// global, and orders by specificity (policy match first) then
// created_at ascending. LIMIT 1 — we only need the winner.
let row = sqlx::query(
"SELECT id, code, kind, amount, max_uses, used_count, expires_at,
applies_to_product_id, applies_to_policy_id, referrer_label,
description, active, featured, created_at, updated_at
FROM discount_codes
WHERE featured = 1
AND active = 1
AND (expires_at IS NULL OR expires_at > ?)
AND (max_uses IS NULL OR used_count < max_uses)
AND (
applies_to_policy_id = ?
OR (applies_to_policy_id IS NULL AND applies_to_product_id = ?)
OR (applies_to_policy_id IS NULL AND applies_to_product_id IS NULL)
)
ORDER BY
CASE
WHEN applies_to_policy_id = ? THEN 0
WHEN applies_to_product_id = ? THEN 1
ELSE 2
END,
created_at ASC
LIMIT 1",
)
.bind(&now)
.bind(policy_id)
.bind(product_id)
.bind(policy_id)
.bind(product_id)
.fetch_optional(pool)
.await?;
Ok(row.map(row_to_discount_code))
}
pub async fn set_discount_code_active(
pool: &SqlitePool,
id: &str,
@@ -2317,6 +2382,7 @@ pub async fn update_discount_code(
expires_at: Option<Option<&str>>,
description: Option<&str>,
referrer_label: Option<Option<&str>>,
featured: Option<bool>,
) -> AppResult<DiscountCode> {
// Re-fetch to validate amount against the existing kind.
let existing = get_discount_code_by_id(pool, id)
@@ -2377,6 +2443,9 @@ pub async fn update_discount_code(
if referrer_label.is_some() {
sets.push("referrer_label = ?");
}
if featured.is_some() {
sets.push("featured = ?");
}
if sets.is_empty() {
return Ok(existing);
}
@@ -2402,6 +2471,9 @@ pub async fn update_discount_code(
if let Some(opt_r) = referrer_label {
q = q.bind(opt_r);
}
if let Some(f) = featured {
q = q.bind(f as i64);
}
q = q.bind(&now).bind(id);
q.execute(pool).await?;