Phase 3 backend: tier-gate enforcement + billing webhook
- Tier capability model (pro-layers, geo-analytics, custom-domain, retention) - Pro-only layer types (video/particle/animatedGradient) rejected on card create/replace/patch for Free; geo analytics gated to Pro+ (402 tier_limit) - Gates read the authoritative DB tier, so upgrades apply without re-login - POST /v1/webhooks/revenuecat: shared-secret auth (constant-time), maps RevenueCat events to users.tier (purchase→pro/team/enterprise, cancel→free), unknown user = 2xx no-op; users::update_tier query 93 backend tests; fmt + clippy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
9b7c845319
commit
4ad3220917
@@ -46,6 +46,17 @@ pub async fn find_by_email(db: &Db, email: &str) -> Result<Option<User>, sqlx::E
|
||||
Ok(row.map(UserRow::into_domain))
|
||||
}
|
||||
|
||||
/// Update a user's tier (billing webhook). Returns the number of rows affected
|
||||
/// (0 = no such user).
|
||||
pub async fn update_tier(db: &Db, id: Uuid, tier: &str) -> Result<u64, sqlx::Error> {
|
||||
let result = sqlx::query("UPDATE users SET tier = $2, updated_at = now() WHERE id = $1")
|
||||
.bind(id)
|
||||
.bind(tier)
|
||||
.execute(db)
|
||||
.await?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
|
||||
pub async fn find_by_id(db: &Db, id: Uuid) -> Result<Option<User>, sqlx::Error> {
|
||||
let row: Option<UserRow> = sqlx::query_as(
|
||||
r#"SELECT id, email, handle, display_name, tier, password_hash,
|
||||
|
||||
Reference in New Issue
Block a user