mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-20 17:02:53 -04:00
better user referral program (#1512)
This commit is contained in:
+58
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n token as `token: UserReferralCodeToken`,\n owner_user_token as `owner_user_token: UserToken`,\n code,\n code_lowercase\nFROM user_referral_codes\nWHERE code_lowercase = ?\n AND deleted_at IS NULL\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token: UserReferralCodeToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "owner_user_token: UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "code",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "code_lowercase",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "56462cb07d49a001095ff118bfdf4fab4c30d1c7e6dca09a11e82f5956508e0b"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO user_referral_codes (\n token,\n code,\n code_lowercase,\n owner_user_token\n)\nVALUES (?, ?, ?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "5e2e68267e858b449de90dbf0d5b20d60b8cb82c4a4ddf56a49200ac92912817"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO user_referrals (\n invited_user_token,\n referrer_user_token,\n maybe_referral_code_token,\n maybe_referral_url,\n maybe_landing_url\n)\nVALUES (?, ?, ?, ?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 5
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "acd6e67603ca687686513b080bbfa642d3e2ffe8c97b0f9dd646f3ef2f8d92df"
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n token as `token: UserReferralCodeToken`,\n code,\n code_lowercase,\n created_at,\n updated_at\nFROM user_referral_codes\nWHERE owner_user_token = ?\n AND deleted_at IS NULL\nORDER BY created_at ASC\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token: UserReferralCodeToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "code",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "code_lowercase",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Datetime",
|
||||
"flags": "NOT_NULL | BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "updated_at",
|
||||
"type_info": {
|
||||
"type": "Datetime",
|
||||
"flags": "NOT_NULL | BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b5ce1dffc1d6e1762844f3a040a9a9cba2a608931824d99eebba67a7d718719e"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE user_referral_codes\nSET\n deleted_at = CURRENT_TIMESTAMP,\n code_lowercase = ?\nWHERE token = ?\n AND owner_user_token = ?\n AND deleted_at IS NULL\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b8254ed110147c66fbeef783283ff4b2565b60c9443572c67091ecd60c9d97d6"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
DROP TABLE IF EXISTS user_referrals;
|
||||
@@ -0,0 +1,31 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
CREATE TABLE user_referrals (
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- The user who was invited/referred.
|
||||
invited_user_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The user who referred the invited user.
|
||||
referrer_user_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- OPTIONAL. The referral code token used, if any.
|
||||
maybe_referral_code_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- OPTIONAL. The referral URL the invited user arrived from.
|
||||
maybe_referral_url VARCHAR(255) DEFAULT NULL,
|
||||
|
||||
-- OPTIONAL. The landing URL the invited user first visited.
|
||||
maybe_landing_url VARCHAR(255) DEFAULT NULL,
|
||||
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- INDICES --
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY unique_invited_user_token (invited_user_token),
|
||||
KEY index_referrer_user_token (referrer_user_token),
|
||||
KEY index_created_at (created_at)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
DROP TABLE IF EXISTS user_referral_codes;
|
||||
@@ -0,0 +1,34 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
CREATE TABLE user_referral_codes (
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- Unique token identifier for this referral code record.
|
||||
token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The human-readable referral code (e.g. a username or custom code).
|
||||
-- This can be changed.
|
||||
code VARCHAR(32) NOT NULL,
|
||||
|
||||
-- Lowercased version of `code` for case-insensitive uniqueness lookups.
|
||||
code_lowercase VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The user who owns this referral code.
|
||||
owner_user_token VARCHAR(32) NOT NULL,
|
||||
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- Soft delete.
|
||||
deleted_at DATETIME NULL,
|
||||
|
||||
-- INDICES --
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY unique_token (token),
|
||||
UNIQUE KEY unique_code_lowercase (code_lowercase),
|
||||
KEY index_owner_user_token (owner_user_token),
|
||||
KEY index_deleted_at (deleted_at)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
@@ -0,0 +1,33 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
CREATE TABLE user_referral_codes (
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- Unique token identifier for this referral code record.
|
||||
token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The human-readable referral code (e.g. a username or custom code).
|
||||
code VARCHAR(32) NOT NULL,
|
||||
|
||||
-- Lowercased version of `code` for case-insensitive uniqueness lookups.
|
||||
code_lowercase VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The user who owns this referral code.
|
||||
owner_user_token VARCHAR(32) NOT NULL,
|
||||
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- Soft delete.
|
||||
deleted_at DATETIME NULL,
|
||||
|
||||
-- INDICES --
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY unique_token (token),
|
||||
UNIQUE KEY unique_code_lowercase (code_lowercase),
|
||||
KEY index_owner_user_token (owner_user_token),
|
||||
KEY index_deleted_at (deleted_at)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
@@ -0,0 +1,31 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
CREATE TABLE user_referrals (
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- The user who was invited/referred.
|
||||
invited_user_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The user who referred the invited user.
|
||||
referrer_user_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- OPTIONAL. The referral code token used, if any.
|
||||
maybe_referral_code_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- OPTIONAL. The referral URL the invited user arrived from.
|
||||
maybe_referral_url VARCHAR(255) DEFAULT NULL,
|
||||
|
||||
-- OPTIONAL. The landing URL the invited user first visited.
|
||||
maybe_landing_url VARCHAR(255) DEFAULT NULL,
|
||||
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- INDICES --
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY unique_invited_user_token (invited_user_token),
|
||||
KEY index_referrer_user_token (referrer_user_token),
|
||||
KEY index_created_at (created_at)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
@@ -10,6 +10,7 @@ pub mod omni_gen;
|
||||
pub mod prompts;
|
||||
pub mod stripe_artcraft;
|
||||
pub mod subscriptions;
|
||||
pub mod user_referral_codes;
|
||||
pub mod users;
|
||||
pub mod utils;
|
||||
pub mod web_referrals;
|
||||
+4
@@ -23,6 +23,10 @@ pub struct StripeArtcraftCreateSubscriptionCheckoutWithUserSignupRequest {
|
||||
|
||||
/// Optional: A referral username or code from a referring user.
|
||||
pub maybe_referral_username: Option<String>,
|
||||
|
||||
/// Optional: A referral code created by another user. If present, takes priority over
|
||||
/// `maybe_referral_username` for resolving the referring user.
|
||||
pub maybe_referral_code: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, Copy, Clone)]
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::user_referral_codes::UserReferralCodeToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct CreateReferralCodeRequest {
|
||||
/// The referral code string. Alphanumeric plus underscore, period, dash.
|
||||
pub code: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct CreateReferralCodeResponse {
|
||||
pub success: bool,
|
||||
pub token: UserReferralCodeToken,
|
||||
pub code: String,
|
||||
pub code_lowercase: String,
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::user_referral_codes::UserReferralCodeToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct DeleteReferralCodePathInfo {
|
||||
pub token: UserReferralCodeToken,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct DeleteReferralCodeResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::user_referral_codes::UserReferralCodeToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ListReferralCodesResponse {
|
||||
pub success: bool,
|
||||
pub referral_codes: Vec<ReferralCodeEntry>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ReferralCodeEntry {
|
||||
pub token: UserReferralCodeToken,
|
||||
pub code: String,
|
||||
pub code_lowercase: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod create_referral_code;
|
||||
pub mod delete_referral_code;
|
||||
pub mod list_referral_codes;
|
||||
@@ -13,6 +13,7 @@ pub enum DatabaseInsertError {
|
||||
|
||||
/// An uncategorized non-database error occurred.
|
||||
/// This will likely result in a 500 for the user.
|
||||
#[deprecated(note = "This is not very semantic. Get rid of it. Stop using it.")]
|
||||
AnyhowError(anyhow::Error),
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ pub mod tts;
|
||||
pub mod twitch;
|
||||
pub mod unsubscribe_reason;
|
||||
pub mod user_impersonation_requests;
|
||||
pub mod user_referral_codes;
|
||||
pub mod user_referrals;
|
||||
pub mod users;
|
||||
pub mod vocoder;
|
||||
pub mod voice_clone_requests;
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
use crate::errors::database_insert_error::DatabaseInsertError;
|
||||
use sqlx::{Executor, MySql};
|
||||
use tokens::tokens::user_referral_codes::UserReferralCodeToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
pub struct CreateReferralCodeArgs<'a> {
|
||||
pub owner_user_token: &'a UserToken,
|
||||
pub code: &'a str,
|
||||
pub code_lowercase: &'a str,
|
||||
}
|
||||
|
||||
pub async fn create_referral_code<'e, 'c: 'e, E>(
|
||||
args: CreateReferralCodeArgs<'_>,
|
||||
mysql_executor: E,
|
||||
) -> Result<UserReferralCodeToken, DatabaseInsertError>
|
||||
where
|
||||
E: 'e + Executor<'c, Database = MySql>,
|
||||
{
|
||||
let token = UserReferralCodeToken::generate();
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO user_referral_codes (
|
||||
token,
|
||||
code,
|
||||
code_lowercase,
|
||||
owner_user_token
|
||||
)
|
||||
VALUES (?, ?, ?, ?)
|
||||
"#,
|
||||
token.as_str(),
|
||||
args.code,
|
||||
args.code_lowercase,
|
||||
args.owner_user_token.as_str(),
|
||||
)
|
||||
.execute(mysql_executor)
|
||||
.await
|
||||
.map_err(DatabaseInsertError::from)?;
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use sqlx::{Executor, MySql};
|
||||
use tokens::tokens::user_referral_codes::UserReferralCodeToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
pub struct ReferralCodeRow {
|
||||
pub token: UserReferralCodeToken,
|
||||
pub code: String,
|
||||
pub code_lowercase: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
struct RawRow {
|
||||
token: UserReferralCodeToken,
|
||||
code: String,
|
||||
code_lowercase: String,
|
||||
created_at: NaiveDateTime,
|
||||
updated_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
pub async fn list_referral_codes_for_user<'e, 'c: 'e, E>(
|
||||
owner_user_token: &UserToken,
|
||||
mysql_executor: E,
|
||||
) -> Result<Vec<ReferralCodeRow>, sqlx::Error>
|
||||
where
|
||||
E: 'e + Executor<'c, Database = MySql>,
|
||||
{
|
||||
let rows = sqlx::query_as!(
|
||||
RawRow,
|
||||
r#"
|
||||
SELECT
|
||||
token as `token: UserReferralCodeToken`,
|
||||
code,
|
||||
code_lowercase,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM user_referral_codes
|
||||
WHERE owner_user_token = ?
|
||||
AND deleted_at IS NULL
|
||||
ORDER BY created_at ASC
|
||||
"#,
|
||||
owner_user_token.as_str(),
|
||||
)
|
||||
.fetch_all(mysql_executor)
|
||||
.await?;
|
||||
|
||||
Ok(rows.into_iter().map(|r| ReferralCodeRow {
|
||||
token: r.token,
|
||||
code: r.code,
|
||||
code_lowercase: r.code_lowercase,
|
||||
created_at: r.created_at.and_utc(),
|
||||
updated_at: r.updated_at.and_utc(),
|
||||
}).collect())
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
use sqlx::{Executor, MySql};
|
||||
use tokens::tokens::user_referral_codes::UserReferralCodeToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
pub struct ReferralCodeLookupResult {
|
||||
pub token: UserReferralCodeToken,
|
||||
pub owner_user_token: UserToken,
|
||||
pub code: String,
|
||||
pub code_lowercase: String,
|
||||
}
|
||||
|
||||
struct RawRow {
|
||||
token: UserReferralCodeToken,
|
||||
owner_user_token: UserToken,
|
||||
code: String,
|
||||
code_lowercase: String,
|
||||
}
|
||||
|
||||
/// Look up a non-deleted referral code by its lowercased code value.
|
||||
pub async fn lookup_referral_code_by_code<'e, 'c: 'e, E>(
|
||||
code_lowercase: &str,
|
||||
mysql_executor: E,
|
||||
) -> Result<Option<ReferralCodeLookupResult>, sqlx::Error>
|
||||
where
|
||||
E: 'e + Executor<'c, Database = MySql>,
|
||||
{
|
||||
let result = sqlx::query_as!(
|
||||
RawRow,
|
||||
r#"
|
||||
SELECT
|
||||
token as `token: UserReferralCodeToken`,
|
||||
owner_user_token as `owner_user_token: UserToken`,
|
||||
code,
|
||||
code_lowercase
|
||||
FROM user_referral_codes
|
||||
WHERE code_lowercase = ?
|
||||
AND deleted_at IS NULL
|
||||
LIMIT 1
|
||||
"#,
|
||||
code_lowercase,
|
||||
)
|
||||
.fetch_one(mysql_executor)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(row) => Ok(Some(ReferralCodeLookupResult {
|
||||
token: row.token,
|
||||
owner_user_token: row.owner_user_token,
|
||||
code: row.code,
|
||||
code_lowercase: row.code_lowercase,
|
||||
})),
|
||||
Err(sqlx::Error::RowNotFound) => Ok(None),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod create_referral_code;
|
||||
pub mod list_referral_codes_for_user;
|
||||
pub mod lookup_referral_code_by_code;
|
||||
pub mod soft_delete_referral_code;
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
use sqlx::{Executor, MySql};
|
||||
use tokens::tokens::user_referral_codes::UserReferralCodeToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
use uuid_utils::uuid::generate_random_uuid;
|
||||
|
||||
/// Soft-delete a referral code owned by the given user.
|
||||
/// Sets `deleted_at` and replaces `code_lowercase` with random entropy
|
||||
/// to free up the unique constraint for reuse.
|
||||
/// Returns true if a row was updated.
|
||||
pub async fn soft_delete_referral_code<'e, 'c: 'e, E>(
|
||||
token: &UserReferralCodeToken,
|
||||
owner_user_token: &UserToken,
|
||||
mysql_executor: E,
|
||||
) -> Result<bool, sqlx::Error>
|
||||
where
|
||||
E: 'e + Executor<'c, Database = MySql>,
|
||||
{
|
||||
// Replace code_lowercase with random entropy (max 32 chars) to free uniqueness constraint.
|
||||
let tombstone = format!("_deleted_{}", &generate_random_uuid()[..20]);
|
||||
|
||||
let result = sqlx::query!(
|
||||
r#"
|
||||
UPDATE user_referral_codes
|
||||
SET
|
||||
deleted_at = CURRENT_TIMESTAMP,
|
||||
code_lowercase = ?
|
||||
WHERE token = ?
|
||||
AND owner_user_token = ?
|
||||
AND deleted_at IS NULL
|
||||
"#,
|
||||
tombstone,
|
||||
token.as_str(),
|
||||
owner_user_token.as_str(),
|
||||
)
|
||||
.execute(mysql_executor)
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
use sqlx::{Executor, MySql};
|
||||
use tokens::tokens::user_referral_codes::UserReferralCodeToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
pub struct InsertUserReferralArgs<'a> {
|
||||
pub invited_user_token: &'a UserToken,
|
||||
pub referrer_user_token: &'a UserToken,
|
||||
pub maybe_referral_code_token: Option<&'a UserReferralCodeToken>,
|
||||
pub maybe_referral_url: Option<&'a str>,
|
||||
pub maybe_landing_url: Option<&'a str>,
|
||||
}
|
||||
|
||||
pub async fn insert_user_referral<'e, 'c: 'e, E>(
|
||||
args: InsertUserReferralArgs<'_>,
|
||||
mysql_executor: E,
|
||||
) -> Result<(), sqlx::Error>
|
||||
where
|
||||
E: 'e + Executor<'c, Database = MySql>,
|
||||
{
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO user_referrals (
|
||||
invited_user_token,
|
||||
referrer_user_token,
|
||||
maybe_referral_code_token,
|
||||
maybe_referral_url,
|
||||
maybe_landing_url
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
"#,
|
||||
args.invited_user_token.as_str(),
|
||||
args.referrer_user_token.as_str(),
|
||||
args.maybe_referral_code_token.map(|t| t.as_str()),
|
||||
args.maybe_referral_url,
|
||||
args.maybe_landing_url,
|
||||
)
|
||||
.execute(mysql_executor)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod insert_user_referral;
|
||||
@@ -47,6 +47,7 @@ pub(crate) enum TokenPrefix {
|
||||
User,
|
||||
UserImpersonationRequest,
|
||||
UserBookmark,
|
||||
UserReferralCode,
|
||||
UserSession,
|
||||
UserSubscription,
|
||||
VoiceConversionModel,
|
||||
@@ -134,6 +135,7 @@ impl PrefixGenerator for TokenPrefix {
|
||||
Self::User => "user_", // NB: Previously "U:"
|
||||
Self::UserImpersonationRequest => "uimpr_",
|
||||
Self::UserBookmark => "ub_",
|
||||
Self::UserReferralCode => "urc_",
|
||||
Self::UserSession => "session_",
|
||||
Self::UserSubscription => "subscription_", // NB: Previously "SUB:"
|
||||
Self::VoiceConversionModel => "vcm_",
|
||||
|
||||
@@ -31,6 +31,7 @@ pub mod twitch_oauth_tokens_grouping;
|
||||
pub mod twitch_oauth_tokens_internal;
|
||||
pub mod user_bookmarks;
|
||||
pub mod user_impersonation_requests;
|
||||
pub mod user_referral_codes;
|
||||
pub mod user_sessions;
|
||||
pub mod user_subscriptions;
|
||||
pub mod users;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::prefixes::TokenPrefix;
|
||||
|
||||
/// The primary key for user referral codes.
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize, Default, ToSchema)]
|
||||
#[cfg_attr(feature = "database", derive(sqlx::Type))]
|
||||
#[cfg_attr(feature = "database", sqlx(transparent))]
|
||||
pub struct UserReferralCodeToken(pub String);
|
||||
|
||||
impl_mysql_token_from_row!(UserReferralCodeToken);
|
||||
impl_string_token!(UserReferralCodeToken);
|
||||
impl_crockford_generator!(UserReferralCodeToken, 18usize, TokenPrefix::UserReferralCode, CrockfordLower);
|
||||
+106
-29
@@ -5,7 +5,11 @@ use crate::endpoints::checkout_with_user_signup::user_creation_case::user_creati
|
||||
use crate::endpoints::checkout_with_user_signup::user_exists_case::user_exists_case;
|
||||
use crate::utils::artcraft_stripe_config::ArtcraftStripeConfigWithClient;
|
||||
use crate::utils::common_web_error::CommonWebError;
|
||||
use mysql_queries::queries::user_referral_codes::lookup_referral_code_by_code::lookup_referral_code_by_code;
|
||||
use mysql_queries::queries::user_referrals::insert_user_referral::{insert_user_referral, InsertUserReferralArgs};
|
||||
use mysql_queries::queries::users::user::get::get_user_token_by_username_with_executor::get_user_token_by_username_with_executor;
|
||||
use tokens::tokens::user_referral_codes::UserReferralCodeToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
use actix_artcraft::sessions::user_sessions::http_user_session_manager::HttpUserSessionManager;
|
||||
use actix_web::web::{Data, Json};
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
@@ -93,29 +97,12 @@ pub async fn stripe_artcraft_create_subscription_checkout_with_user_signup_handl
|
||||
|
||||
let maybe_landing_url = request.maybe_landing_url.clone();
|
||||
|
||||
let maybe_referral_partner = request.maybe_referral_username.as_deref()
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s[..s.len().min(32)].to_string());
|
||||
|
||||
// Look up referring user by username (optional, fail-open).
|
||||
let maybe_referral_user_token = match request.maybe_referral_username.as_deref() {
|
||||
Some(raw) => {
|
||||
let lookup_username = raw.trim().to_lowercase();
|
||||
if lookup_username.is_empty() {
|
||||
None
|
||||
} else {
|
||||
match get_user_token_by_username_with_executor(&lookup_username, &mut *mysql_connection).await {
|
||||
Ok(token) => token,
|
||||
Err(err) => {
|
||||
warn!("Referral user lookup failed (continuing): {:?}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
// Resolve referral info from code (preferred) or username (fallback).
|
||||
let referral_info = resolve_referral_info(
|
||||
request.maybe_referral_code.as_deref(),
|
||||
request.maybe_referral_username.as_deref(),
|
||||
&mut mysql_connection,
|
||||
).await;
|
||||
|
||||
let maybe_user_metadata = internal_user_lookup
|
||||
.lookup_user_from_http_request_and_mysql_connection(&http_request, &mut mysql_connection)
|
||||
@@ -128,16 +115,34 @@ pub async fn stripe_artcraft_create_subscription_checkout_with_user_signup_handl
|
||||
let creation_payload= match maybe_user_metadata {
|
||||
None => {
|
||||
info!("Creating new user, then creating checkout session...");
|
||||
user_creation_case(
|
||||
let payload = user_creation_case(
|
||||
&http_request,
|
||||
&price_id,
|
||||
&mut mysql_connection,
|
||||
&stripe_config,
|
||||
maybe_referral_url,
|
||||
maybe_landing_url,
|
||||
maybe_referral_partner,
|
||||
maybe_referral_user_token,
|
||||
).await?
|
||||
maybe_referral_url.clone(),
|
||||
maybe_landing_url.clone(),
|
||||
referral_info.maybe_referral_partner,
|
||||
referral_info.maybe_referral_user_token.clone(),
|
||||
).await?;
|
||||
|
||||
// Record the referral relationship if we resolved a referrer.
|
||||
if let (Some(new_user), Some(referrer_user_token)) = (&payload.maybe_new_user_metadata, &referral_info.maybe_referral_user_token) {
|
||||
if let Err(err) = insert_user_referral(
|
||||
InsertUserReferralArgs {
|
||||
invited_user_token: &new_user.user_token,
|
||||
referrer_user_token,
|
||||
maybe_referral_code_token: referral_info.maybe_referral_code_token.as_ref(),
|
||||
maybe_referral_url: maybe_referral_url.as_deref(),
|
||||
maybe_landing_url: maybe_landing_url.as_deref(),
|
||||
},
|
||||
&mut *mysql_connection,
|
||||
).await {
|
||||
warn!("Failed to insert user_referral record (continuing): {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
payload
|
||||
},
|
||||
Some(user_metadata) => {
|
||||
info!("Creating checkout session for user: {:?}", user_metadata.user_token_typed);
|
||||
@@ -229,3 +234,75 @@ pub fn create_http_response_new_user(
|
||||
.content_type(CONTENT_TYPE_APPLICATION_JSON)
|
||||
.body(body))
|
||||
}
|
||||
|
||||
struct ResolvedReferralInfo {
|
||||
maybe_referral_partner: Option<String>,
|
||||
maybe_referral_user_token: Option<UserToken>,
|
||||
maybe_referral_code_token: Option<UserReferralCodeToken>,
|
||||
}
|
||||
|
||||
/// Resolve referral info from code (preferred) or username (fallback).
|
||||
async fn resolve_referral_info(
|
||||
maybe_referral_code: Option<&str>,
|
||||
maybe_referral_username: Option<&str>,
|
||||
mysql_connection: &mut sqlx::pool::PoolConnection<sqlx::MySql>,
|
||||
) -> ResolvedReferralInfo {
|
||||
// Try referral code first.
|
||||
if let Some(raw_code) = maybe_referral_code {
|
||||
let trimmed = raw_code.trim();
|
||||
if !trimmed.is_empty() {
|
||||
let code_lowercase = trimmed.to_lowercase();
|
||||
match lookup_referral_code_by_code(&code_lowercase, &mut **mysql_connection).await {
|
||||
Ok(Some(result)) => {
|
||||
let partner = trimmed[..trimmed.len().min(32)].to_string();
|
||||
return ResolvedReferralInfo {
|
||||
maybe_referral_partner: Some(partner),
|
||||
maybe_referral_user_token: Some(result.owner_user_token),
|
||||
maybe_referral_code_token: Some(result.token),
|
||||
};
|
||||
}
|
||||
Ok(None) => {
|
||||
// Code not found — fall through to username lookup.
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Referral code lookup failed (continuing): {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to referral username.
|
||||
if let Some(raw) = maybe_referral_username {
|
||||
let trimmed = raw.trim();
|
||||
let maybe_partner = if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed[..trimmed.len().min(32)].to_string())
|
||||
};
|
||||
|
||||
let lookup_username = trimmed.to_lowercase();
|
||||
let maybe_user_token = if lookup_username.is_empty() {
|
||||
None
|
||||
} else {
|
||||
match get_user_token_by_username_with_executor(&lookup_username, &mut **mysql_connection).await {
|
||||
Ok(token) => token,
|
||||
Err(err) => {
|
||||
warn!("Referral user lookup failed (continuing): {:?}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return ResolvedReferralInfo {
|
||||
maybe_referral_partner: maybe_partner,
|
||||
maybe_referral_user_token: maybe_user_token,
|
||||
maybe_referral_code_token: None,
|
||||
};
|
||||
}
|
||||
|
||||
ResolvedReferralInfo {
|
||||
maybe_referral_partner: None,
|
||||
maybe_referral_user_token: None,
|
||||
maybe_referral_code_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,6 +316,9 @@ use artcraft_api_defs::moderation::wallets::list_user_wallets::*;
|
||||
use artcraft_api_defs::moderation::wallets::moderator_add_banked_balance_to_wallet::*;
|
||||
use artcraft_api_defs::moderation::wallets::moderator_create_wallet_for_user::*;
|
||||
use artcraft_api_defs::moderation::wallets::moderator_get_wallet::*;
|
||||
use artcraft_api_defs::user_referral_codes::create_referral_code::*;
|
||||
use artcraft_api_defs::user_referral_codes::delete_referral_code::*;
|
||||
use artcraft_api_defs::user_referral_codes::list_referral_codes::*;
|
||||
use artcraft_api_defs::web_referrals::log_web_referral::*;
|
||||
use crate::http_server::endpoints::web_referrals::log_web_referral_handler::*;
|
||||
use crate::http_server::endpoints::image_studio::update_gpt_image_job_status_handler::*;
|
||||
@@ -536,6 +539,10 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
crate::http_server::endpoints::subscriptions::get_session_subscription_handler::get_session_subscription_handler,
|
||||
// Web Referrals
|
||||
crate::http_server::endpoints::web_referrals::log_web_referral_handler::log_web_referral_handler,
|
||||
// User Referral Codes
|
||||
crate::http_server::endpoints::user_referral_codes::create_referral_code_handler::create_referral_code_handler,
|
||||
crate::http_server::endpoints::user_referral_codes::list_referral_codes_handler::list_referral_codes_handler,
|
||||
crate::http_server::endpoints::user_referral_codes::delete_referral_code_handler::delete_referral_code_handler,
|
||||
// Image Studio
|
||||
crate::http_server::endpoints::image_studio::update_gpt_image_job_status_handler::update_gpt_image_job_status_handler,
|
||||
),
|
||||
@@ -1210,6 +1217,14 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
// Web Referrals
|
||||
LogWebReferralRequest,
|
||||
LogWebReferralResponse,
|
||||
|
||||
// User Referral Codes
|
||||
CreateReferralCodeRequest,
|
||||
CreateReferralCodeResponse,
|
||||
ListReferralCodesResponse,
|
||||
ReferralCodeEntry,
|
||||
DeleteReferralCodePathInfo,
|
||||
DeleteReferralCodeResponse,
|
||||
))
|
||||
)]
|
||||
pub struct ApiDoc;
|
||||
|
||||
@@ -25,6 +25,7 @@ pub mod trending;
|
||||
pub mod tts;
|
||||
pub mod user_bookmarks;
|
||||
pub mod user_ratings;
|
||||
pub mod user_referral_codes;
|
||||
pub mod user_wallets;
|
||||
pub mod users;
|
||||
pub mod voice_clone_requests;
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::{self, Json};
|
||||
use actix_web::HttpRequest;
|
||||
use log::warn;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use artcraft_api_defs::user_referral_codes::create_referral_code::{CreateReferralCodeRequest, CreateReferralCodeResponse};
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::state::server_state::ServerState;
|
||||
use mysql_queries::errors::database_insert_error::DatabaseInsertError;
|
||||
use mysql_queries::queries::user_referral_codes::create_referral_code::{create_referral_code, CreateReferralCodeArgs};
|
||||
use mysql_queries::queries::user_referral_codes::list_referral_codes_for_user::list_referral_codes_for_user;
|
||||
|
||||
const MAX_ACTIVE_CODES: usize = 5;
|
||||
|
||||
/// Create a new referral code for the logged-in user.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
tag = "User Referral Codes",
|
||||
path = "/v1/user_referral_codes/create",
|
||||
request_body = CreateReferralCodeRequest,
|
||||
responses(
|
||||
(status = 200, description = "Success", body = CreateReferralCodeResponse),
|
||||
(status = 400, description = "Bad input"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 500, description = "Server error"),
|
||||
),
|
||||
)]
|
||||
pub async fn create_referral_code_handler(
|
||||
http_request: HttpRequest,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
request: web::Json<CreateReferralCodeRequest>,
|
||||
) -> Result<Json<CreateReferralCodeResponse>, AdvancedCommonWebError> {
|
||||
let mut mysql_connection = server_state.mysql_pool.acquire().await
|
||||
.map_err(|e| AdvancedCommonWebError::from(e))?;
|
||||
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
AdvancedCommonWebError::from(e)
|
||||
})?;
|
||||
|
||||
let user_session = match maybe_user_session {
|
||||
Some(session) if !session.is_banned => session,
|
||||
_ => return Err(AdvancedCommonWebError::NotAuthorized),
|
||||
};
|
||||
|
||||
let user_token = &user_session.user_token;
|
||||
|
||||
// Validate code: trim, check length, check characters.
|
||||
let code = request.code.trim().to_string();
|
||||
|
||||
if code.is_empty() {
|
||||
return Err(AdvancedCommonWebError::BadInputWithSimpleMessage(
|
||||
"Referral code cannot be empty".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if code.len() > 32 {
|
||||
return Err(AdvancedCommonWebError::BadInputWithSimpleMessage(
|
||||
"Referral code must be 32 characters or fewer".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if !code.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '.' || c == '-') {
|
||||
return Err(AdvancedCommonWebError::BadInputWithSimpleMessage(
|
||||
"Referral code may only contain letters, numbers, underscores, periods, and dashes".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let code_lowercase = code.to_lowercase();
|
||||
|
||||
// Check the user doesn't already have too many active codes.
|
||||
let existing = list_referral_codes_for_user(user_token, &mut *mysql_connection).await
|
||||
.map_err(|e| AdvancedCommonWebError::from(e))?;
|
||||
|
||||
if existing.len() >= MAX_ACTIVE_CODES {
|
||||
return Err(AdvancedCommonWebError::BadInputWithSimpleMessage(
|
||||
format!("You can have at most {} active referral codes. Delete one first.", MAX_ACTIVE_CODES),
|
||||
));
|
||||
}
|
||||
|
||||
// Insert the code. If code_lowercase is already taken, we get a duplicate key error.
|
||||
let result = create_referral_code(
|
||||
CreateReferralCodeArgs {
|
||||
owner_user_token: user_token,
|
||||
code: &code,
|
||||
code_lowercase: &code_lowercase,
|
||||
},
|
||||
&mut *mysql_connection,
|
||||
).await;
|
||||
|
||||
let token = match result {
|
||||
Ok(token) => token,
|
||||
Err(err) => return match err {
|
||||
DatabaseInsertError::DuplicateKeyError => {
|
||||
return Err(AdvancedCommonWebError::BadInputWithSimpleMessage(
|
||||
"This referral code is already in use".to_string(),
|
||||
));
|
||||
},
|
||||
DatabaseInsertError::SqlxError(e) => Err(AdvancedCommonWebError::from(e)),
|
||||
DatabaseInsertError::AnyhowError(e) => Err(AdvancedCommonWebError::from_anyhow_error(e)),
|
||||
},
|
||||
};
|
||||
|
||||
Ok(Json(CreateReferralCodeResponse {
|
||||
success: true,
|
||||
token,
|
||||
code,
|
||||
code_lowercase,
|
||||
}))
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::{self, Json};
|
||||
use actix_web::HttpRequest;
|
||||
use log::warn;
|
||||
|
||||
use artcraft_api_defs::user_referral_codes::delete_referral_code::{DeleteReferralCodePathInfo, DeleteReferralCodeResponse};
|
||||
use tokens::tokens::user_referral_codes::UserReferralCodeToken;
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::state::server_state::ServerState;
|
||||
use mysql_queries::queries::user_referral_codes::soft_delete_referral_code::soft_delete_referral_code;
|
||||
|
||||
/// Delete (soft-delete) a referral code owned by the logged-in user.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
tag = "User Referral Codes",
|
||||
path = "/v1/user_referral_codes/code/{token}",
|
||||
params(
|
||||
("token" = UserReferralCodeToken, description = "The referral code token to delete"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Success", body = DeleteReferralCodeResponse),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 500, description = "Server error"),
|
||||
),
|
||||
)]
|
||||
pub async fn delete_referral_code_handler(
|
||||
http_request: HttpRequest,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
path: web::Path<DeleteReferralCodePathInfo>,
|
||||
) -> Result<Json<DeleteReferralCodeResponse>, AdvancedCommonWebError> {
|
||||
let mut mysql_connection = server_state.mysql_pool.acquire().await
|
||||
.map_err(|e| AdvancedCommonWebError::from(e))?;
|
||||
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
AdvancedCommonWebError::from(e)
|
||||
})?;
|
||||
|
||||
let user_session = match maybe_user_session {
|
||||
Some(session) if !session.is_banned => session,
|
||||
_ => return Err(AdvancedCommonWebError::NotAuthorized),
|
||||
};
|
||||
|
||||
let user_token = &user_session.user_token;
|
||||
|
||||
let deleted = soft_delete_referral_code(
|
||||
&path.token,
|
||||
user_token,
|
||||
&mut *mysql_connection,
|
||||
).await
|
||||
.map_err(|e| AdvancedCommonWebError::from(e))?;
|
||||
|
||||
if !deleted {
|
||||
return Err(AdvancedCommonWebError::NotAuthorized);
|
||||
}
|
||||
|
||||
Ok(Json(DeleteReferralCodeResponse {
|
||||
success: true,
|
||||
}))
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::{self, Json};
|
||||
use actix_web::HttpRequest;
|
||||
use log::warn;
|
||||
|
||||
use artcraft_api_defs::user_referral_codes::list_referral_codes::{ListReferralCodesResponse, ReferralCodeEntry};
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::state::server_state::ServerState;
|
||||
use mysql_queries::queries::user_referral_codes::list_referral_codes_for_user::list_referral_codes_for_user;
|
||||
|
||||
/// List all active referral codes for the logged-in user.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
tag = "User Referral Codes",
|
||||
path = "/v1/user_referral_codes/list",
|
||||
responses(
|
||||
(status = 200, description = "Success", body = ListReferralCodesResponse),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 500, description = "Server error"),
|
||||
),
|
||||
)]
|
||||
pub async fn list_referral_codes_handler(
|
||||
http_request: HttpRequest,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<ListReferralCodesResponse>, AdvancedCommonWebError> {
|
||||
let mut mysql_connection = server_state.mysql_pool.acquire().await
|
||||
.map_err(|e| AdvancedCommonWebError::from(e))?;
|
||||
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
AdvancedCommonWebError::from(e)
|
||||
})?;
|
||||
|
||||
let user_session = match maybe_user_session {
|
||||
Some(session) if !session.is_banned => session,
|
||||
_ => return Err(AdvancedCommonWebError::NotAuthorized),
|
||||
};
|
||||
|
||||
let user_token = &user_session.user_token;
|
||||
|
||||
let rows = list_referral_codes_for_user(user_token, &mut *mysql_connection).await
|
||||
.map_err(|e| AdvancedCommonWebError::from(e))?;
|
||||
|
||||
let referral_codes = rows.into_iter().map(|r| ReferralCodeEntry {
|
||||
token: r.token,
|
||||
code: r.code,
|
||||
code_lowercase: r.code_lowercase,
|
||||
created_at: r.created_at,
|
||||
updated_at: r.updated_at,
|
||||
}).collect();
|
||||
|
||||
Ok(Json(ListReferralCodesResponse {
|
||||
success: true,
|
||||
referral_codes,
|
||||
}))
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod create_referral_code_handler;
|
||||
pub mod delete_referral_code_handler;
|
||||
pub mod list_referral_codes_handler;
|
||||
+32
-24
@@ -7,9 +7,8 @@ use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
use crate::util::cleaners::sanitize_referral_username::sanitize_referral_username;
|
||||
use crate::util::lookup::resolve_referral_info::resolve_referral_info;
|
||||
use crate::http_server::validations::is_reserved_username::is_reserved_username;
|
||||
use mysql_queries::queries::users::user::get::get_user_token_by_username_with_executor::get_user_token_by_username_with_executor;
|
||||
use crate::http_server::validations::validate_passwords::validate_passwords;
|
||||
use crate::http_server::validations::validate_username::validate_username;
|
||||
use crate::util::enroll_in_studio::enroll_in_studio;
|
||||
@@ -24,6 +23,7 @@ use http_server_common::response::serialize_as_json_error::serialize_as_json_err
|
||||
use log::{info, warn};
|
||||
use mysql_queries::mediators::firehose_publisher::FirehosePublisher;
|
||||
use mysql_queries::queries::users::user::create::create_account_error::CreateAccountError;
|
||||
use mysql_queries::queries::user_referrals::insert_user_referral::{insert_user_referral, InsertUserReferralArgs};
|
||||
use mysql_queries::queries::users::user::create::create_account_from_email_and_password::{create_account_from_email_and_password, CreateAccountFromEmailPasswordArgs};
|
||||
use mysql_queries::queries::users::user_sessions::create_user_session_with_executor::create_user_session_with_executor;
|
||||
use password::bcrypt_hash_password::bcrypt_hash_password;
|
||||
@@ -54,6 +54,10 @@ pub struct CreateAccountRequest {
|
||||
|
||||
/// Optional: A referral username or code from a referring user.
|
||||
pub maybe_referral_username: Option<String>,
|
||||
|
||||
/// Optional: A referral code created by another user. If present, takes priority over
|
||||
/// `maybe_referral_username` for resolving the referring user.
|
||||
pub maybe_referral_code: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(ToSchema, Serialize)]
|
||||
@@ -224,24 +228,12 @@ pub async fn create_account_handler(
|
||||
CreateAccountErrorResponse::server_error()
|
||||
})?;
|
||||
|
||||
// Look up referring user by username (optional, fail-open).
|
||||
let maybe_referral_user_token = match request.maybe_referral_username.as_deref() {
|
||||
Some(raw) => {
|
||||
let lookup_username = raw.trim().to_lowercase();
|
||||
if lookup_username.is_empty() {
|
||||
None
|
||||
} else {
|
||||
match get_user_token_by_username_with_executor(&lookup_username, &mut *mysql_connection).await {
|
||||
Ok(token) => token,
|
||||
Err(err) => {
|
||||
warn!("Referral user lookup failed (continuing): {:?}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
// Resolve referral info from code (preferred) or username (fallback).
|
||||
let referral_info = resolve_referral_info(
|
||||
request.maybe_referral_code.as_deref(),
|
||||
request.maybe_referral_username.as_deref(),
|
||||
&mut mysql_connection,
|
||||
).await;
|
||||
|
||||
let create_account_result = create_account_from_email_and_password(
|
||||
CreateAccountFromEmailPasswordArgs {
|
||||
@@ -252,10 +244,10 @@ pub async fn create_account_handler(
|
||||
password_hash: &password_hash,
|
||||
ip_address: &ip_address,
|
||||
maybe_source,
|
||||
maybe_referral_url,
|
||||
maybe_landing_url,
|
||||
maybe_referral_partner: request.maybe_referral_username.as_deref().and_then(sanitize_referral_username),
|
||||
maybe_referral_user_token: maybe_referral_user_token.as_ref(),
|
||||
maybe_referral_url: maybe_referral_url.clone(),
|
||||
maybe_landing_url: maybe_landing_url.clone(),
|
||||
maybe_referral_partner: referral_info.maybe_referral_partner,
|
||||
maybe_referral_user_token: referral_info.maybe_referral_user_token.as_ref(),
|
||||
maybe_user_token: None, // NB: This parameter is for internal testing only
|
||||
},
|
||||
&mut mysql_connection,
|
||||
@@ -291,6 +283,22 @@ pub async fn create_account_handler(
|
||||
|
||||
info!("new user id: {}", new_user_data.user_id);
|
||||
|
||||
// Record the referral relationship if we resolved a referrer.
|
||||
if let Some(referrer_user_token) = &referral_info.maybe_referral_user_token {
|
||||
if let Err(err) = insert_user_referral(
|
||||
InsertUserReferralArgs {
|
||||
invited_user_token: &new_user_data.user_token,
|
||||
referrer_user_token,
|
||||
maybe_referral_code_token: referral_info.maybe_referral_code_token.as_ref(),
|
||||
maybe_referral_url: maybe_referral_url.as_deref(),
|
||||
maybe_landing_url: maybe_landing_url.as_deref(),
|
||||
},
|
||||
&mut *mysql_connection,
|
||||
).await {
|
||||
warn!("Failed to insert user_referral record (continuing): {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
let session_token = create_user_session_with_executor(
|
||||
&new_user_data.user_token,
|
||||
&ip_address,
|
||||
|
||||
+34
-27
@@ -6,7 +6,8 @@
|
||||
use std::fmt;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
use crate::util::cleaners::sanitize_referral_username::sanitize_referral_username;
|
||||
use crate::util::lookup::resolve_referral_info::resolve_referral_info;
|
||||
use mysql_queries::queries::user_referrals::insert_user_referral::{insert_user_referral, InsertUserReferralArgs};
|
||||
use crate::http_server::endpoints::users::google_sso::check_claims::check_claims;
|
||||
use crate::http_server::endpoints::users::google_sso::handle_existing_sso_account::{handle_existing_sso_account, ExistingAccountArgs};
|
||||
use crate::http_server::endpoints::users::google_sso::handle_new_sso_account::{handle_new_sso_account, NewSsoArgs};
|
||||
@@ -20,7 +21,6 @@ use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
|
||||
use log::{info, warn};
|
||||
use mysql_queries::queries::google_sign_in_accounts::get_google_sign_in_account_by_subject::get_google_sign_in_account;
|
||||
use mysql_queries::queries::users::user::get::get_user_token_by_username_with_executor::get_user_token_by_username_with_executor;
|
||||
use mysql_queries::queries::users::user_sessions::create_user_session_with_transactor::create_user_session_with_transactor;
|
||||
use mysql_queries::utils::transactor::Transactor;
|
||||
use sqlx::{Acquire, MySqlPool};
|
||||
@@ -93,6 +93,10 @@ pub struct GoogleCreateAccountRequest {
|
||||
|
||||
/// Optional: A referral username or code from a referring user.
|
||||
pub maybe_referral_username: Option<String>,
|
||||
|
||||
/// Optional: A referral code created by another user. If present, takes priority over
|
||||
/// `maybe_referral_username` for resolving the referring user.
|
||||
pub maybe_referral_code: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(ToSchema, Serialize)]
|
||||
@@ -256,27 +260,12 @@ pub async fn google_sso_handler(
|
||||
|
||||
let maybe_landing_url = request.maybe_landing_url.clone();
|
||||
|
||||
let maybe_referral_partner = request.maybe_referral_username.as_deref()
|
||||
.and_then(sanitize_referral_username);
|
||||
|
||||
// Look up referring user by username (optional, fail-open).
|
||||
let maybe_referral_user_token = match request.maybe_referral_username.as_deref() {
|
||||
Some(raw) => {
|
||||
let lookup_username = raw.trim().to_lowercase();
|
||||
if lookup_username.is_empty() {
|
||||
None
|
||||
} else {
|
||||
match get_user_token_by_username_with_executor(&lookup_username, &mut *mysql_connection).await {
|
||||
Ok(token) => token,
|
||||
Err(err) => {
|
||||
warn!("Referral user lookup failed (continuing): {:?}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
// Resolve referral info from code (preferred) or username (fallback).
|
||||
let referral_info = resolve_referral_info(
|
||||
request.maybe_referral_code.as_deref(),
|
||||
request.maybe_referral_username.as_deref(),
|
||||
&mut mysql_connection,
|
||||
).await;
|
||||
|
||||
let result = handle_new_sso_account(NewSsoArgs {
|
||||
http_request: &http_request,
|
||||
@@ -284,12 +273,30 @@ pub async fn google_sso_handler(
|
||||
claims_subject: &claims_subject,
|
||||
claims_email_address: &claims_email_address,
|
||||
mysql_connection: &mut mysql_connection,
|
||||
maybe_referral_url,
|
||||
maybe_landing_url,
|
||||
maybe_referral_partner,
|
||||
maybe_referral_user_token,
|
||||
maybe_referral_url: maybe_referral_url.clone(),
|
||||
maybe_landing_url: maybe_landing_url.clone(),
|
||||
maybe_referral_partner: referral_info.maybe_referral_partner,
|
||||
maybe_referral_user_token: referral_info.maybe_referral_user_token.clone(),
|
||||
}).await?;
|
||||
|
||||
// Record the referral relationship if this is a genuinely new account.
|
||||
if result.is_new_account {
|
||||
if let Some(referrer_user_token) = &referral_info.maybe_referral_user_token {
|
||||
if let Err(err) = insert_user_referral(
|
||||
InsertUserReferralArgs {
|
||||
invited_user_token: &result.user_token,
|
||||
referrer_user_token,
|
||||
maybe_referral_code_token: referral_info.maybe_referral_code_token.as_ref(),
|
||||
maybe_referral_url: maybe_referral_url.as_deref(),
|
||||
maybe_landing_url: maybe_landing_url.as_deref(),
|
||||
},
|
||||
&mut *mysql_connection,
|
||||
).await {
|
||||
warn!("Failed to insert user_referral record (continuing): {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
user_token = result.user_token;
|
||||
maybe_user_display_name = Some(result.user_display_name);
|
||||
username_not_yet_customized = result.username_is_not_customized;
|
||||
|
||||
+1
@@ -27,6 +27,7 @@ pub struct NewSsoAccountInfo {
|
||||
pub user_token: UserToken,
|
||||
pub user_display_name: String,
|
||||
pub username_is_not_customized: bool,
|
||||
pub is_new_account: bool,
|
||||
}
|
||||
|
||||
pub async fn handle_new_sso_account(
|
||||
|
||||
+1
@@ -58,5 +58,6 @@ pub async fn handle_new_sso_account_for_existing_user(
|
||||
user_token: args.user_account.token,
|
||||
user_display_name: args.user_account.display_name,
|
||||
username_is_not_customized: args.user_account.username_is_not_customized,
|
||||
is_new_account: false,
|
||||
})
|
||||
}
|
||||
|
||||
+1
@@ -129,6 +129,7 @@ pub async fn handle_new_sso_account_for_new_user(
|
||||
user_token,
|
||||
user_display_name,
|
||||
username_is_not_customized: true, // New account with random username
|
||||
is_new_account: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -15,6 +15,7 @@ use crate::http_server::routes::application_routes::subscription_routes::add_sub
|
||||
use crate::http_server::routes::application_routes::tag_routes::add_tag_routes;
|
||||
use crate::http_server::routes::application_routes::user_bookmarks_routes::add_user_bookmarks_routes;
|
||||
use crate::http_server::routes::application_routes::user_rating_routes::add_user_rating_routes;
|
||||
use crate::http_server::routes::application_routes::user_referral_code_routes::add_user_referral_code_routes;
|
||||
use crate::http_server::routes::application_routes::user_routes::add_user_routes;
|
||||
use crate::http_server::routes::application_routes::wallet_routes::add_wallet_routes;
|
||||
use crate::http_server::routes::application_routes::web_referrals_routes::add_web_referrals_routes;
|
||||
@@ -67,6 +68,7 @@ where
|
||||
app = add_comments_routes(app); // /v1/comments/...
|
||||
app = add_user_bookmarks_routes(app); // /v1/user_bookmarks/...
|
||||
app = add_user_rating_routes(app); // /v1/user_rating/...
|
||||
app = add_user_referral_code_routes(app); // /v1/user_referral_codes/...
|
||||
app = add_user_routes(app); // /create_account, /session, /login, /logout, etc.
|
||||
|
||||
// Artcraft Billing pieces
|
||||
|
||||
@@ -15,6 +15,7 @@ mod subscription_routes;
|
||||
mod tag_routes;
|
||||
mod user_bookmarks_routes;
|
||||
mod user_rating_routes;
|
||||
mod user_referral_code_routes;
|
||||
mod user_routes;
|
||||
mod wallet_routes;
|
||||
mod web_referrals_routes;
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
use actix_service::ServiceFactory;
|
||||
use actix_web::body::MessageBody;
|
||||
use actix_web::dev::{ServiceRequest, ServiceResponse};
|
||||
use actix_web::{web, App, Error, HttpResponse};
|
||||
|
||||
use crate::http_server::endpoints::user_referral_codes::create_referral_code_handler::create_referral_code_handler;
|
||||
use crate::http_server::endpoints::user_referral_codes::delete_referral_code_handler::delete_referral_code_handler;
|
||||
use crate::http_server::endpoints::user_referral_codes::list_referral_codes_handler::list_referral_codes_handler;
|
||||
|
||||
pub fn add_user_referral_code_routes<T, B>(app: App<T>) -> App<T>
|
||||
where
|
||||
T: ServiceFactory<ServiceRequest, Config = (), Error = Error, Response = ServiceResponse<B>, InitError = ()>,
|
||||
B: MessageBody,
|
||||
{
|
||||
app
|
||||
.service(web::resource("/v1/user_referral_codes/create")
|
||||
.route(web::post().to(create_referral_code_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(web::resource("/v1/user_referral_codes/list")
|
||||
.route(web::get().to(list_referral_codes_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(web::resource("/v1/user_referral_codes/code/{token}")
|
||||
.route(web::delete().to(delete_referral_code_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod empty_media_file_token_to_null;
|
||||
pub mod sanitize_referral_code;
|
||||
pub mod sanitize_referral_username;
|
||||
@@ -0,0 +1,46 @@
|
||||
const MAX_REFERRAL_PARTNER_LENGTH: usize = 32;
|
||||
|
||||
/// Sanitize a referral code for storage
|
||||
/// in the `users.maybe_referral_partner` column.
|
||||
/// Trims whitespace and truncates to 32 characters.
|
||||
/// Returns None if empty after trimming.
|
||||
pub fn sanitize_referral_code(value: &str) -> Option<String> {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let truncated = &trimmed[..trimmed.len().min(MAX_REFERRAL_PARTNER_LENGTH)];
|
||||
Some(truncated.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn empty_returns_none() {
|
||||
assert_eq!(sanitize_referral_code(""), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whitespace_only_returns_none() {
|
||||
assert_eq!(sanitize_referral_code(" "), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trims_whitespace() {
|
||||
assert_eq!(sanitize_referral_code(" PROMO-2026 "), Some("PROMO-2026".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncates_long_values() {
|
||||
let long = "a".repeat(50);
|
||||
let result = sanitize_referral_code(&long).unwrap();
|
||||
assert_eq!(result.len(), 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn passes_through_normal_values() {
|
||||
assert_eq!(sanitize_referral_code("summer_sale"), Some("summer_sale".to_string()));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod fetch_all_required_media_files;
|
||||
pub mod lookup_image_urls_as_optional_list;
|
||||
pub mod lookup_image_urls_as_map;
|
||||
pub mod lookup_media_file_urls_as_map;
|
||||
pub mod lookup_media_file_urls_as_map;
|
||||
pub mod resolve_referral_info;
|
||||
@@ -0,0 +1,94 @@
|
||||
use log::warn;
|
||||
use mysql_queries::queries::user_referral_codes::lookup_referral_code_by_code::lookup_referral_code_by_code;
|
||||
use mysql_queries::queries::users::user::get::get_user_token_by_username_with_executor::get_user_token_by_username_with_executor;
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::MySql;
|
||||
use tokens::tokens::user_referral_codes::UserReferralCodeToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::util::cleaners::sanitize_referral_code::sanitize_referral_code;
|
||||
use crate::util::cleaners::sanitize_referral_username::sanitize_referral_username;
|
||||
|
||||
/// The resolved referral information to store on the new user record.
|
||||
pub struct ResolvedReferralInfo {
|
||||
/// The raw referral partner string (trimmed, max 32 chars) for `users.maybe_referral_partner`.
|
||||
pub maybe_referral_partner: Option<String>,
|
||||
|
||||
/// The resolved user token of the referrer for `users.maybe_referral_user_token`.
|
||||
pub maybe_referral_user_token: Option<UserToken>,
|
||||
|
||||
/// The referral code token used, if the referral was resolved via a code.
|
||||
pub maybe_referral_code_token: Option<UserReferralCodeToken>,
|
||||
}
|
||||
|
||||
/// Resolve referral info from either a referral code or a referral username.
|
||||
///
|
||||
/// If `maybe_referral_code` is present, try that first:
|
||||
/// - Trim, lowercase, look up in `user_referral_codes` table
|
||||
/// - If found: use the code's `owner_user_token` and the trimmed (pre-lowercase) code as partner
|
||||
///
|
||||
/// Otherwise, fall back to `maybe_referral_username`:
|
||||
/// - Sanitize (trim + truncate to 32 chars) for `maybe_referral_partner`
|
||||
/// - Trim + lowercase + look up user by username for `maybe_referral_user_token`
|
||||
///
|
||||
/// All lookups are fail-open: errors are logged and result in `None`.
|
||||
pub async fn resolve_referral_info(
|
||||
maybe_referral_code: Option<&str>,
|
||||
maybe_referral_username: Option<&str>,
|
||||
mysql_connection: &mut PoolConnection<MySql>,
|
||||
) -> ResolvedReferralInfo {
|
||||
// Try referral code first.
|
||||
if let Some(raw_code) = maybe_referral_code {
|
||||
let trimmed = raw_code.trim();
|
||||
if !trimmed.is_empty() {
|
||||
let code_lowercase = trimmed.to_lowercase();
|
||||
|
||||
match lookup_referral_code_by_code(&code_lowercase, &mut **mysql_connection).await {
|
||||
Ok(Some(result)) => {
|
||||
let partner = sanitize_referral_code(trimmed);
|
||||
return ResolvedReferralInfo {
|
||||
maybe_referral_partner: partner,
|
||||
maybe_referral_user_token: Some(result.owner_user_token),
|
||||
maybe_referral_code_token: Some(result.token),
|
||||
};
|
||||
}
|
||||
Ok(None) => {
|
||||
// Code not found — fall through to username lookup.
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Referral code lookup failed (continuing): {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to referral username.
|
||||
if let Some(raw_username) = maybe_referral_username {
|
||||
let maybe_referral_partner = sanitize_referral_username(raw_username);
|
||||
|
||||
let lookup_username = raw_username.trim().to_lowercase();
|
||||
let maybe_referral_user_token = if lookup_username.is_empty() {
|
||||
None
|
||||
} else {
|
||||
match get_user_token_by_username_with_executor(&lookup_username, &mut **mysql_connection).await {
|
||||
Ok(token) => token,
|
||||
Err(err) => {
|
||||
warn!("Referral user lookup failed (continuing): {:?}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return ResolvedReferralInfo {
|
||||
maybe_referral_partner,
|
||||
maybe_referral_user_token,
|
||||
maybe_referral_code_token: None,
|
||||
};
|
||||
}
|
||||
|
||||
ResolvedReferralInfo {
|
||||
maybe_referral_partner: None,
|
||||
maybe_referral_user_token: None,
|
||||
maybe_referral_code_token: None,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user