Add UGC Content Creator microservice (10th professional role)
- New service at apps/ugc_content_creators (port 8095) - DB model + repository in crates/db/src/models/ugc_content_creator.rs - Migration: ugc_content_creator_profiles table with platforms, content_niches, content_formats, follower_count, handles, and standard status/timestamps - Contracts: is_professional_profile_approved() handles UGC_CONTENT_CREATOR case - Gateway: routes /api/ugc-content-creators to new service - Workspace Cargo.toml updated with new member Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f9995586e2
commit
89b055b329
10 changed files with 197 additions and 0 deletions
|
|
@ -15,6 +15,7 @@ members = [
|
|||
"apps/social_media_managers",
|
||||
"apps/fitness_trainers",
|
||||
"apps/catering_services",
|
||||
"apps/ugc_content_creators",
|
||||
"crates/contracts",
|
||||
"crates/db",
|
||||
"crates/auth",
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ struct Services {
|
|||
social_media_managers_url: String,
|
||||
fitness_trainers_url: String,
|
||||
catering_services_url: String,
|
||||
ugc_content_creators_url: String,
|
||||
// ── Payments ─────────────────────────────────────────────────────────
|
||||
payments_url: String,
|
||||
// ── Employees (Internal) ─────────────────────────────────────────────
|
||||
|
|
@ -62,6 +63,8 @@ impl Services {
|
|||
.unwrap_or_else(|_| "http://localhost:8092".to_string()),
|
||||
catering_services_url: std::env::var("CATERING_SERVICES_SERVICE_URL")
|
||||
.unwrap_or_else(|_| "http://localhost:8093".to_string()),
|
||||
ugc_content_creators_url: std::env::var("UGC_CONTENT_CREATORS_SERVICE_URL")
|
||||
.unwrap_or_else(|_| "http://localhost:8095".to_string()),
|
||||
payments_url: std::env::var("PAYMENTS_SERVICE_URL")
|
||||
.unwrap_or_else(|_| "http://localhost:8094".to_string()),
|
||||
employees_url: std::env::var("EMPLOYEES_SERVICE_URL")
|
||||
|
|
@ -153,6 +156,9 @@ impl Services {
|
|||
else if path.starts_with("/api/catering-services") {
|
||||
Some(self.catering_services_url.clone())
|
||||
}
|
||||
else if path.starts_with("/api/ugc-content-creators") {
|
||||
Some(self.ugc_content_creators_url.clone())
|
||||
}
|
||||
// ── Payments + Invoices ───────────────────────────────────────────
|
||||
else if path.starts_with("/api/payments")
|
||||
|| path.starts_with("/api/admin/invoices")
|
||||
|
|
|
|||
18
apps/ugc_content_creators/Cargo.toml
Normal file
18
apps/ugc_content_creators/Cargo.toml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "ugc_content_creators"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
axum = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
sqlx = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
db = { path = "../../crates/db" }
|
||||
auth = { path = "../../crates/auth" }
|
||||
contracts = { path = "../../crates/contracts" }
|
||||
cache = { path = "../../crates/cache" }
|
||||
28
apps/ugc_content_creators/src/handlers.rs
Normal file
28
apps/ugc_content_creators/src/handlers.rs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
use axum::{extract::State, http::StatusCode, response::IntoResponse, routing::get, Json, Router};
|
||||
use db::models::ugc_content_creator::{UgcContentCreatorRepository, UpsertUgcContentCreatorProfilePayload};
|
||||
use contracts::{auth_middleware::AuthUser, ProfessionState};
|
||||
|
||||
pub fn router() -> Router<ProfessionState> {
|
||||
Router::new()
|
||||
.route("/profile/me", get(get_profile).patch(update_profile))
|
||||
.merge(contracts::profession_shared::shared_routes("UGC_CONTENT_CREATOR"))
|
||||
}
|
||||
|
||||
async fn get_profile(State(state): State<ProfessionState>, auth: AuthUser) -> impl IntoResponse {
|
||||
match UgcContentCreatorRepository::get_by_user_id(&state.pool, auth.user_id).await {
|
||||
Ok(Some(p)) => (StatusCode::OK, Json(p)).into_response(),
|
||||
Ok(None) => (StatusCode::NOT_FOUND, "Profile not found").into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_profile(
|
||||
State(state): State<ProfessionState>,
|
||||
auth: AuthUser,
|
||||
Json(payload): Json<UpsertUgcContentCreatorProfilePayload>,
|
||||
) -> impl IntoResponse {
|
||||
match UgcContentCreatorRepository::upsert(&state.pool, auth.user_id, payload).await {
|
||||
Ok(p) => (StatusCode::OK, Json(p)).into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
49
apps/ugc_content_creators/src/main.rs
Normal file
49
apps/ugc_content_creators/src/main.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
mod handlers;
|
||||
|
||||
use axum::{routing::get, Router};
|
||||
use std::net::SocketAddr;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
use contracts::ProfessionState;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::EnvFilter::new(
|
||||
std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into()),
|
||||
))
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
|
||||
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
||||
let pool = sqlx::postgres::PgPoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(&database_url)
|
||||
.await
|
||||
.expect("Failed to connect to postgres");
|
||||
|
||||
let redis_url = std::env::var("REDIS_URL")
|
||||
.unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
|
||||
let redis = cache::connect(&redis_url)
|
||||
.await
|
||||
.expect("Failed to connect to Redis");
|
||||
|
||||
tracing::info!("UGC Content Creators service — connected to DB and Redis");
|
||||
|
||||
let state = ProfessionState { pool, redis };
|
||||
|
||||
let app = Router::new()
|
||||
.nest("/api/ugc-content-creators", handlers::router())
|
||||
.route("/health", get(|| async { "UGC Content Creators OK" }))
|
||||
.with_state(state);
|
||||
|
||||
let port: u16 = std::env::var("PORT")
|
||||
.unwrap_or_else(|_| "8095".to_string())
|
||||
.parse()
|
||||
.expect("PORT must be a number");
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||
|
||||
tracing::info!("UGC Content Creators service listening on {}", addr);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}
|
||||
|
|
@ -288,6 +288,12 @@ async fn is_professional_profile_approved(
|
|||
.fetch_optional(pool)
|
||||
.await?
|
||||
}
|
||||
"UGC_CONTENT_CREATOR" => {
|
||||
sqlx::query_scalar::<_, String>("SELECT status FROM ugc_content_creator_profiles WHERE user_id = $1")
|
||||
.bind(user_id)
|
||||
.fetch_optional(pool)
|
||||
.await?
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
DROP TABLE IF EXISTS ugc_content_creator_profiles;
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
-- 10. UGC CONTENT CREATOR PROFILES
|
||||
CREATE TABLE IF NOT EXISTS ugc_content_creator_profiles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE,
|
||||
display_name VARCHAR(255) NOT NULL DEFAULT '',
|
||||
bio TEXT,
|
||||
location VARCHAR(255),
|
||||
-- Profession-specific
|
||||
platforms TEXT[] DEFAULT '{}', -- e.g. ['Instagram', 'YouTube', 'TikTok']
|
||||
content_niches TEXT[] DEFAULT '{}', -- e.g. ['Beauty', 'Tech', 'Food', 'Lifestyle']
|
||||
content_formats TEXT[] DEFAULT '{}', -- e.g. ['Reels', 'Unboxing', 'Reviews', 'GRWM']
|
||||
follower_count INTEGER DEFAULT 0,
|
||||
avg_views_per_post INTEGER DEFAULT 0,
|
||||
has_media_kit BOOLEAN NOT NULL DEFAULT false,
|
||||
instagram_handle VARCHAR(100),
|
||||
youtube_channel_url VARCHAR(500),
|
||||
portfolio_url VARCHAR(500),
|
||||
starting_price_inr INTEGER DEFAULT 0, -- in paise
|
||||
custom_data JSONB,
|
||||
-- Verification & status
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'PENDING', -- PENDING, APPROVED, REJECTED, SUSPENDED
|
||||
rejection_reason TEXT,
|
||||
approved_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ugc_content_creator_profiles_status ON ugc_content_creator_profiles(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_ugc_content_creator_profiles_user_id ON ugc_content_creator_profiles(user_id);
|
||||
|
|
@ -16,6 +16,7 @@ pub mod graphic_designer;
|
|||
pub mod social_media_manager;
|
||||
pub mod fitness_trainer;
|
||||
pub mod catering_service;
|
||||
pub mod ugc_content_creator;
|
||||
pub mod requirement;
|
||||
pub mod lead_request;
|
||||
pub mod application;
|
||||
|
|
|
|||
58
crates/db/src/models/ugc_content_creator.rs
Normal file
58
crates/db/src/models/ugc_content_creator.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, PgPool};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct UgcContentCreatorProfile {
|
||||
pub id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub display_name: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
pub location: Option<String>,
|
||||
pub custom_data: Option<serde_json::Value>,
|
||||
pub status: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UpsertUgcContentCreatorProfilePayload {
|
||||
pub display_name: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
pub location: Option<String>,
|
||||
pub custom_data: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
pub struct UgcContentCreatorRepository;
|
||||
|
||||
impl UgcContentCreatorRepository {
|
||||
pub async fn get_by_user_id(pool: &PgPool, user_id: Uuid) -> Result<Option<UgcContentCreatorProfile>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
UgcContentCreatorProfile,
|
||||
r#"SELECT id, user_id, display_name, bio, location,
|
||||
custom_data,
|
||||
status, created_at, updated_at
|
||||
FROM ugc_content_creator_profiles WHERE user_id = $1"#,
|
||||
user_id
|
||||
).fetch_optional(pool).await
|
||||
}
|
||||
|
||||
pub async fn upsert(pool: &PgPool, user_id: Uuid, p: UpsertUgcContentCreatorProfilePayload) -> Result<UgcContentCreatorProfile, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
UgcContentCreatorProfile,
|
||||
r#"INSERT INTO ugc_content_creator_profiles (user_id, display_name, bio, location, custom_data)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (user_id) DO UPDATE SET
|
||||
display_name = COALESCE(EXCLUDED.display_name, ugc_content_creator_profiles.display_name),
|
||||
bio = EXCLUDED.bio,
|
||||
location = EXCLUDED.location,
|
||||
custom_data = EXCLUDED.custom_data,
|
||||
updated_at = NOW()
|
||||
RETURNING id, user_id, display_name, bio, location,
|
||||
custom_data,
|
||||
status, created_at, updated_at"#,
|
||||
user_id, p.display_name, p.bio, p.location, p.custom_data
|
||||
).fetch_one(pool).await
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue