feat: pricing packages with multi-select roles, lead requests, mock checkout
This commit is contained in:
parent
c433ab5fed
commit
f7e18cd4d6
20 changed files with 2892 additions and 54 deletions
|
|
@ -405,7 +405,38 @@ async fn view_contact(
|
|||
return (StatusCode::FORBIDDEN, "Access denied").into_response();
|
||||
}
|
||||
|
||||
// Fetch applicant contact info via applicant_user_id → users
|
||||
let free_views = company.free_contact_views;
|
||||
let purchased_views = company.purchased_contact_views;
|
||||
|
||||
if free_views <= 0 && purchased_views <= 0 {
|
||||
return (StatusCode::PAYMENT_REQUIRED, Json(serde_json::json!({
|
||||
"error": "Contact view quota exhausted",
|
||||
"code": "QUOTA_EXHAUSTED",
|
||||
"requires_purchase": true,
|
||||
"message": "You have used all your free contact views. Please purchase a contact view package to continue."
|
||||
}))).into_response();
|
||||
}
|
||||
|
||||
let used_free = free_views > 0;
|
||||
|
||||
if used_free {
|
||||
sqlx::query(
|
||||
"UPDATE company_profiles SET free_contact_views = free_contact_views - 1, updated_at = NOW() WHERE id = $1"
|
||||
)
|
||||
.bind(company.id)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.ok();
|
||||
} else {
|
||||
sqlx::query(
|
||||
"UPDATE company_profiles SET purchased_contact_views = purchased_contact_views - 1, updated_at = NOW() WHERE id = $1"
|
||||
)
|
||||
.bind(company.id)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
let contact = sqlx::query_as::<_, (Option<String>, String, Option<String>)>(
|
||||
r#"
|
||||
SELECT u.full_name, u.email, u.phone
|
||||
|
|
@ -419,14 +450,23 @@ async fn view_contact(
|
|||
|
||||
match contact {
|
||||
Ok(Some((full_name, email, phone))) => {
|
||||
// Fetch updated quota to return to client
|
||||
let updated_company = CompanyRepository::get_by_user_id(&state.pool, auth.user_id)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
let (free_remaining, purchased_remaining) = updated_company
|
||||
.map(|c| (c.free_contact_views, c.purchased_contact_views))
|
||||
.unwrap_or((0, 0));
|
||||
let new_free = if used_free { free_views - 1 } else { free_views };
|
||||
let new_purchased = if used_free { purchased_views } else { purchased_views - 1 };
|
||||
|
||||
let _ = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO notifications (user_id, title, body, notification_type, reference_id)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
"#
|
||||
)
|
||||
.bind(app.applicant_user_id)
|
||||
.bind("Your contact was viewed")
|
||||
.bind(format!("{} viewed your application for {}", company.company_name, job.title))
|
||||
.bind("APPLICATION")
|
||||
.bind(id)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
(StatusCode::OK, Json(serde_json::json!({
|
||||
"application_id": id,
|
||||
|
|
@ -434,12 +474,12 @@ async fn view_contact(
|
|||
"email": email,
|
||||
"phone": phone,
|
||||
"quota": {
|
||||
"free_remaining": free_remaining,
|
||||
"purchased_remaining": purchased_remaining,
|
||||
"total_remaining": free_remaining + purchased_remaining
|
||||
"used_free_view": used_free,
|
||||
"free_remaining": new_free,
|
||||
"purchased_remaining": new_purchased,
|
||||
"total_remaining": new_free + new_purchased
|
||||
}
|
||||
})))
|
||||
.into_response()
|
||||
}))).into_response()
|
||||
}
|
||||
Ok(None) => (StatusCode::NOT_FOUND, "Applicant not found").into_response(),
|
||||
Err(e) => {
|
||||
|
|
|
|||
540
apps/leads/src/lead_requests.rs
Normal file
540
apps/leads/src/lead_requests.rs
Normal file
|
|
@ -0,0 +1,540 @@
|
|||
use crate::AppState;
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PaginationQuery {
|
||||
pub page: Option<i64>,
|
||||
pub limit: Option<i64>,
|
||||
pub status: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SendLeadRequestPayload {
|
||||
pub lead_id: Uuid,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, FromRow)]
|
||||
pub struct LeadRequestRow {
|
||||
pub id: Uuid,
|
||||
pub lead_id: Uuid,
|
||||
pub user_role_profile_id: Uuid,
|
||||
pub customer_user_id: Uuid,
|
||||
pub status: String,
|
||||
pub tracecoins_reserved: i32,
|
||||
pub message: Option<String>,
|
||||
pub expires_at: chrono::DateTime<chrono::Utc>,
|
||||
pub accepted_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub rejected_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub rejected_reason: Option<String>,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct LeadRequestResponse {
|
||||
pub id: Uuid,
|
||||
pub lead_id: Uuid,
|
||||
pub user_role_profile_id: Uuid,
|
||||
pub customer_user_id: Uuid,
|
||||
pub professional_name: Option<String>,
|
||||
pub professional_role: Option<String>,
|
||||
pub customer_name: Option<String>,
|
||||
pub lead_title: Option<String>,
|
||||
pub status: String,
|
||||
pub tracecoins_reserved: i32,
|
||||
pub message: Option<String>,
|
||||
pub expires_at: chrono::DateTime<chrono::Utc>,
|
||||
pub accepted_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub rejected_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub rejected_reason: Option<String>,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
pub fn router() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.route("/", get(list_lead_requests))
|
||||
.route("/send", post(send_lead_request))
|
||||
.route("/{id}/accept", post(accept_lead_request))
|
||||
.route("/{id}/reject", post(reject_lead_request))
|
||||
.route("/my-requests", get(my_requests))
|
||||
.route("/my-pending", get(my_pending_requests))
|
||||
.route("/customer/{lead_id}", get(get_customer_lead_requests))
|
||||
}
|
||||
|
||||
fn lead_request_to_response(row: LeadRequestRow) -> LeadRequestResponse {
|
||||
LeadRequestResponse {
|
||||
id: row.id,
|
||||
lead_id: row.lead_id,
|
||||
user_role_profile_id: row.user_role_profile_id,
|
||||
customer_user_id: row.customer_user_id,
|
||||
professional_name: None,
|
||||
professional_role: None,
|
||||
customer_name: None,
|
||||
lead_title: None,
|
||||
status: row.status,
|
||||
tracecoins_reserved: row.tracecoins_reserved,
|
||||
message: row.message,
|
||||
expires_at: row.expires_at,
|
||||
accepted_at: row.accepted_at,
|
||||
rejected_at: row.rejected_at,
|
||||
rejected_reason: row.rejected_reason,
|
||||
created_at: row.created_at,
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_lead_requests(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(q): Query<PaginationQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let page = q.page.unwrap_or(1);
|
||||
let limit = q.limit.unwrap_or(20);
|
||||
let offset = (page - 1) * limit;
|
||||
|
||||
let status_filter = q.status
|
||||
.as_ref()
|
||||
.map(|s| format!("AND lr.status = '{}'", s))
|
||||
.unwrap_or_default();
|
||||
|
||||
let requests = match sqlx::query_as::<_, LeadRequestRow>(&format!(
|
||||
r#"
|
||||
SELECT lr.* FROM lead_requests lr
|
||||
WHERE 1=1 {}
|
||||
ORDER BY lr.created_at DESC
|
||||
LIMIT {} OFFSET {}
|
||||
"#,
|
||||
status_filter, limit, offset
|
||||
))
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
};
|
||||
|
||||
let requests: Vec<LeadRequestResponse> = requests.into_iter().map(lead_request_to_response).collect();
|
||||
|
||||
(StatusCode::OK, Json(serde_json::json!({
|
||||
"data": requests,
|
||||
"pagination": { "page": page, "limit": limit }
|
||||
}))).into_response()
|
||||
}
|
||||
|
||||
async fn send_lead_request(
|
||||
State(state): State<Arc<AppState>>,
|
||||
axum::extract::ConnectInfo(addr): axum::extract::ConnectInfo<std::net::SocketAddr>,
|
||||
Json(payload): Json<SendLeadRequestPayload>,
|
||||
) -> impl IntoResponse {
|
||||
let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap_or_default();
|
||||
|
||||
let user_role_profile_id = match sqlx::query_scalar::<_, Uuid>(
|
||||
"SELECT id FROM user_role_profiles WHERE user_id = $1 LIMIT 1"
|
||||
)
|
||||
.bind(user_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
{
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => return (StatusCode::NOT_FOUND, "Professional profile not found. Please complete your profile first.").into_response(),
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
};
|
||||
|
||||
let lead = match sqlx::query_as::<_, (Uuid, String, Uuid, String, i32)>(
|
||||
"SELECT id, title, customer_user_id, status, COALESCE(current_acceptances, 0) FROM leads WHERE id = $1"
|
||||
)
|
||||
.bind(payload.lead_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
{
|
||||
Ok(Some(l)) => l,
|
||||
Ok(None) => return (StatusCode::NOT_FOUND, "Lead not found").into_response(),
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
};
|
||||
|
||||
if lead.3 != "OPEN" {
|
||||
return (StatusCode::BAD_REQUEST, "Lead is not open for requests").into_response();
|
||||
}
|
||||
|
||||
if lead.4 >= 10 {
|
||||
return (StatusCode::BAD_REQUEST, "Lead has reached maximum acceptances").into_response();
|
||||
}
|
||||
|
||||
let duplicate = match sqlx::query_scalar::<_, Uuid>(
|
||||
"SELECT id FROM lead_requests WHERE lead_id = $1 AND user_role_profile_id = $2 AND status IN ('PENDING', 'ACCEPTED')"
|
||||
)
|
||||
.bind(payload.lead_id)
|
||||
.bind(user_role_profile_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
{
|
||||
Ok(Some(_)) => true,
|
||||
Ok(None) => false,
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
};
|
||||
|
||||
if duplicate {
|
||||
return (StatusCode::CONFLICT, "You have already sent a request for this lead").into_response();
|
||||
}
|
||||
|
||||
let request_count: (i64,) = match sqlx::query_as(
|
||||
"SELECT COUNT(*) FROM lead_requests WHERE lead_id = $1 AND status IN ('PENDING', 'ACCEPTED')"
|
||||
)
|
||||
.bind(payload.lead_id)
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
};
|
||||
|
||||
if request_count.0 >= 20 {
|
||||
return (StatusCode::CONFLICT, "Lead has reached maximum requests").into_response();
|
||||
}
|
||||
|
||||
let wallet = match sqlx::query_as::<_, (Uuid, i64)>(
|
||||
"SELECT id, balance FROM tracecoin_wallets WHERE user_id = $1"
|
||||
)
|
||||
.bind(user_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
{
|
||||
Ok(Some(w)) => w,
|
||||
Ok(None) => return (StatusCode::BAD_REQUEST, "Wallet not found. Please contact support.").into_response(),
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
};
|
||||
|
||||
let tracecoins_cost = 25;
|
||||
if wallet.1 < tracecoins_cost as i64 {
|
||||
return (StatusCode::PAYMENT_REQUIRED, format!("Insufficient balance. You need at least {} Tracecoins.", tracecoins_cost)).into_response();
|
||||
}
|
||||
|
||||
let expires_at = chrono::Utc::now() + chrono::Duration::hours(24);
|
||||
|
||||
let result = sqlx::query_as::<_, LeadRequestRow>(
|
||||
r#"
|
||||
INSERT INTO lead_requests (lead_id, user_role_profile_id, customer_user_id, status, tracecoins_reserved, message, expires_at)
|
||||
VALUES ($1, $2, $3, 'PENDING', $4, $5, $6)
|
||||
RETURNING *
|
||||
"#
|
||||
)
|
||||
.bind(payload.lead_id)
|
||||
.bind(user_role_profile_id)
|
||||
.bind(lead.2)
|
||||
.bind(tracecoins_cost)
|
||||
.bind(&payload.message)
|
||||
.bind(expires_at)
|
||||
.fetch_one(&state.pool)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(req) => {
|
||||
let _ = sqlx::query(
|
||||
r#"
|
||||
UPDATE tracecoin_wallets SET
|
||||
balance = balance - $1,
|
||||
reserved = COALESCE(reserved, 0) + $1,
|
||||
updated_at = NOW()
|
||||
WHERE user_id = $2
|
||||
"#
|
||||
)
|
||||
.bind(tracecoins_cost as i64)
|
||||
.bind(user_id)
|
||||
.execute(&state.pool)
|
||||
.await;
|
||||
|
||||
let _ = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO notifications (user_id, title, body, notification_type, reference_id)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
"#
|
||||
)
|
||||
.bind(lead.2)
|
||||
.bind("New Lead Request")
|
||||
.bind("You have a new lead request. Please review and respond within 24 hours.")
|
||||
.bind("LEAD_REQUEST")
|
||||
.bind(req.id)
|
||||
.execute(&state.pool)
|
||||
.await;
|
||||
|
||||
let response = lead_request_to_response(req);
|
||||
(StatusCode::CREATED, Json(response)).into_response()
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn accept_lead_request(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<Uuid>,
|
||||
axum::extract::ConnectInfo(addr): axum::extract::ConnectInfo<std::net::SocketAddr>,
|
||||
) -> impl IntoResponse {
|
||||
let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap_or_default();
|
||||
|
||||
let request = match sqlx::query_as::<_, LeadRequestRow>(
|
||||
"SELECT * FROM lead_requests WHERE id = $1 AND status = 'PENDING'"
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
{
|
||||
Ok(Some(r)) => r,
|
||||
Ok(None) => return (StatusCode::NOT_FOUND, "Lead request not found or already processed").into_response(),
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
};
|
||||
|
||||
if request.customer_user_id != user_id {
|
||||
return (StatusCode::FORBIDDEN, "You are not authorized to accept this request").into_response();
|
||||
}
|
||||
|
||||
if request.expires_at < chrono::Utc::now() {
|
||||
return (StatusCode::BAD_REQUEST, "This request has expired").into_response();
|
||||
}
|
||||
|
||||
let lead_acceptances: (i32,) = match sqlx::query_as(
|
||||
"SELECT COALESCE(current_acceptances, 0) FROM leads WHERE id = $1"
|
||||
)
|
||||
.bind(request.lead_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
{
|
||||
Ok(Some(l)) => l,
|
||||
Ok(None) => return (StatusCode::NOT_FOUND, "Lead not found").into_response(),
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
};
|
||||
|
||||
if lead_acceptances.0 >= 10 {
|
||||
return (StatusCode::BAD_REQUEST, "Lead has reached maximum acceptances").into_response();
|
||||
}
|
||||
|
||||
let _ = sqlx::query(
|
||||
"UPDATE lead_requests SET status = 'ACCEPTED', accepted_at = NOW(), updated_at = NOW() WHERE id = $1"
|
||||
)
|
||||
.bind(id)
|
||||
.execute(&state.pool)
|
||||
.await;
|
||||
|
||||
let _ = sqlx::query(
|
||||
"UPDATE leads SET current_acceptances = current_acceptances + 1, updated_at = NOW() WHERE id = $1"
|
||||
)
|
||||
.bind(request.lead_id)
|
||||
.execute(&state.pool)
|
||||
.await;
|
||||
|
||||
let _ = sqlx::query(
|
||||
r#"
|
||||
UPDATE tracecoin_wallets SET
|
||||
reserved = reserved - $1,
|
||||
updated_at = NOW()
|
||||
WHERE user_id = $2
|
||||
"#
|
||||
)
|
||||
.bind(request.tracecoins_reserved as i64)
|
||||
.bind(user_id)
|
||||
.execute(&state.pool)
|
||||
.await;
|
||||
|
||||
if lead_acceptances.0 + 1 >= 10 {
|
||||
let _ = sqlx::query("UPDATE leads SET status = 'CLOSED', updated_at = NOW() WHERE id = $1")
|
||||
.bind(request.lead_id)
|
||||
.execute(&state.pool)
|
||||
.await;
|
||||
}
|
||||
|
||||
let _ = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO notifications (user_id, title, body, notification_type, reference_id)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
"#
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind("Lead Request Accepted")
|
||||
.bind("Your lead request has been accepted! Contact details have been shared.")
|
||||
.bind("LEAD_REQUEST")
|
||||
.bind(id)
|
||||
.execute(&state.pool)
|
||||
.await;
|
||||
|
||||
(StatusCode::OK, Json(serde_json::json!({
|
||||
"message": "Lead request accepted successfully",
|
||||
"contact_details_shared": true
|
||||
}))).into_response()
|
||||
}
|
||||
|
||||
async fn reject_lead_request(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<Uuid>,
|
||||
axum::extract::ConnectInfo(addr): axum::extract::ConnectInfo<std::net::SocketAddr>,
|
||||
) -> impl IntoResponse {
|
||||
let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap_or_default();
|
||||
|
||||
let request = match sqlx::query_as::<_, LeadRequestRow>(
|
||||
"SELECT * FROM lead_requests WHERE id = $1 AND status = 'PENDING'"
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
{
|
||||
Ok(Some(r)) => r,
|
||||
Ok(None) => return (StatusCode::NOT_FOUND, "Lead request not found or already processed").into_response(),
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
};
|
||||
|
||||
if request.customer_user_id != user_id {
|
||||
return (StatusCode::FORBIDDEN, "You are not authorized to reject this request").into_response();
|
||||
}
|
||||
|
||||
let _ = sqlx::query(
|
||||
"UPDATE lead_requests SET status = 'REJECTED', rejected_at = NOW(), rejected_reason = 'Rejected by customer', updated_at = NOW() WHERE id = $1"
|
||||
)
|
||||
.bind(id)
|
||||
.execute(&state.pool)
|
||||
.await;
|
||||
|
||||
let _ = sqlx::query(
|
||||
r#"
|
||||
UPDATE tracecoin_wallets SET
|
||||
balance = balance + $1,
|
||||
reserved = reserved - $1,
|
||||
updated_at = NOW()
|
||||
WHERE user_id = $2
|
||||
"#
|
||||
)
|
||||
.bind(request.tracecoins_reserved as i64)
|
||||
.bind(user_id)
|
||||
.execute(&state.pool)
|
||||
.await;
|
||||
|
||||
let _ = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO notifications (user_id, title, body, notification_type, reference_id)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
"#
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind("Lead Request Rejected")
|
||||
.bind("Your lead request was not accepted. Tracecoins have been refunded.")
|
||||
.bind("LEAD_REQUEST")
|
||||
.bind(id)
|
||||
.execute(&state.pool)
|
||||
.await;
|
||||
|
||||
(StatusCode::OK, Json(serde_json::json!({
|
||||
"message": "Lead request rejected. Tracecoins refunded.",
|
||||
"refunded": request.tracecoins_reserved
|
||||
}))).into_response()
|
||||
}
|
||||
|
||||
async fn my_requests(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(q): Query<PaginationQuery>,
|
||||
axum::extract::ConnectInfo(addr): axum::extract::ConnectInfo<std::net::SocketAddr>,
|
||||
) -> impl IntoResponse {
|
||||
let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap_or_default();
|
||||
let page = q.page.unwrap_or(1);
|
||||
let limit = q.limit.unwrap_or(20);
|
||||
let offset = (page - 1) * limit;
|
||||
|
||||
let status_filter = q.status
|
||||
.as_ref()
|
||||
.map(|s| format!("AND lr.status = '{}'", s))
|
||||
.unwrap_or_default();
|
||||
|
||||
let requests = match sqlx::query_as::<_, LeadRequestRow>(&format!(
|
||||
r#"
|
||||
SELECT lr.* FROM lead_requests lr
|
||||
JOIN user_role_profiles urp ON urp.id = lr.user_role_profile_id
|
||||
WHERE urp.user_id = $1 {}
|
||||
ORDER BY lr.created_at DESC
|
||||
LIMIT {} OFFSET {}
|
||||
"#,
|
||||
status_filter, limit, offset
|
||||
))
|
||||
.bind(user_id)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
};
|
||||
|
||||
let requests: Vec<LeadRequestResponse> = requests.into_iter().map(lead_request_to_response).collect();
|
||||
|
||||
(StatusCode::OK, Json(serde_json::json!({
|
||||
"data": requests,
|
||||
"pagination": { "page": page, "limit": limit }
|
||||
}))).into_response()
|
||||
}
|
||||
|
||||
async fn my_pending_requests(
|
||||
State(state): State<Arc<AppState>>,
|
||||
axum::extract::ConnectInfo(addr): axum::extract::ConnectInfo<std::net::SocketAddr>,
|
||||
) -> impl IntoResponse {
|
||||
let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap_or_default();
|
||||
|
||||
let requests = match sqlx::query_as::<_, LeadRequestRow>(
|
||||
r#"
|
||||
SELECT lr.* FROM lead_requests lr
|
||||
WHERE lr.customer_user_id = $1 AND lr.status = 'PENDING'
|
||||
ORDER BY lr.expires_at ASC
|
||||
"#
|
||||
)
|
||||
.bind(user_id)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
};
|
||||
|
||||
let requests: Vec<LeadRequestResponse> = requests.into_iter().map(lead_request_to_response).collect();
|
||||
|
||||
(StatusCode::OK, Json(serde_json::json!({
|
||||
"data": requests
|
||||
}))).into_response()
|
||||
}
|
||||
|
||||
async fn get_customer_lead_requests(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(lead_id): Path<Uuid>,
|
||||
Query(q): Query<PaginationQuery>,
|
||||
axum::extract::ConnectInfo(addr): axum::extract::ConnectInfo<std::net::SocketAddr>,
|
||||
) -> impl IntoResponse {
|
||||
let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap_or_default();
|
||||
let page = q.page.unwrap_or(1);
|
||||
let limit = q.limit.unwrap_or(20);
|
||||
let offset = (page - 1) * limit;
|
||||
|
||||
let requests = match sqlx::query_as::<_, LeadRequestRow>(&format!(
|
||||
r#"
|
||||
SELECT lr.* FROM lead_requests lr
|
||||
WHERE lr.lead_id = $1 AND lr.customer_user_id = $2
|
||||
ORDER BY lr.created_at DESC
|
||||
LIMIT {} OFFSET {}
|
||||
"#,
|
||||
limit, offset
|
||||
))
|
||||
.bind(lead_id)
|
||||
.bind(user_id)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
};
|
||||
|
||||
let requests: Vec<LeadRequestResponse> = requests.into_iter().map(lead_request_to_response).collect();
|
||||
|
||||
(StatusCode::OK, Json(serde_json::json!({
|
||||
"data": requests,
|
||||
"pagination": { "page": page, "limit": limit }
|
||||
}))).into_response()
|
||||
}
|
||||
|
|
@ -11,6 +11,8 @@ use std::sync::Arc;
|
|||
use tower_http::cors::{Any, CorsLayer};
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
pub mod lead_requests;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub pool: PgPool,
|
||||
|
|
@ -55,7 +57,7 @@ async fn create_lead(
|
|||
INSERT INTO leads (title, description, location, profession_key)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id, title, description, location, profession_key, status, created_at
|
||||
"#,
|
||||
"#
|
||||
)
|
||||
.bind(&payload.title)
|
||||
.bind(&payload.description)
|
||||
|
|
@ -120,6 +122,7 @@ async fn main() {
|
|||
.route("/leads", get(list_leads))
|
||||
.route("/leads", post(create_lead))
|
||||
.route("/leads/:id", get(get_lead))
|
||||
.nest("/api/lead-requests", lead_requests::router())
|
||||
.layer(cors)
|
||||
.with_state(state);
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ use uuid::Uuid;
|
|||
use sqlx::postgres::PgPool;
|
||||
use sqlx::FromRow;
|
||||
|
||||
pub mod packages;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
beeceptor_url: String,
|
||||
|
|
@ -67,7 +69,8 @@ struct PricingPackageRow {
|
|||
struct PaymentRow {
|
||||
id: Uuid,
|
||||
user_id: Uuid,
|
||||
tracecoins_credited: i32,
|
||||
package_id: Option<Uuid>,
|
||||
tracecoins_credited: Option<i32>,
|
||||
}
|
||||
|
||||
async fn create_order(
|
||||
|
|
@ -77,11 +80,9 @@ async fn create_order(
|
|||
) -> Result<Json<CreateOrderResponse>, (StatusCode, String)> {
|
||||
tracing::info!("Creating payment order: amount={}", payload.amount);
|
||||
|
||||
// Validate package_id
|
||||
let package_id_str = payload.package_id.as_ref().ok_or((StatusCode::BAD_REQUEST, "package_id is required".to_string()))?;
|
||||
let package_id = Uuid::parse_str(package_id_str).map_err(|_| (StatusCode::BAD_REQUEST, "Invalid package id".to_string()))?;
|
||||
|
||||
// Fetch package to get tracecoins amount
|
||||
let package = sqlx::query_as::<_, PricingPackageRow>(
|
||||
"SELECT tracecoins_amount FROM pricing_packages WHERE id = $1 AND is_active = true",
|
||||
)
|
||||
|
|
@ -93,7 +94,6 @@ async fn create_order(
|
|||
let package = package.ok_or((StatusCode::BAD_REQUEST, "Invalid or inactive package".to_string()))?;
|
||||
let tracecoins_credited = package.tracecoins_amount;
|
||||
|
||||
// Call Beeceptor to create order
|
||||
let resp = state
|
||||
.client
|
||||
.post(&state.beeceptor_url)
|
||||
|
|
@ -130,9 +130,11 @@ async fn create_order(
|
|||
.unwrap_or("mock_order_123")
|
||||
.to_string();
|
||||
|
||||
// Insert payment record
|
||||
sqlx::query(
|
||||
"INSERT INTO payments (user_id, package_id, razorpay_order_id, amount_inr, tracecoins_credited, status) VALUES ($1, $2, $3, $4, $5, 'PENDING')",
|
||||
r#"
|
||||
INSERT INTO payments (user_id, package_id, razorpay_order_id, amount, tracecoins_credited, status)
|
||||
VALUES ($1, $2, $3, $4, $5, 'PENDING')
|
||||
"#,
|
||||
)
|
||||
.bind(auth.user_id)
|
||||
.bind(package_id)
|
||||
|
|
@ -158,7 +160,6 @@ async fn verify_payment(
|
|||
) -> Result<Json<VerifyPaymentResponse>, (StatusCode, String)> {
|
||||
tracing::info!("Verifying payment: order_id={}", payload.order_id);
|
||||
|
||||
// Verify with Beeceptor
|
||||
let verify_url = format!("{}/verify", state.beeceptor_url.trim_end_matches('/'));
|
||||
let resp = state
|
||||
.client
|
||||
|
|
@ -185,9 +186,12 @@ async fn verify_payment(
|
|||
));
|
||||
}
|
||||
|
||||
// Find pending payment by razorpay_order_id
|
||||
let payment = sqlx::query_as::<_, PaymentRow>(
|
||||
"SELECT id, user_id, tracecoins_credited FROM payments WHERE razorpay_order_id = $1 AND status = 'PENDING'",
|
||||
r#"
|
||||
SELECT id, user_id, package_id, tracecoins_credited
|
||||
FROM payments
|
||||
WHERE razorpay_order_id = $1 AND status = 'PENDING'
|
||||
"#,
|
||||
)
|
||||
.bind(&payload.order_id)
|
||||
.fetch_optional(&state.pool)
|
||||
|
|
@ -199,14 +203,20 @@ async fn verify_payment(
|
|||
None => return Err((StatusCode::NOT_FOUND, "Payment not found or already processed".to_string())),
|
||||
};
|
||||
|
||||
// Ensure the authenticated user matches the payment user
|
||||
if payment.user_id != auth.user_id {
|
||||
return Err((StatusCode::FORBIDDEN, "Payment does not belong to user".to_string()));
|
||||
}
|
||||
|
||||
// Update payment status to SUCCESS
|
||||
let tracecoins = payment.tracecoins_credited.unwrap_or(0);
|
||||
|
||||
sqlx::query(
|
||||
"UPDATE payments SET status = 'SUCCESS', verified_at = NOW(), razorpay_payment_id = $1 WHERE id = $2",
|
||||
r#"
|
||||
UPDATE payments SET
|
||||
status = 'SUCCESS',
|
||||
razorpay_payment_id = $1,
|
||||
verified_at = NOW()
|
||||
WHERE id = $2
|
||||
"#,
|
||||
)
|
||||
.bind(&payload.payment_id)
|
||||
.bind(payment.id)
|
||||
|
|
@ -214,49 +224,50 @@ async fn verify_payment(
|
|||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
|
||||
// Credit wallet (increase balance)
|
||||
sqlx::query(
|
||||
"INSERT INTO tracecoin_wallets (user_id, balance, reserved) VALUES ($1, $2, 0) ON CONFLICT (user_id) DO UPDATE SET balance = tracecoin_wallets.balance + excluded.balance",
|
||||
r#"
|
||||
INSERT INTO tracecoin_wallets (user_id, balance, reserved)
|
||||
VALUES ($1, $2, 0)
|
||||
ON CONFLICT (user_id) DO UPDATE SET
|
||||
balance = tracecoin_wallets.balance + excluded.balance
|
||||
"#,
|
||||
)
|
||||
.bind(payment.user_id)
|
||||
.bind(payment.tracecoins_credited)
|
||||
.bind(tracecoins as i64)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
|
||||
// Get wallet id for ledger
|
||||
match sqlx::query_scalar::<_, Uuid>(
|
||||
"SELECT id FROM tracecoin_wallets WHERE user_id = $1",
|
||||
if let Ok(Some(wallet_id)) = sqlx::query_scalar::<_, Uuid>(
|
||||
"SELECT id FROM tracecoin_wallets WHERE user_id = $1"
|
||||
)
|
||||
.bind(payment.user_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
{
|
||||
Ok(Some(wallet_id)) => {
|
||||
sqlx::query(
|
||||
"INSERT INTO tracecoin_ledger (wallet_id, type, amount, reason, reference_id) VALUES ($1, 'CREDIT', $2, $3, $4)",
|
||||
)
|
||||
.bind(wallet_id)
|
||||
.bind(payment.tracecoins_credited as i64)
|
||||
.bind("PURCHASE")
|
||||
.bind(payment.id)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO tracecoin_ledger (wallet_id, transaction_type, amount, balance_after, reference_type, reference_id, description)
|
||||
VALUES ($1, 'CREDIT', $2, $2, 'PAYMENT', $3, 'Package purchase')
|
||||
"#,
|
||||
)
|
||||
.bind(wallet_id)
|
||||
.bind(tracecoins as i64)
|
||||
.bind(payment.id)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
// Send notification to user about successful purchase
|
||||
let _ = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO notifications (user_id, title, body, type, reference_id)
|
||||
INSERT INTO notifications (user_id, title, body, notification_type, reference_id)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
"#,
|
||||
)
|
||||
.bind(payment.user_id)
|
||||
.bind("Tracecoins Purchased Successfully")
|
||||
.bind(format!("Your {} Tracecoin package has been credited to your wallet.", payment.tracecoins_credited))
|
||||
.bind(format!("Your {} Tracecoin package has been credited to your wallet.", tracecoins))
|
||||
.bind("PAYMENT")
|
||||
.bind(payment.id)
|
||||
.execute(&state.pool)
|
||||
|
|
@ -348,6 +359,7 @@ async fn main() {
|
|||
.route("/api/payments/create-order", post(create_order))
|
||||
.route("/api/payments/verify", post(verify_payment))
|
||||
.route("/api/payments/{id}/status", get(get_payment_status))
|
||||
.nest("/api/packages", packages::router())
|
||||
.with_state(state);
|
||||
|
||||
let port: u16 = std::env::var("PORT")
|
||||
|
|
@ -356,7 +368,7 @@ async fn main() {
|
|||
.expect("PORT must be a valid u16");
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||
|
||||
tracing::info!("Payments service (mock via Beeceptor) listening on {}", addr);
|
||||
tracing::info!("Payments service listening on {}", addr);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
|
|
|
|||
418
apps/payments/src/packages.rs
Normal file
418
apps/payments/src/packages.rs
Normal file
|
|
@ -0,0 +1,418 @@
|
|||
use crate::AppState;
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
routing::{delete, get, patch, post},
|
||||
Json, Router,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PackageTypeQuery {
|
||||
pub package_type: Option<String>,
|
||||
pub applicable_role: Option<String>,
|
||||
pub active_only: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PaginationQuery {
|
||||
pub page: Option<i64>,
|
||||
pub limit: Option<i64>,
|
||||
pub search: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreatePackageRequest {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub package_type: String,
|
||||
pub applicable_roles: Vec<String>,
|
||||
pub tracecoins_amount: i32,
|
||||
pub price: i32,
|
||||
pub duration_days: Option<i32>,
|
||||
pub valid_from: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub valid_until: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub is_promotional: Option<bool>,
|
||||
pub is_active: Option<bool>,
|
||||
pub features: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpdatePackageRequest {
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub tracecoins_amount: Option<i32>,
|
||||
pub price: Option<i32>,
|
||||
pub duration_days: Option<i32>,
|
||||
pub valid_from: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub valid_until: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub is_promotional: Option<bool>,
|
||||
pub is_active: Option<bool>,
|
||||
pub features: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, FromRow)]
|
||||
pub struct PricingPackageRow {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub package_type: String,
|
||||
pub applicable_roles: Vec<String>,
|
||||
pub tracecoins_amount: i32,
|
||||
pub price: i32,
|
||||
pub duration_days: Option<i32>,
|
||||
pub valid_from: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub valid_until: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub is_promotional: bool,
|
||||
pub is_active: bool,
|
||||
pub features: Option<serde_json::Value>,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PricingPackageResponse {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub package_type: String,
|
||||
pub applicable_roles: Vec<String>,
|
||||
pub tracecoins_amount: i32,
|
||||
pub price: i32,
|
||||
pub duration_days: Option<i32>,
|
||||
pub valid_from: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub valid_until: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub is_promotional: bool,
|
||||
pub is_active: bool,
|
||||
pub features: Option<serde_json::Value>,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
pub is_available: bool,
|
||||
pub is_expired: bool,
|
||||
}
|
||||
|
||||
impl From<PricingPackageRow> for PricingPackageResponse {
|
||||
fn from(row: PricingPackageRow) -> Self {
|
||||
let now = chrono::Utc::now();
|
||||
let is_expired = row.valid_until.map(|v| v < now).unwrap_or(false);
|
||||
let is_not_started = row.valid_from.map(|v| v > now).unwrap_or(false);
|
||||
let is_available = row.is_active && !is_expired && !is_not_started;
|
||||
|
||||
PricingPackageResponse {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
package_type: row.package_type,
|
||||
applicable_roles: row.applicable_roles,
|
||||
tracecoins_amount: row.tracecoins_amount,
|
||||
price: row.price,
|
||||
duration_days: row.duration_days,
|
||||
valid_from: row.valid_from,
|
||||
valid_until: row.valid_until,
|
||||
is_promotional: row.is_promotional,
|
||||
is_active: row.is_active,
|
||||
features: row.features,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
is_available,
|
||||
is_expired,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", get(list_packages))
|
||||
.route("/", post(create_package))
|
||||
.route("/{id}", get(get_package))
|
||||
.route("/{id}", patch(update_package))
|
||||
.route("/{id}", delete(delete_package))
|
||||
.route("/by-type", get(get_packages_by_type))
|
||||
.route("/for-role", get(get_packages_for_role))
|
||||
}
|
||||
|
||||
async fn list_packages(
|
||||
State(state): State<AppState>,
|
||||
Query(q): Query<PaginationQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let page = q.page.unwrap_or(1);
|
||||
let limit = q.limit.unwrap_or(20).min(100);
|
||||
let offset = (page - 1) * limit;
|
||||
|
||||
let search_filter = q.search
|
||||
.as_ref()
|
||||
.map(|s| format!("AND (name ILIKE '%{}%' OR description ILIKE '%{}%')", s.replace('\'', "''"), s.replace('\'', "''")))
|
||||
.unwrap_or_default();
|
||||
|
||||
let packages = sqlx::query_as::<_, PricingPackageRow>(
|
||||
&format!(
|
||||
r#"
|
||||
SELECT id, name, description, package_type, applicable_roles,
|
||||
tracecoins_amount, price, duration_days, valid_from, valid_until,
|
||||
is_promotional, is_active, features, created_at, updated_at
|
||||
FROM pricing_packages
|
||||
WHERE 1=1 {}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT {} OFFSET {}
|
||||
"#,
|
||||
search_filter, limit, offset
|
||||
)
|
||||
)
|
||||
.fetch_all(&state.pool)
|
||||
.await;
|
||||
|
||||
let packages = match packages {
|
||||
Ok(p) => p,
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
};
|
||||
|
||||
let total: (i64,) = match sqlx::query_as(
|
||||
&format!(
|
||||
"SELECT COUNT(*) FROM pricing_packages WHERE 1=1 {}",
|
||||
search_filter
|
||||
)
|
||||
)
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
{
|
||||
Ok(t) => t,
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
};
|
||||
|
||||
let packages: Vec<PricingPackageResponse> = packages.into_iter().map(|p| p.into()).collect();
|
||||
|
||||
(StatusCode::OK, Json(serde_json::json!({
|
||||
"data": packages,
|
||||
"pagination": {
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total": total.0,
|
||||
"pages": (total.0 as f64 / limit as f64).ceil() as i64
|
||||
}
|
||||
}))).into_response()
|
||||
}
|
||||
|
||||
async fn get_package(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
match sqlx::query_as::<_, PricingPackageRow>(
|
||||
r#"
|
||||
SELECT id, name, description, package_type, applicable_roles,
|
||||
tracecoins_amount, price, duration_days, valid_from, valid_until,
|
||||
is_promotional, is_active, features, created_at, updated_at
|
||||
FROM pricing_packages WHERE id = $1
|
||||
"#
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
{
|
||||
Ok(Some(pkg)) => {
|
||||
let response: PricingPackageResponse = pkg.into();
|
||||
(StatusCode::OK, Json(response)).into_response()
|
||||
}
|
||||
Ok(None) => (StatusCode::NOT_FOUND, "Package not found").into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_package(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<CreatePackageRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let result = sqlx::query_as::<_, PricingPackageRow>(
|
||||
r#"
|
||||
INSERT INTO pricing_packages (name, description, package_type, applicable_roles,
|
||||
tracecoins_amount, price, duration_days, valid_from, valid_until,
|
||||
is_promotional, is_active, features)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
RETURNING id, name, description, package_type, applicable_roles,
|
||||
tracecoins_amount, price, duration_days, valid_from, valid_until,
|
||||
is_promotional, is_active, features, created_at, updated_at
|
||||
"#
|
||||
)
|
||||
.bind(&payload.name)
|
||||
.bind(&payload.description)
|
||||
.bind(&payload.package_type)
|
||||
.bind(&payload.applicable_roles)
|
||||
.bind(payload.tracecoins_amount)
|
||||
.bind(payload.price)
|
||||
.bind(payload.duration_days)
|
||||
.bind(payload.valid_from)
|
||||
.bind(payload.valid_until)
|
||||
.bind(payload.is_promotional.unwrap_or(false))
|
||||
.bind(payload.is_active.unwrap_or(true))
|
||||
.bind(payload.features)
|
||||
.fetch_one(&state.pool)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(pkg) => {
|
||||
let response: PricingPackageResponse = pkg.into();
|
||||
(StatusCode::CREATED, Json(response)).into_response()
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_package(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(payload): Json<UpdatePackageRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let existing = sqlx::query_as::<_, PricingPackageRow>(
|
||||
"SELECT * FROM pricing_packages WHERE id = $1"
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await;
|
||||
|
||||
let existing = match existing {
|
||||
Ok(Some(e)) => e,
|
||||
Ok(None) => return (StatusCode::NOT_FOUND, "Package not found").into_response(),
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
};
|
||||
|
||||
let updated = sqlx::query_as::<_, PricingPackageRow>(
|
||||
r#"
|
||||
UPDATE pricing_packages SET
|
||||
name = COALESCE($2, name),
|
||||
description = COALESCE($3, description),
|
||||
tracecoins_amount = COALESCE($4, tracecoins_amount),
|
||||
price = COALESCE($5, price),
|
||||
duration_days = COALESCE($6, duration_days),
|
||||
valid_from = COALESCE($7, valid_from),
|
||||
valid_until = COALESCE($8, valid_until),
|
||||
is_promotional = COALESCE($9, is_promotional),
|
||||
is_active = COALESCE($10, is_active),
|
||||
features = COALESCE($11, features),
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
RETURNING id, name, description, package_type, applicable_roles,
|
||||
tracecoins_amount, price, duration_days, valid_from, valid_until,
|
||||
is_promotional, is_active, features, created_at, updated_at
|
||||
"#
|
||||
)
|
||||
.bind(id)
|
||||
.bind(&payload.name)
|
||||
.bind(&payload.description)
|
||||
.bind(payload.tracecoins_amount)
|
||||
.bind(payload.price)
|
||||
.bind(payload.duration_days)
|
||||
.bind(payload.valid_from)
|
||||
.bind(payload.valid_until)
|
||||
.bind(payload.is_promotional)
|
||||
.bind(payload.is_active)
|
||||
.bind(payload.features)
|
||||
.fetch_one(&state.pool)
|
||||
.await;
|
||||
|
||||
match updated {
|
||||
Ok(pkg) => {
|
||||
let response: PricingPackageResponse = pkg.into();
|
||||
(StatusCode::OK, Json(response)).into_response()
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete_package(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
match sqlx::query("DELETE FROM pricing_packages WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
{
|
||||
Ok(r) if r.rows_affected() > 0 => {
|
||||
(StatusCode::OK, Json(serde_json::json!({"message": "Package deleted"}))).into_response()
|
||||
}
|
||||
Ok(_) => (StatusCode::NOT_FOUND, "Package not found").into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_packages_by_type(
|
||||
State(state): State<AppState>,
|
||||
Query(q): Query<PackageTypeQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let package_type = q.package_type.as_deref().unwrap_or("TRACECOIN_BUNDLE");
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
let packages = sqlx::query_as::<_, PricingPackageRow>(
|
||||
r#"
|
||||
SELECT id, name, description, package_type, applicable_roles,
|
||||
tracecoins_amount, price, duration_days, valid_from, valid_until,
|
||||
is_promotional, is_active, features, created_at, updated_at
|
||||
FROM pricing_packages
|
||||
WHERE package_type = $1
|
||||
AND is_active = true
|
||||
AND (valid_from IS NULL OR valid_from <= $2)
|
||||
AND (valid_until IS NULL OR valid_until > $2)
|
||||
ORDER BY is_promotional DESC, price ASC
|
||||
"#
|
||||
)
|
||||
.bind(package_type)
|
||||
.bind(now)
|
||||
.fetch_all(&state.pool)
|
||||
.await;
|
||||
|
||||
let packages = match packages {
|
||||
Ok(p) => p,
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
};
|
||||
|
||||
let packages: Vec<PricingPackageResponse> = packages.into_iter().map(|p| p.into()).collect();
|
||||
|
||||
(StatusCode::OK, Json(serde_json::json!({
|
||||
"data": packages,
|
||||
"package_type": package_type
|
||||
}))).into_response()
|
||||
}
|
||||
|
||||
async fn get_packages_for_role(
|
||||
State(state): State<AppState>,
|
||||
Query(q): Query<PackageTypeQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let applicable_role = q.applicable_role.as_deref().unwrap_or("");
|
||||
let active_only = q.active_only.unwrap_or(true);
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
let packages = sqlx::query_as::<_, PricingPackageRow>(
|
||||
&format!(
|
||||
r#"
|
||||
SELECT id, name, description, package_type, applicable_roles,
|
||||
tracecoins_amount, price, duration_days, valid_from, valid_until,
|
||||
is_promotional, is_active, features, created_at, updated_at
|
||||
FROM pricing_packages
|
||||
WHERE ($1 = '' OR $1 = ANY(applicable_roles))
|
||||
AND (is_active = true OR {} = false)
|
||||
AND (valid_from IS NULL OR valid_from <= $2)
|
||||
AND (valid_until IS NULL OR valid_until > $2)
|
||||
ORDER BY is_promotional DESC, price ASC
|
||||
"#,
|
||||
if active_only { "true" } else { "false" }
|
||||
)
|
||||
)
|
||||
.bind(applicable_role)
|
||||
.bind(now)
|
||||
.fetch_all(&state.pool)
|
||||
.await;
|
||||
|
||||
let packages = match packages {
|
||||
Ok(p) => p,
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
};
|
||||
|
||||
let packages: Vec<PricingPackageResponse> = packages.into_iter().map(|p| p.into()).collect();
|
||||
|
||||
(StatusCode::OK, Json(serde_json::json!({
|
||||
"data": packages,
|
||||
"applicable_role": applicable_role
|
||||
}))).into_response()
|
||||
}
|
||||
|
|
@ -125,10 +125,10 @@ SELECT gen_random_uuid(), p.user_id, 'ugc_content_creator', p.display_name, p.bi
|
|||
FROM ugc_content_creator_profiles p
|
||||
WHERE NOT EXISTS (SELECT 1 FROM user_role_profiles urp WHERE urp.user_id = p.user_id AND urp.role_key = 'ugc_content_creator');
|
||||
|
||||
-- Backfill from company_profiles
|
||||
-- Backfill from company_profiles (companies don't have bio)
|
||||
INSERT INTO user_role_profiles (id, user_id, role_key, display_name, bio, location, status, created_at, updated_at)
|
||||
SELECT gen_random_uuid(), cp.user_id, 'company', cp.company_name, cp.bio, NULL,
|
||||
COALESCE(cp.status, 'ACTIVE'), cp.created_at, COALESCE(cp.updated_at, NOW())
|
||||
SELECT gen_random_uuid(), cp.user_id, 'company', cp.company_name, NULL, NULL,
|
||||
'ACTIVE', cp.created_at, COALESCE(cp.updated_at, NOW())
|
||||
FROM company_profiles cp
|
||||
WHERE NOT EXISTS (SELECT 1 FROM user_role_profiles urp WHERE urp.user_id = cp.user_id AND urp.role_key = 'company');
|
||||
|
||||
|
|
|
|||
726
crates/db/migrations_new/001_minimal_setup.up.sql
Normal file
726
crates/db/migrations_new/001_minimal_setup.up.sql
Normal file
|
|
@ -0,0 +1,726 @@
|
|||
-- Minimal setup migration - creates essential tables needed by complete migration
|
||||
BEGIN;
|
||||
|
||||
-- Users table
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
phone VARCHAR(50),
|
||||
password_hash TEXT,
|
||||
full_name VARCHAR(255),
|
||||
account_type VARCHAR(50) DEFAULT 'USER',
|
||||
email_verified BOOLEAN DEFAULT false,
|
||||
phone_verified BOOLEAN DEFAULT false,
|
||||
status VARCHAR(50) DEFAULT 'ACTIVE',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_login_at TIMESTAMPTZ,
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- Roles table
|
||||
CREATE TABLE IF NOT EXISTS roles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(100) NOT NULL UNIQUE,
|
||||
code VARCHAR(50) NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
can_approve_requests BOOLEAN DEFAULT false,
|
||||
can_manage_system_settings BOOLEAN DEFAULT false,
|
||||
audience VARCHAR(50) DEFAULT 'USER',
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- User roles table
|
||||
CREATE TABLE IF NOT EXISTS user_roles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Role permissions table
|
||||
CREATE TABLE IF NOT EXISTS role_permissions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
||||
permission VARCHAR(100) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Departments table
|
||||
CREATE TABLE IF NOT EXISTS departments (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
code VARCHAR(50),
|
||||
description TEXT,
|
||||
department_head UUID REFERENCES users(id),
|
||||
department_email VARCHAR(255),
|
||||
visibility VARCHAR(50) DEFAULT 'VISIBLE',
|
||||
transfers_enabled BOOLEAN DEFAULT false,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Designations table
|
||||
CREATE TABLE IF NOT EXISTS designations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
title VARCHAR(255) NOT NULL,
|
||||
code VARCHAR(50),
|
||||
department_id UUID REFERENCES departments(id) ON DELETE SET NULL,
|
||||
description TEXT,
|
||||
level INTEGER DEFAULT 1,
|
||||
can_manage_team BOOLEAN DEFAULT false,
|
||||
can_approve BOOLEAN DEFAULT false,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Employees table
|
||||
CREATE TABLE IF NOT EXISTS employees (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE,
|
||||
department_id UUID REFERENCES departments(id) ON DELETE SET NULL,
|
||||
designation_id UUID REFERENCES designations(id) ON DELETE SET NULL,
|
||||
status VARCHAR(50) DEFAULT 'ACTIVE',
|
||||
joining_date DATE,
|
||||
employment_status VARCHAR(50),
|
||||
manager_employee_id UUID REFERENCES employees(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Refresh tokens table
|
||||
CREATE TABLE IF NOT EXISTS refresh_tokens (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token_hash VARCHAR(255) NOT NULL UNIQUE,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Notification preferences table
|
||||
CREATE TABLE IF NOT EXISTS notification_preferences (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE,
|
||||
email_enabled BOOLEAN DEFAULT true,
|
||||
push_enabled BOOLEAN DEFAULT true,
|
||||
sms_enabled BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Profile extension tables (with user_role_profile_id for new schema)
|
||||
CREATE TABLE IF NOT EXISTS photographer_profiles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_role_profile_id UUID,
|
||||
equipment_list TEXT,
|
||||
years_of_experience INTEGER,
|
||||
hourly_rate INTEGER,
|
||||
specialties TEXT[] DEFAULT '{}',
|
||||
camera_brands TEXT[] DEFAULT '{}',
|
||||
studio_available BOOLEAN DEFAULT false,
|
||||
outdoor_shoots BOOLEAN DEFAULT true,
|
||||
travel_radius_km INTEGER DEFAULT 50,
|
||||
starting_price_inr INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tutor_profiles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_role_profile_id UUID,
|
||||
subjects TEXT[] DEFAULT '{}',
|
||||
board_types TEXT[] DEFAULT '{}',
|
||||
qualification VARCHAR(255),
|
||||
teaches_online BOOLEAN DEFAULT true,
|
||||
teaches_offline BOOLEAN DEFAULT true,
|
||||
experience_years INTEGER DEFAULT 0,
|
||||
hourly_rate_inr INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS makeup_artist_profiles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_role_profile_id UUID,
|
||||
specialties TEXT[] DEFAULT '{}',
|
||||
experience_years INTEGER DEFAULT 0,
|
||||
hourly_rate INTEGER DEFAULT 0,
|
||||
willing_to_travel BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS developer_profiles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_role_profile_id UUID,
|
||||
skills TEXT[] DEFAULT '{}',
|
||||
experience_years INTEGER DEFAULT 0,
|
||||
hourly_rate INTEGER DEFAULT 0,
|
||||
preferred_roles TEXT[] DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS video_editor_profiles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_role_profile_id UUID,
|
||||
software_expertise TEXT[] DEFAULT '{}',
|
||||
experience_years INTEGER DEFAULT 0,
|
||||
hourly_rate INTEGER DEFAULT 0,
|
||||
portfolio_url TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS graphic_designer_profiles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_role_profile_id UUID,
|
||||
design_specialties TEXT[] DEFAULT '{}',
|
||||
experience_years INTEGER DEFAULT 0,
|
||||
hourly_rate INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS social_media_manager_profiles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_role_profile_id UUID,
|
||||
platforms_managed TEXT[] DEFAULT '{}',
|
||||
experience_years INTEGER DEFAULT 0,
|
||||
hourly_rate INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fitness_trainer_profiles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_role_profile_id UUID,
|
||||
certifications TEXT[] DEFAULT '{}',
|
||||
specializations TEXT[] DEFAULT '{}',
|
||||
experience_years INTEGER DEFAULT 0,
|
||||
hourly_rate INTEGER DEFAULT 0,
|
||||
offers_online_sessions BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS catering_service_profiles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_role_profile_id UUID,
|
||||
cuisine_types TEXT[] DEFAULT '{}',
|
||||
min_order_amount INTEGER DEFAULT 0,
|
||||
experience_years INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ugc_content_creator_profiles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_role_profile_id UUID,
|
||||
platforms TEXT[] DEFAULT '{}',
|
||||
follower_count INTEGER DEFAULT 0,
|
||||
niche TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Company profiles
|
||||
CREATE TABLE IF NOT EXISTS company_profiles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
company_name VARCHAR(255) NOT NULL,
|
||||
registration_number VARCHAR(100),
|
||||
industry VARCHAR(150),
|
||||
website_url VARCHAR(255),
|
||||
employee_count INTEGER,
|
||||
business_type VARCHAR(100),
|
||||
gst_number VARCHAR(50),
|
||||
contact_name VARCHAR(255),
|
||||
contact_email VARCHAR(255),
|
||||
contact_phone VARCHAR(50),
|
||||
address_line1 TEXT,
|
||||
city VARCHAR(100),
|
||||
state VARCHAR(100),
|
||||
country VARCHAR(100) DEFAULT 'India',
|
||||
postal_code VARCHAR(20),
|
||||
status VARCHAR(50) DEFAULT 'ACTIVE',
|
||||
free_job_slots INTEGER DEFAULT 3,
|
||||
purchased_job_slots INTEGER DEFAULT 0,
|
||||
free_contact_views INTEGER DEFAULT 10,
|
||||
purchased_contact_views INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(user_id)
|
||||
);
|
||||
|
||||
-- Customer profiles
|
||||
CREATE TABLE IF NOT EXISTS customer_profiles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
full_name VARCHAR(255),
|
||||
phone VARCHAR(50),
|
||||
city VARCHAR(100),
|
||||
area VARCHAR(100),
|
||||
preferred_professions TEXT[] DEFAULT '{}',
|
||||
active_requirement_count INTEGER DEFAULT 0,
|
||||
status VARCHAR(50) DEFAULT 'ACTIVE',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(user_id)
|
||||
);
|
||||
|
||||
-- Job seeker profiles
|
||||
CREATE TABLE IF NOT EXISTS job_seeker_profiles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
full_name VARCHAR(255),
|
||||
location VARCHAR(255),
|
||||
summary TEXT,
|
||||
experience_years INTEGER DEFAULT 0,
|
||||
skills TEXT[] DEFAULT '{}',
|
||||
resume_url TEXT,
|
||||
active_application_count INTEGER DEFAULT 0,
|
||||
status VARCHAR(50) DEFAULT 'ACTIVE',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(user_id)
|
||||
);
|
||||
|
||||
-- Jobs table
|
||||
CREATE TABLE IF NOT EXISTS jobs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
company_id UUID REFERENCES company_profiles(id) ON DELETE CASCADE,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
requirements TEXT,
|
||||
location VARCHAR(255),
|
||||
job_type VARCHAR(50),
|
||||
experience_min INTEGER DEFAULT 0,
|
||||
experience_max INTEGER DEFAULT 0,
|
||||
salary_min INTEGER,
|
||||
salary_max INTEGER,
|
||||
status VARCHAR(50) DEFAULT 'ACTIVE',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Job applications table
|
||||
CREATE TABLE IF NOT EXISTS job_applications (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
job_id UUID NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
|
||||
applicant_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
cover_letter TEXT,
|
||||
resume_url TEXT,
|
||||
status VARCHAR(50) DEFAULT 'PENDING',
|
||||
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Create user_role_profiles first (needed by leads table)
|
||||
CREATE TABLE IF NOT EXISTS user_role_profiles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
role_key VARCHAR(50) NOT NULL,
|
||||
display_name TEXT,
|
||||
bio TEXT,
|
||||
location TEXT,
|
||||
avatar_url TEXT,
|
||||
phone TEXT,
|
||||
email TEXT,
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE',
|
||||
verification_status VARCHAR(50) DEFAULT 'PENDING',
|
||||
approval_status VARCHAR(50) DEFAULT 'PENDING',
|
||||
rejection_reason TEXT,
|
||||
approved_at TIMESTAMPTZ,
|
||||
verified_at TIMESTAMPTZ,
|
||||
is_profile_public BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(user_id, role_key)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_role_profiles_user_id ON user_role_profiles(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_role_profiles_role_key ON user_role_profiles(role_key);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_role_profiles_status ON user_role_profiles(status);
|
||||
|
||||
-- Leads table (renamed from requirements)
|
||||
CREATE TABLE IF NOT EXISTS leads (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_role_profile_id UUID REFERENCES user_role_profiles(id),
|
||||
customer_id UUID REFERENCES users(id),
|
||||
profession_key VARCHAR(50),
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
location VARCHAR(255),
|
||||
budget_min INTEGER,
|
||||
budget_max INTEGER,
|
||||
urgency VARCHAR(50) DEFAULT 'NORMAL',
|
||||
status VARCHAR(50) DEFAULT 'OPEN',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Other essential tables
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
notification_type VARCHAR(50),
|
||||
reference_id UUID,
|
||||
is_read BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS reviews (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_role_profile_id UUID NOT NULL REFERENCES user_role_profiles(id),
|
||||
reviewer_user_id UUID NOT NULL REFERENCES users(id),
|
||||
rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5),
|
||||
comment TEXT,
|
||||
status VARCHAR(50) DEFAULT 'VISIBLE',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tracecoin_wallets (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE,
|
||||
balance BIGINT DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tracecoin_ledger (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
wallet_id UUID NOT NULL REFERENCES tracecoin_wallets(id),
|
||||
transaction_type VARCHAR(50) NOT NULL,
|
||||
amount BIGINT NOT NULL,
|
||||
balance_after BIGINT NOT NULL,
|
||||
reference_type VARCHAR(50),
|
||||
reference_id UUID,
|
||||
description TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS services (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_role_profile_id UUID REFERENCES user_role_profiles(id),
|
||||
service_name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
price INTEGER,
|
||||
price_type VARCHAR(50) DEFAULT 'FIXED',
|
||||
duration_minutes INTEGER,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pricing_packages (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_role_profile_id UUID REFERENCES user_role_profiles(id),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
price INTEGER NOT NULL,
|
||||
duration_days INTEGER,
|
||||
features JSONB,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS coupons (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
code VARCHAR(50) NOT NULL UNIQUE,
|
||||
discount_type VARCHAR(20) NOT NULL,
|
||||
discount_value INTEGER NOT NULL,
|
||||
min_order_amount INTEGER DEFAULT 0,
|
||||
max_uses INTEGER,
|
||||
used_count INTEGER DEFAULT 0,
|
||||
valid_from TIMESTAMPTZ,
|
||||
valid_until TIMESTAMPTZ,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS coupon_redemptions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
coupon_id UUID NOT NULL REFERENCES coupons(id),
|
||||
user_id UUID NOT NULL REFERENCES users(id),
|
||||
redeemed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS discounts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
package_id UUID NOT NULL REFERENCES pricing_packages(id),
|
||||
discount_type VARCHAR(20) NOT NULL,
|
||||
discount_value INTEGER NOT NULL,
|
||||
valid_from TIMESTAMPTZ,
|
||||
valid_until TIMESTAMPTZ,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payments (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id),
|
||||
amount INTEGER NOT NULL,
|
||||
currency VARCHAR(10) DEFAULT 'INR',
|
||||
payment_method VARCHAR(50),
|
||||
payment_status VARCHAR(50) DEFAULT 'PENDING',
|
||||
reference_id VARCHAR(255),
|
||||
transaction_id VARCHAR(255),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS invoices (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id),
|
||||
invoice_number VARCHAR(50) NOT NULL UNIQUE,
|
||||
amount INTEGER NOT NULL,
|
||||
status VARCHAR(50) DEFAULT 'DRAFT',
|
||||
issued_at TIMESTAMPTZ,
|
||||
due_at TIMESTAMPTZ,
|
||||
paid_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS portfolio_items (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_role_profile_id UUID REFERENCES user_role_profiles(id),
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
media_url TEXT,
|
||||
media_type VARCHAR(50),
|
||||
display_order INTEGER DEFAULT 0,
|
||||
is_featured BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS verification_requests (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_role_profile_id UUID REFERENCES user_role_profiles(id),
|
||||
verification_type VARCHAR(50) NOT NULL DEFAULT 'IDENTITY',
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
|
||||
submitted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
reviewed_at TIMESTAMPTZ,
|
||||
reviewed_by_user_id UUID REFERENCES users(id),
|
||||
remarks TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS verification_documents (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
verification_request_id UUID NOT NULL REFERENCES verification_requests(id) ON DELETE CASCADE,
|
||||
document_type VARCHAR(100) NOT NULL,
|
||||
file_url TEXT NOT NULL,
|
||||
file_name TEXT,
|
||||
mime_type TEXT,
|
||||
status VARCHAR(50) DEFAULT 'PENDING',
|
||||
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
reviewed_at TIMESTAMPTZ,
|
||||
reviewed_by_user_id UUID REFERENCES users(id),
|
||||
remarks TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS verification_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
verification_request_id UUID NOT NULL REFERENCES verification_requests(id),
|
||||
action VARCHAR(50) NOT NULL,
|
||||
old_status VARCHAR(50),
|
||||
new_status VARCHAR(50),
|
||||
acted_by_user_id UUID REFERENCES users(id),
|
||||
remarks TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS verifications (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_role_profile_id UUID NOT NULL REFERENCES user_role_profiles(id),
|
||||
verification_type VARCHAR(50) NOT NULL,
|
||||
document_url TEXT,
|
||||
status VARCHAR(50) DEFAULT 'PENDING',
|
||||
verified_at TIMESTAMPTZ,
|
||||
verified_by UUID REFERENCES users(id),
|
||||
rejection_reason TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_settings (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE,
|
||||
theme VARCHAR(20) DEFAULT 'LIGHT',
|
||||
language VARCHAR(10) DEFAULT 'en',
|
||||
timezone VARCHAR(50) DEFAULT 'Asia/Kolkata',
|
||||
email_notifications BOOLEAN DEFAULT true,
|
||||
push_notifications BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS account_deletion_requests (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
reason TEXT,
|
||||
scheduled_deletion_at TIMESTAMPTZ,
|
||||
status VARCHAR(50) DEFAULT 'PENDING',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES users(id),
|
||||
action VARCHAR(100) NOT NULL,
|
||||
entity_type VARCHAR(50),
|
||||
entity_id UUID,
|
||||
metadata JSONB,
|
||||
ip_address TEXT,
|
||||
user_agent TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS email_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES users(id),
|
||||
email_type VARCHAR(50) NOT NULL,
|
||||
recipient_email VARCHAR(255) NOT NULL,
|
||||
subject VARCHAR(255),
|
||||
status VARCHAR(50) DEFAULT 'SENT',
|
||||
error_message TEXT,
|
||||
sent_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- KB tables
|
||||
CREATE TABLE IF NOT EXISTS kb_categories (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
slug VARCHAR(255) NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
display_order INTEGER DEFAULT 0,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS kb_articles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
category_id UUID REFERENCES kb_categories(id) ON DELETE SET NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
slug VARCHAR(255) NOT NULL UNIQUE,
|
||||
content TEXT,
|
||||
summary TEXT,
|
||||
tags TEXT[],
|
||||
views_count INTEGER DEFAULT 0,
|
||||
is_published BOOLEAN DEFAULT false,
|
||||
is_featured BOOLEAN DEFAULT false,
|
||||
published_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Support tables
|
||||
CREATE TABLE IF NOT EXISTS support_tickets (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id),
|
||||
subject VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
priority VARCHAR(20) DEFAULT 'MEDIUM',
|
||||
status VARCHAR(50) DEFAULT 'OPEN',
|
||||
assigned_to UUID REFERENCES users(id),
|
||||
resolved_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS support_ticket_messages (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
ticket_id UUID NOT NULL REFERENCES support_tickets(id) ON DELETE CASCADE,
|
||||
sender_id UUID NOT NULL REFERENCES users(id),
|
||||
message TEXT NOT NULL,
|
||||
is_internal BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Config tables
|
||||
CREATE TABLE IF NOT EXISTS runtime_configs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
config_key VARCHAR(100) NOT NULL UNIQUE,
|
||||
config_value JSONB NOT NULL,
|
||||
description TEXT,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS onboarding_configs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
role_key VARCHAR(50) NOT NULL,
|
||||
steps JSONB NOT NULL,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dashboard_configs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
role_key VARCHAR(50) NOT NULL,
|
||||
audience VARCHAR(50) DEFAULT 'USER',
|
||||
widgets JSONB NOT NULL,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Create indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON user_roles(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_roles_role_id ON user_roles(role_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_roles_status ON user_roles(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON notifications(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_notifications_is_read ON notifications(is_read);
|
||||
CREATE INDEX IF NOT EXISTS idx_reviews_user_role_profile_id ON reviews(user_role_profile_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_services_user_role_profile_id ON services(user_role_profile_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_pricing_packages_user_role_profile_id ON pricing_packages(user_role_profile_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_portfolio_items_user_role_profile_id ON portfolio_items(user_role_profile_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_jobs_company_id ON jobs(company_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_job_applications_job_id ON job_applications(job_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_job_applications_applicant ON job_applications(applicant_user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_leads_status ON leads(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_leads_user_role_profile_id ON leads(user_role_profile_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tracecoin_ledger_wallet_id ON tracecoin_ledger(wallet_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_coupons_code ON coupons(code);
|
||||
CREATE INDEX IF NOT EXISTS idx_support_tickets_user_id ON support_tickets(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_support_tickets_status ON support_tickets(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_activity_logs_user_id ON activity_logs(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_activity_logs_created_at ON activity_logs(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_email_logs_user_id ON email_logs(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_email_logs_sent_at ON email_logs(sent_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_kb_articles_category_id ON kb_articles(category_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_kb_articles_slug ON kb_articles(slug);
|
||||
CREATE INDEX IF NOT EXISTS idx_verification_requests_user_role_profile_id ON verification_requests(user_role_profile_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_verifications_user_role_profile_id ON verifications(user_role_profile_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_settings_user_id ON user_settings(user_id);
|
||||
|
||||
-- Insert default roles
|
||||
INSERT INTO roles (id, name, code, description, audience, is_active) VALUES
|
||||
('00000000-0000-0000-0000-000000000001', 'Super Admin', 'SUPER_ADMIN', 'Full system access', 'ADMIN', true),
|
||||
('00000000-0000-0000-0000-000000000002', 'Admin', 'ADMIN', 'Administrative access', 'ADMIN', true),
|
||||
('00000000-0000-0000-0000-000000000003', 'User', 'USER', 'Regular user', 'USER', true),
|
||||
('00000000-0000-0000-0000-000000000004', 'Company', 'COMPANY', 'Company account', 'BUSINESS', true),
|
||||
('00000000-0000-0000-0000-000000000005', 'Customer', 'CUSTOMER', 'Customer account', 'USER', true)
|
||||
ON CONFLICT (code) DO NOTHING;
|
||||
|
||||
COMMIT;
|
||||
285
crates/db/migrations_new/002_pricing_and_leads.up.sql
Normal file
285
crates/db/migrations_new/002_pricing_and_leads.up.sql
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
-- ============================================================================
|
||||
-- MIGRATION: Complete Schema Updates for Pricing & Lead Requests
|
||||
-- ============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- ============================================================================
|
||||
-- 1. Update pricing_packages table with new columns
|
||||
-- ============================================================================
|
||||
|
||||
ALTER TABLE pricing_packages
|
||||
DROP COLUMN IF EXISTS user_role_profile_id,
|
||||
ADD COLUMN IF NOT EXISTS package_type VARCHAR(50) NOT NULL DEFAULT 'TRACECOIN_BUNDLE',
|
||||
ADD COLUMN IF NOT EXISTS applicable_roles TEXT[] DEFAULT '{}',
|
||||
ADD COLUMN IF NOT EXISTS tracecoins_amount INTEGER DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS valid_from TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS valid_until TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS is_promotional BOOLEAN DEFAULT false;
|
||||
|
||||
-- Add index for package lookup
|
||||
CREATE INDEX IF NOT EXISTS idx_pricing_packages_type ON pricing_packages(package_type);
|
||||
|
||||
-- ============================================================================
|
||||
-- 2. Update payments table with new columns
|
||||
-- ============================================================================
|
||||
|
||||
ALTER TABLE payments
|
||||
ADD COLUMN IF NOT EXISTS package_id UUID,
|
||||
ADD COLUMN IF NOT EXISTS razorpay_order_id VARCHAR(255),
|
||||
ADD COLUMN IF NOT EXISTS razorpay_payment_id VARCHAR(255),
|
||||
ADD COLUMN IF NOT EXISTS tracecoins_credited INTEGER DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS verified_at TIMESTAMPTZ;
|
||||
|
||||
-- Add foreign key if not exists
|
||||
ALTER TABLE payments
|
||||
ADD CONSTRAINT payments_package_id_fkey
|
||||
FOREIGN KEY (package_id) REFERENCES pricing_packages(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_package_id ON payments(package_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_razorpay_order_id ON payments(razorpay_order_id);
|
||||
|
||||
-- ============================================================================
|
||||
-- 3. Create lead_requests table
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS lead_requests (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
lead_id UUID NOT NULL REFERENCES leads(id) ON DELETE CASCADE,
|
||||
user_role_profile_id UUID NOT NULL REFERENCES user_role_profiles(id) ON DELETE CASCADE,
|
||||
customer_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
|
||||
tracecoins_reserved INTEGER NOT NULL DEFAULT 25,
|
||||
message TEXT,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
accepted_at TIMESTAMPTZ,
|
||||
rejected_at TIMESTAMPTZ,
|
||||
rejected_reason TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_lead_requests_lead_id ON lead_requests(lead_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_lead_requests_user_role_profile_id ON lead_requests(user_role_profile_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_lead_requests_customer_user_id ON lead_requests(customer_user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_lead_requests_status ON lead_requests(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_lead_requests_expires_at ON lead_requests(expires_at);
|
||||
|
||||
-- ============================================================================
|
||||
-- 4. Add display_code to company_profiles
|
||||
-- ============================================================================
|
||||
|
||||
ALTER TABLE company_profiles
|
||||
ADD COLUMN IF NOT EXISTS display_code VARCHAR(20) UNIQUE;
|
||||
|
||||
-- ============================================================================
|
||||
-- 5. Add display_code to customer_profiles
|
||||
-- ============================================================================
|
||||
|
||||
ALTER TABLE customer_profiles
|
||||
ADD COLUMN IF NOT EXISTS display_code VARCHAR(20) UNIQUE;
|
||||
|
||||
-- ============================================================================
|
||||
-- 6. Add display_code to job_seeker_profiles
|
||||
-- ============================================================================
|
||||
|
||||
ALTER TABLE job_seeker_profiles
|
||||
ADD COLUMN IF NOT EXISTS display_code VARCHAR(20) UNIQUE;
|
||||
|
||||
-- ============================================================================
|
||||
-- 7. Add free_requirement_slots and purchased_requirement_slots to customer_profiles
|
||||
-- ============================================================================
|
||||
|
||||
ALTER TABLE customer_profiles
|
||||
ADD COLUMN IF NOT EXISTS free_requirement_slots INTEGER DEFAULT 2,
|
||||
ADD COLUMN IF NOT EXISTS purchased_requirement_slots INTEGER DEFAULT 0;
|
||||
|
||||
-- ============================================================================
|
||||
-- 8. Update leads table with new columns
|
||||
-- ============================================================================
|
||||
|
||||
ALTER TABLE leads
|
||||
ADD COLUMN IF NOT EXISTS customer_user_id UUID REFERENCES users(id),
|
||||
ADD COLUMN IF NOT EXISTS max_acceptances INTEGER DEFAULT 10,
|
||||
ADD COLUMN IF NOT EXISTS current_acceptances INTEGER DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS expires_at TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS cover_image_url TEXT;
|
||||
|
||||
-- ============================================================================
|
||||
-- 9. Add tracecoins_reserved column to lead_requests for proper tracking
|
||||
-- ============================================================================
|
||||
|
||||
-- Already added in step 3
|
||||
|
||||
-- ============================================================================
|
||||
-- 10. Create function to auto-generate display codes
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION generate_display_code(prefix VARCHAR(10))
|
||||
RETURNS VARCHAR(20) AS $$
|
||||
DECLARE
|
||||
new_code VARCHAR(20);
|
||||
seq_num INTEGER;
|
||||
BEGIN
|
||||
-- Get the next sequence number for this prefix
|
||||
SELECT COALESCE(
|
||||
(SELECT MAX(CAST(SUBSTRING(code FROM 4) AS INTEGER)) + 1
|
||||
FROM (
|
||||
SELECT display_code as code FROM company_profiles WHERE display_code LIKE prefix || '%'
|
||||
UNION ALL
|
||||
SELECT display_code as code FROM customer_profiles WHERE display_code LIKE prefix || '%'
|
||||
UNION ALL
|
||||
SELECT display_code as code FROM job_seeker_profiles WHERE display_code LIKE prefix || '%'
|
||||
) all_codes
|
||||
),
|
||||
1
|
||||
) INTO seq_num;
|
||||
|
||||
new_code := prefix || LPAD(seq_num::TEXT, 4, '0');
|
||||
RETURN new_code;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- ============================================================================
|
||||
-- 11. Create indexes for performance
|
||||
-- ============================================================================
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_leads_customer_user_id ON leads(customer_user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_leads_status_expires ON leads(status, expires_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_role_profiles_role_key ON user_role_profiles(role_key);
|
||||
|
||||
-- ============================================================================
|
||||
-- 12. Create tracecoin ledger entry types enum support
|
||||
-- ============================================================================
|
||||
|
||||
-- Add transaction_type values for ledger tracking
|
||||
-- The ledger already has 'type' column, ensure we have proper entries
|
||||
|
||||
-- ============================================================================
|
||||
-- 13. Add indexes for verification queries
|
||||
-- ============================================================================
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_verifications_user_role_profile_id ON verifications(user_role_profile_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_verifications_status ON verifications(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_verification_requests_user_role_profile_id ON verification_requests(user_role_profile_id);
|
||||
|
||||
-- ============================================================================
|
||||
-- 14. Add tracecoin refund support to ledger
|
||||
-- ============================================================================
|
||||
|
||||
-- Reserve tracecoins for lead request
|
||||
CREATE OR REPLACE FUNCTION reserve_tracecoins_for_lead_request(
|
||||
p_user_id UUID,
|
||||
p_amount INTEGER,
|
||||
p_lead_request_id UUID
|
||||
) RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
v_wallet_id UUID;
|
||||
v_balance BIGINT;
|
||||
BEGIN
|
||||
-- Get wallet
|
||||
SELECT id, balance INTO v_wallet_id, v_balance
|
||||
FROM tracecoin_wallets
|
||||
WHERE user_id = p_user_id
|
||||
FOR UPDATE;
|
||||
|
||||
IF v_wallet_id IS NULL THEN
|
||||
RETURN FALSE;
|
||||
END IF;
|
||||
|
||||
IF v_balance < p_amount THEN
|
||||
RETURN FALSE;
|
||||
END IF;
|
||||
|
||||
-- Deduct from balance (reserved, not yet spent)
|
||||
UPDATE tracecoin_wallets
|
||||
SET balance = balance - p_amount,
|
||||
reserved = COALESCE(reserved, 0) + p_amount,
|
||||
updated_at = NOW()
|
||||
WHERE id = v_wallet_id;
|
||||
|
||||
-- Add ledger entry
|
||||
INSERT INTO tracecoin_ledger (wallet_id, type, amount, balance_after, reference_type, reference_id, reason)
|
||||
VALUES (v_wallet_id, 'RESERVE', -p_amount, v_balance - p_amount, 'LEAD_REQUEST', p_lead_request_id, 'Lead request reservation');
|
||||
|
||||
RETURN TRUE;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Confirm tracecoins (when customer accepts)
|
||||
CREATE OR REPLACE FUNCTION confirm_tracecoins_for_lead(
|
||||
p_user_id UUID,
|
||||
p_amount INTEGER,
|
||||
p_lead_request_id UUID
|
||||
) RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
v_wallet_id UUID;
|
||||
v_balance BIGINT;
|
||||
BEGIN
|
||||
-- Get wallet
|
||||
SELECT id, balance, COALESCE(reserved, 0) INTO v_wallet_id, v_balance, v_balance
|
||||
FROM tracecoin_wallets
|
||||
WHERE user_id = p_user_id
|
||||
FOR UPDATE;
|
||||
|
||||
IF v_wallet_id IS NULL THEN
|
||||
RETURN FALSE;
|
||||
END IF;
|
||||
|
||||
-- Move from reserved to spent
|
||||
UPDATE tracecoin_wallets
|
||||
SET reserved = reserved - p_amount,
|
||||
updated_at = NOW()
|
||||
WHERE id = v_wallet_id;
|
||||
|
||||
-- Add ledger entry for confirmation
|
||||
INSERT INTO tracecoin_ledger (wallet_id, type, amount, balance_after, reference_type, reference_id, reason)
|
||||
VALUES (v_wallet_id, 'SPEND', -p_amount, v_balance, 'LEAD_REQUEST', p_lead_request_id, 'Lead request accepted');
|
||||
|
||||
RETURN TRUE;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Release tracecoins (when customer rejects or request expires)
|
||||
CREATE OR REPLACE FUNCTION release_tracecoins_for_lead(
|
||||
p_user_id UUID,
|
||||
p_amount INTEGER,
|
||||
p_lead_request_id UUID
|
||||
) RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
v_wallet_id UUID;
|
||||
v_balance BIGINT;
|
||||
BEGIN
|
||||
-- Get wallet
|
||||
SELECT id, balance INTO v_wallet_id, v_balance
|
||||
FROM tracecoin_wallets
|
||||
WHERE user_id = p_user_id
|
||||
FOR UPDATE;
|
||||
|
||||
IF v_wallet_id IS NULL THEN
|
||||
RETURN FALSE;
|
||||
END IF;
|
||||
|
||||
-- Return to available balance from reserved
|
||||
UPDATE tracecoin_wallets
|
||||
SET balance = balance + p_amount,
|
||||
reserved = reserved - p_amount,
|
||||
updated_at = NOW()
|
||||
WHERE id = v_wallet_id;
|
||||
|
||||
-- Add ledger entry for release
|
||||
INSERT INTO tracecoin_ledger (wallet_id, type, amount, balance_after, reference_type, reference_id, reason)
|
||||
VALUES (v_wallet_id, 'RELEASE', p_amount, v_balance + p_amount, 'LEAD_REQUEST', p_lead_request_id, 'Lead request rejected/expired');
|
||||
|
||||
RETURN TRUE;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- ============================================================================
|
||||
-- 15. Update tracecoin_wallets to have reserved column
|
||||
-- ============================================================================
|
||||
|
||||
ALTER TABLE tracecoin_wallets
|
||||
ADD COLUMN IF NOT EXISTS reserved BIGINT DEFAULT 0;
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -0,0 +1,618 @@
|
|||
-- ============================================================================
|
||||
-- Nxtgauge Database Complete Migration
|
||||
-- Version: 20260415000000
|
||||
-- This migration performs a COMPLETE schema transformation
|
||||
-- NO FALLBACKS - This is a one-way migration
|
||||
-- ============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- ============================================================================
|
||||
-- PHASE 1: Create New Core Tables
|
||||
-- ============================================================================
|
||||
|
||||
-- 1.1 user_sessions (new)
|
||||
CREATE TABLE IF NOT EXISTS user_sessions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
session_token TEXT UNIQUE NOT NULL,
|
||||
ip_address TEXT,
|
||||
user_agent TEXT,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_sessions_user_id ON user_sessions(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_sessions_token ON user_sessions(session_token);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_sessions_expires ON user_sessions(expires_at);
|
||||
|
||||
-- 1.2 user_role_profiles (NEW ROOT - CRITICAL)
|
||||
CREATE TABLE IF NOT EXISTS user_role_profiles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
role_key VARCHAR(50) NOT NULL,
|
||||
display_name TEXT,
|
||||
bio TEXT,
|
||||
location TEXT,
|
||||
avatar_url TEXT,
|
||||
phone TEXT,
|
||||
email TEXT,
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE',
|
||||
verification_status VARCHAR(50) DEFAULT 'PENDING',
|
||||
approval_status VARCHAR(50) DEFAULT 'PENDING',
|
||||
rejection_reason TEXT,
|
||||
approved_at TIMESTAMPTZ,
|
||||
verified_at TIMESTAMPTZ,
|
||||
is_profile_public BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(user_id, role_key)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_role_profiles_user_id ON user_role_profiles(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_role_profiles_role_key ON user_role_profiles(role_key);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_role_profiles_status ON user_role_profiles(status);
|
||||
|
||||
-- ============================================================================
|
||||
-- PHASE 2: Skip backfill for fresh database
|
||||
-- ============================================================================
|
||||
-- (No data to backfill from - profile tables use user_role_profile_id directly)
|
||||
|
||||
-- ============================================================================
|
||||
-- PHASE 3: Update Extension Tables to Use user_role_profile_id
|
||||
-- ============================================================================
|
||||
|
||||
-- Add user_role_profile_id column to ALL extension tables (already exist in fresh schema)
|
||||
|
||||
-- ============================================================================
|
||||
-- PHASE 5: Update Portfolio Tables
|
||||
-- ============================================================================
|
||||
|
||||
-- Add user_role_profile_id to portfolio_items
|
||||
ALTER TABLE portfolio_items ADD COLUMN IF NOT EXISTS user_role_profile_id UUID;
|
||||
ALTER TABLE portfolio_items ADD COLUMN IF NOT EXISTS display_order INTEGER DEFAULT 0;
|
||||
|
||||
-- Backfill portfolio_items from professionals
|
||||
UPDATE portfolio_items pi SET user_role_profile_id = urp.id
|
||||
FROM user_role_profiles urp
|
||||
WHERE pi.professional_id IS NOT NULL
|
||||
AND urp.user_id = (SELECT user_id FROM professionals p WHERE p.id = pi.professional_id);
|
||||
|
||||
-- Update remaining using user_id
|
||||
UPDATE portfolio_items pi SET user_role_profile_id = urp.id
|
||||
FROM user_role_profiles urp
|
||||
WHERE pi.user_id IS NOT NULL AND pi.user_role_profile_id IS NULL
|
||||
AND EXISTS (SELECT 1 FROM user_role_profiles urp2 WHERE urp2.user_id = pi.user_id AND urp2.role_key = pi.profession_key);
|
||||
|
||||
-- Add user_role_profile_id to services
|
||||
ALTER TABLE services ADD COLUMN IF NOT EXISTS user_role_profile_id UUID;
|
||||
|
||||
-- Backfill services
|
||||
UPDATE services s SET user_role_profile_id = urp.id
|
||||
FROM user_role_profiles urp
|
||||
WHERE s.professional_id IS NOT NULL
|
||||
AND urp.user_id = (SELECT user_id FROM professionals p WHERE p.id = s.professional_id);
|
||||
|
||||
UPDATE services s SET user_role_profile_id = urp.id
|
||||
FROM user_role_profiles urp
|
||||
WHERE s.user_id IS NOT NULL AND s.user_role_profile_id IS NULL
|
||||
AND EXISTS (SELECT 1 FROM user_role_profiles urp2 WHERE urp2.user_id = s.user_id AND urp2.role_key = s.profession_key);
|
||||
|
||||
-- ============================================================================
|
||||
-- PHASE 6: Rename Tables
|
||||
-- ============================================================================
|
||||
|
||||
-- Rename applications -> job_applications
|
||||
ALTER TABLE applications RENAME TO job_applications;
|
||||
|
||||
-- Rename requirements -> leads
|
||||
ALTER TABLE requirements RENAME TO leads;
|
||||
|
||||
-- Rename coupon_uses -> coupon_redemptions
|
||||
ALTER TABLE coupon_uses RENAME TO coupon_redemptions;
|
||||
|
||||
-- ============================================================================
|
||||
-- PHASE 7: Create New Domain Tables
|
||||
-- ============================================================================
|
||||
|
||||
-- 7.1 verification_requests
|
||||
CREATE TABLE IF NOT EXISTS verification_requests (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_role_profile_id UUID REFERENCES user_role_profiles(id),
|
||||
verification_type VARCHAR(50) NOT NULL DEFAULT 'IDENTITY',
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
|
||||
submitted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
reviewed_at TIMESTAMPTZ,
|
||||
reviewed_by_user_id UUID REFERENCES users(id),
|
||||
remarks TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 7.2 verification_documents
|
||||
CREATE TABLE IF NOT EXISTS verification_documents (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
verification_request_id UUID NOT NULL REFERENCES verification_requests(id) ON DELETE CASCADE,
|
||||
document_type VARCHAR(100) NOT NULL,
|
||||
file_url TEXT NOT NULL,
|
||||
file_name TEXT,
|
||||
mime_type TEXT,
|
||||
status VARCHAR(50) DEFAULT 'PENDING',
|
||||
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
reviewed_at TIMESTAMPTZ,
|
||||
reviewed_by_user_id UUID REFERENCES users(id),
|
||||
remarks TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 7.3 verification_logs
|
||||
CREATE TABLE IF NOT EXISTS verification_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
verification_request_id UUID NOT NULL REFERENCES verification_requests(id),
|
||||
action VARCHAR(50) NOT NULL,
|
||||
old_status VARCHAR(50),
|
||||
new_status VARCHAR(50),
|
||||
acted_by_user_id UUID REFERENCES users(id),
|
||||
remarks TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 7.4 approval_requests
|
||||
CREATE TABLE IF NOT EXISTS approval_requests (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
entity_type VARCHAR(50) NOT NULL,
|
||||
entity_id UUID NOT NULL,
|
||||
approval_type VARCHAR(50),
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
|
||||
submitted_by_user_id UUID REFERENCES users(id),
|
||||
reviewed_by_user_id UUID REFERENCES users(id),
|
||||
submitted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
reviewed_at TIMESTAMPTZ,
|
||||
remarks TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 7.5 approval_logs
|
||||
CREATE TABLE IF NOT EXISTS approval_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
approval_request_id UUID NOT NULL REFERENCES approval_requests(id),
|
||||
action VARCHAR(50) NOT NULL,
|
||||
old_status VARCHAR(50),
|
||||
new_status VARCHAR(50),
|
||||
acted_by_user_id UUID REFERENCES users(id),
|
||||
remarks TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 7.6 audit_logs
|
||||
CREATE TABLE IF NOT EXISTS audit_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
actor_user_id UUID REFERENCES users(id),
|
||||
actor_employee_id UUID,
|
||||
actor_type VARCHAR(50),
|
||||
action VARCHAR(100) NOT NULL,
|
||||
entity_type VARCHAR(100),
|
||||
entity_id UUID,
|
||||
entity_label TEXT,
|
||||
module_key VARCHAR(100),
|
||||
source_type VARCHAR(50),
|
||||
source_id UUID,
|
||||
request_id UUID,
|
||||
correlation_id UUID,
|
||||
ip_address TEXT,
|
||||
user_agent TEXT,
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'SUCCESS',
|
||||
summary TEXT,
|
||||
metadata_json JSONB,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 7.7 audit_log_changes
|
||||
CREATE TABLE IF NOT EXISTS audit_log_changes (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
audit_log_id UUID NOT NULL REFERENCES audit_logs(id) ON DELETE CASCADE,
|
||||
field_name TEXT NOT NULL,
|
||||
old_value_text TEXT,
|
||||
new_value_text TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_logs_actor ON audit_logs(actor_user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_logs_entity ON audit_logs(entity_type, entity_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_logs_module ON audit_logs(module_key);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_logs_created ON audit_logs(created_at);
|
||||
|
||||
-- 7.8 orders
|
||||
CREATE TABLE IF NOT EXISTS orders (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id),
|
||||
order_type VARCHAR(50) NOT NULL DEFAULT 'PACKAGE',
|
||||
subtotal_inr INTEGER NOT NULL DEFAULT 0,
|
||||
discount_inr INTEGER NOT NULL DEFAULT 0,
|
||||
tax_inr INTEGER NOT NULL DEFAULT 0,
|
||||
total_inr INTEGER NOT NULL DEFAULT 0,
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 7.9 order_items
|
||||
CREATE TABLE IF NOT EXISTS order_items (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
|
||||
item_type VARCHAR(50) NOT NULL,
|
||||
item_id UUID,
|
||||
item_name TEXT NOT NULL,
|
||||
quantity INTEGER NOT NULL DEFAULT 1,
|
||||
unit_price_inr INTEGER NOT NULL,
|
||||
total_price_inr INTEGER NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 7.10 payment_gateway_configs
|
||||
CREATE TABLE IF NOT EXISTS payment_gateway_configs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
gateway_key VARCHAR(50) NOT NULL,
|
||||
display_name VARCHAR(255),
|
||||
config_json JSONB,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 7.11 payment_transactions
|
||||
CREATE TABLE IF NOT EXISTS payment_transactions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
payment_id UUID NOT NULL REFERENCES payments(id),
|
||||
transaction_type VARCHAR(50) NOT NULL,
|
||||
provider_reference TEXT,
|
||||
request_payload_json JSONB,
|
||||
response_payload_json JSONB,
|
||||
status VARCHAR(50) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 7.12 tax_rules
|
||||
CREATE TABLE IF NOT EXISTS tax_rules (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
tax_type VARCHAR(50) NOT NULL,
|
||||
tax_rate DECIMAL(5,2) NOT NULL,
|
||||
applies_to VARCHAR(50),
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 7.13 kb_sections
|
||||
CREATE TABLE IF NOT EXISTS kb_sections (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
category_id UUID NOT NULL REFERENCES kb_categories(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
slug VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
display_order INTEGER DEFAULT 0,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 7.14 kb_article_feedback
|
||||
CREATE TABLE IF NOT EXISTS kb_article_feedback (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
article_id UUID NOT NULL REFERENCES kb_articles(id) ON DELETE CASCADE,
|
||||
user_id UUID REFERENCES users(id),
|
||||
is_helpful BOOLEAN,
|
||||
feedback_text TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 7.15 notification_templates
|
||||
CREATE TABLE IF NOT EXISTS notification_templates (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
template_key VARCHAR(100) NOT NULL UNIQUE,
|
||||
channel VARCHAR(50) NOT NULL DEFAULT 'EMAIL',
|
||||
title_template TEXT,
|
||||
body_template TEXT,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 7.16 smtp_configs
|
||||
CREATE TABLE IF NOT EXISTS smtp_configs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
provider_name VARCHAR(100),
|
||||
host VARCHAR(255),
|
||||
port INTEGER,
|
||||
username TEXT,
|
||||
encryption_mode VARCHAR(20),
|
||||
from_name VARCHAR(255),
|
||||
from_email VARCHAR(255),
|
||||
is_default BOOLEAN DEFAULT false,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 7.17 dashboard_widgets
|
||||
CREATE TABLE IF NOT EXISTS dashboard_widgets (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
dashboard_config_id UUID NOT NULL REFERENCES dashboard_configs(id) ON DELETE CASCADE,
|
||||
widget_key VARCHAR(100) NOT NULL,
|
||||
widget_title VARCHAR(255),
|
||||
config_json JSONB,
|
||||
display_order INTEGER DEFAULT 0,
|
||||
width_units INTEGER DEFAULT 1,
|
||||
height_units INTEGER DEFAULT 1,
|
||||
is_visible BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- PHASE 8: Update Existing Tables
|
||||
-- ============================================================================
|
||||
|
||||
-- Update users table
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS account_type TEXT DEFAULT 'INDIVIDUAL';
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS last_login_at TIMESTAMPTZ;
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
|
||||
UPDATE users SET updated_at = COALESCE(updated_at, created_at, NOW()) WHERE updated_at IS NULL;
|
||||
|
||||
-- Update roles table
|
||||
ALTER TABLE roles ADD COLUMN IF NOT EXISTS description TEXT;
|
||||
ALTER TABLE roles ADD COLUMN IF NOT EXISTS can_approve_requests BOOLEAN DEFAULT false;
|
||||
ALTER TABLE roles ADD COLUMN IF NOT EXISTS can_manage_system_settings BOOLEAN DEFAULT false;
|
||||
|
||||
-- Update departments table
|
||||
ALTER TABLE departments ADD COLUMN IF NOT EXISTS code VARCHAR(64);
|
||||
ALTER TABLE departments ADD COLUMN IF NOT EXISTS description TEXT;
|
||||
ALTER TABLE departments ADD COLUMN IF NOT EXISTS department_head VARCHAR(255);
|
||||
ALTER TABLE departments ADD COLUMN IF NOT EXISTS department_email VARCHAR(255);
|
||||
ALTER TABLE departments ADD COLUMN IF NOT EXISTS visibility VARCHAR(20) DEFAULT 'INTERNAL';
|
||||
ALTER TABLE departments ADD COLUMN IF NOT EXISTS transfers_enabled BOOLEAN DEFAULT false;
|
||||
ALTER TABLE departments ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
|
||||
UPDATE departments SET updated_at = COALESCE(updated_at, created_at, NOW()) WHERE updated_at IS NULL;
|
||||
|
||||
-- Update designations table
|
||||
ALTER TABLE designations ADD COLUMN IF NOT EXISTS code VARCHAR(64);
|
||||
ALTER TABLE designations ADD COLUMN IF NOT EXISTS department_id UUID REFERENCES departments(id);
|
||||
ALTER TABLE designations ADD COLUMN IF NOT EXISTS description TEXT;
|
||||
ALTER TABLE designations ADD COLUMN IF NOT EXISTS level VARCHAR(100);
|
||||
ALTER TABLE designations ADD COLUMN IF NOT EXISTS can_manage_team BOOLEAN DEFAULT false;
|
||||
ALTER TABLE designations ADD COLUMN IF NOT EXISTS can_approve BOOLEAN DEFAULT false;
|
||||
ALTER TABLE designations ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT true;
|
||||
ALTER TABLE designations ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
|
||||
UPDATE designations SET updated_at = COALESCE(updated_at, created_at, NOW()) WHERE updated_at IS NULL;
|
||||
|
||||
-- Update employees table
|
||||
ALTER TABLE employees ADD COLUMN IF NOT EXISTS joining_date DATE;
|
||||
ALTER TABLE employees ADD COLUMN IF NOT EXISTS employment_status VARCHAR(50) DEFAULT 'ACTIVE';
|
||||
ALTER TABLE employees ADD COLUMN IF NOT EXISTS manager_employee_id UUID REFERENCES employees(id);
|
||||
ALTER TABLE employees ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
|
||||
UPDATE employees SET updated_at = COALESCE(updated_at, created_at, NOW()) WHERE updated_at IS NULL;
|
||||
|
||||
-- Update lead_requests table
|
||||
ALTER TABLE lead_requests ADD COLUMN IF NOT EXISTS user_role_profile_id UUID;
|
||||
UPDATE lead_requests lr SET user_role_profile_id = urp.id
|
||||
FROM user_role_profiles urp
|
||||
WHERE lr.professional_id IS NOT NULL
|
||||
AND urp.user_id = (SELECT user_id FROM professionals p WHERE p.id = lr.professional_id);
|
||||
ALTER TABLE lead_requests ADD COLUMN IF NOT EXISTS remarks TEXT;
|
||||
ALTER TABLE lead_requests ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
|
||||
|
||||
-- Update job_applications table
|
||||
ALTER TABLE job_applications ADD COLUMN IF NOT EXISTS applicant_user_id UUID;
|
||||
UPDATE job_applications ja SET applicant_user_id = (
|
||||
SELECT user_id FROM job_seeker_profiles jsp WHERE jsp.id = ja.job_seeker_id
|
||||
);
|
||||
ALTER TABLE job_applications ADD COLUMN IF NOT EXISTS cover_note TEXT;
|
||||
ALTER TABLE job_applications DROP COLUMN IF EXISTS job_seeker_id;
|
||||
ALTER TABLE job_applications DROP COLUMN IF EXISTS cover_letter;
|
||||
ALTER TABLE job_applications DROP COLUMN IF EXISTS resume_url;
|
||||
ALTER TABLE job_applications DROP COLUMN IF EXISTS contact_viewed;
|
||||
|
||||
-- Update leads table (formerly requirements)
|
||||
ALTER TABLE leads ADD COLUMN IF NOT EXISTS created_by_user_id UUID;
|
||||
UPDATE leads l SET created_by_user_id = (
|
||||
SELECT user_id FROM customer_profiles cp WHERE cp.id = l.customer_id
|
||||
);
|
||||
ALTER TABLE leads ADD COLUMN IF NOT EXISTS required_date DATE;
|
||||
ALTER TABLE leads ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
|
||||
ALTER TABLE leads DROP COLUMN IF EXISTS customer_id;
|
||||
|
||||
-- Update jobs table
|
||||
ALTER TABLE jobs ADD COLUMN IF NOT EXISTS posted_by_user_id UUID;
|
||||
ALTER TABLE jobs ADD COLUMN IF NOT EXISTS mode_of_work VARCHAR(50);
|
||||
ALTER TABLE jobs ADD COLUMN IF NOT EXISTS budget_inr INTEGER;
|
||||
ALTER TABLE jobs ADD COLUMN IF NOT EXISTS salary_range_json JSONB;
|
||||
ALTER TABLE jobs ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
|
||||
UPDATE jobs SET updated_at = COALESCE(updated_at, created_at, NOW()) WHERE updated_at IS NULL;
|
||||
|
||||
-- Update tracecoin_wallets
|
||||
ALTER TABLE tracecoin_wallets RENAME COLUMN balance TO current_balance;
|
||||
ALTER TABLE tracecoin_wallets ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
|
||||
|
||||
-- Update tracecoin_ledger
|
||||
ALTER TABLE tracecoin_ledger ADD COLUMN IF NOT EXISTS balance_after INTEGER;
|
||||
ALTER TABLE tracecoin_ledger ADD COLUMN IF NOT EXISTS remarks TEXT;
|
||||
ALTER TABLE tracecoin_ledger RENAME COLUMN type TO transaction_type;
|
||||
ALTER TABLE tracecoin_ledger RENAME COLUMN reason TO reference_type;
|
||||
|
||||
-- Update coupons
|
||||
ALTER TABLE coupons ADD COLUMN IF NOT EXISTS max_discount_inr INTEGER;
|
||||
ALTER TABLE coupons ADD COLUMN IF NOT EXISTS min_order_value_inr INTEGER DEFAULT 0;
|
||||
ALTER TABLE coupons ADD COLUMN IF NOT EXISTS valid_from TIMESTAMPTZ DEFAULT NOW();
|
||||
ALTER TABLE coupons ADD COLUMN IF NOT EXISTS valid_to TIMESTAMPTZ;
|
||||
|
||||
-- Update coupon_redemptions
|
||||
ALTER TABLE coupon_redemptions ADD COLUMN IF NOT EXISTS order_id UUID REFERENCES orders(id);
|
||||
ALTER TABLE coupon_redemptions ADD COLUMN IF NOT EXISTS discount_amount_inr INTEGER;
|
||||
ALTER TABLE coupon_redemptions RENAME COLUMN used_at TO redeemed_at;
|
||||
|
||||
-- Update invoices
|
||||
ALTER TABLE invoices ADD COLUMN IF NOT EXISTS order_id UUID REFERENCES orders(id);
|
||||
ALTER TABLE invoices ADD COLUMN IF NOT EXISTS discount_inr INTEGER DEFAULT 0;
|
||||
ALTER TABLE invoices ADD COLUMN IF NOT EXISTS due_at TIMESTAMPTZ;
|
||||
ALTER TABLE invoices ADD COLUMN IF NOT EXISTS paid_at TIMESTAMPTZ;
|
||||
|
||||
-- Update payments
|
||||
ALTER TABLE payments ADD COLUMN IF NOT EXISTS payment_gateway_config_id UUID REFERENCES payment_gateway_configs(id);
|
||||
ALTER TABLE payments ADD COLUMN IF NOT EXISTS payment_method VARCHAR(50);
|
||||
ALTER TABLE payments ADD COLUMN IF NOT EXISTS currency_code VARCHAR(10) DEFAULT 'INR';
|
||||
ALTER TABLE payments ADD COLUMN IF NOT EXISTS initiated_at TIMESTAMPTZ DEFAULT NOW();
|
||||
ALTER TABLE payments ADD COLUMN IF NOT EXISTS completed_at TIMESTAMPTZ;
|
||||
|
||||
-- Update kb_articles
|
||||
ALTER TABLE kb_articles ADD COLUMN IF NOT EXISTS section_id UUID REFERENCES kb_sections(id);
|
||||
ALTER TABLE kb_articles ADD COLUMN IF NOT EXISTS article_type VARCHAR(50) DEFAULT 'HOW_TO';
|
||||
ALTER TABLE kb_articles ADD COLUMN IF NOT EXISTS audience_type VARCHAR(50) DEFAULT 'ALL';
|
||||
ALTER TABLE kb_articles RENAME COLUMN body TO content_markdown;
|
||||
ALTER TABLE kb_articles RENAME COLUMN created_by TO author_user_id;
|
||||
|
||||
-- Update support_tickets
|
||||
ALTER TABLE support_tickets ADD COLUMN IF NOT EXISTS created_by_user_id UUID;
|
||||
UPDATE support_tickets SET created_by_user_id = user_id;
|
||||
ALTER TABLE support_tickets RENAME COLUMN assigned_to TO assigned_to_user_id;
|
||||
ALTER TABLE support_tickets ADD COLUMN IF NOT EXISTS related_entity_type VARCHAR(50);
|
||||
ALTER TABLE support_tickets ADD COLUMN IF NOT EXISTS related_entity_id UUID;
|
||||
ALTER TABLE support_tickets ADD COLUMN IF NOT EXISTS closed_at TIMESTAMPTZ;
|
||||
|
||||
-- Update support_ticket_messages
|
||||
ALTER TABLE support_ticket_messages ADD COLUMN IF NOT EXISTS sender_user_id UUID;
|
||||
UPDATE support_ticket_messages SET sender_user_id = sender_id;
|
||||
ALTER TABLE support_ticket_messages RENAME COLUMN body TO message_body;
|
||||
ALTER TABLE support_ticket_messages ADD COLUMN IF NOT EXISTS attachment_url TEXT;
|
||||
|
||||
-- Update notifications
|
||||
ALTER TABLE notifications ADD COLUMN IF NOT EXISTS channel VARCHAR(50) DEFAULT 'IN_APP';
|
||||
ALTER TABLE notifications ADD COLUMN IF NOT EXISTS related_entity_type VARCHAR(50);
|
||||
ALTER TABLE notifications RENAME COLUMN reference_id TO related_entity_id;
|
||||
|
||||
-- Update reviews
|
||||
ALTER TABLE reviews ADD COLUMN IF NOT EXISTS entity_type VARCHAR(50) DEFAULT 'professional';
|
||||
ALTER TABLE reviews RENAME COLUMN customer_id TO reviewer_user_id;
|
||||
ALTER TABLE reviews ADD COLUMN IF NOT EXISTS status VARCHAR(50) DEFAULT 'PUBLISHED';
|
||||
|
||||
-- ============================================================================
|
||||
-- PHASE 9: Drop Deprecated Tables
|
||||
-- ============================================================================
|
||||
|
||||
DROP TABLE IF EXISTS professionals CASCADE;
|
||||
DROP TABLE IF EXISTS onboarding_submissions CASCADE;
|
||||
DROP TABLE IF EXISTS onboarding_configs CASCADE;
|
||||
DROP TABLE IF EXISTS onboarding_states CASCADE;
|
||||
DROP TABLE IF EXISTS submission_documents CASCADE;
|
||||
|
||||
-- ============================================================================
|
||||
-- PHASE 10: Drop Deprecated Columns from Extension Tables
|
||||
-- ============================================================================
|
||||
|
||||
-- Drop old user_id columns from extension tables (AFTER backfilling user_role_profile_id)
|
||||
ALTER TABLE photographer_profiles DROP COLUMN IF EXISTS user_id;
|
||||
ALTER TABLE tutor_profiles DROP COLUMN IF EXISTS user_id;
|
||||
ALTER TABLE makeup_artist_profiles DROP COLUMN IF EXISTS user_id;
|
||||
ALTER TABLE developer_profiles DROP COLUMN IF EXISTS user_id;
|
||||
ALTER TABLE video_editor_profiles DROP COLUMN IF EXISTS user_id;
|
||||
ALTER TABLE graphic_designer_profiles DROP COLUMN IF EXISTS user_id;
|
||||
ALTER TABLE social_media_manager_profiles DROP COLUMN IF EXISTS user_id;
|
||||
ALTER TABLE fitness_trainer_profiles DROP COLUMN IF EXISTS user_id;
|
||||
ALTER TABLE catering_service_profiles DROP COLUMN IF EXISTS user_id;
|
||||
ALTER TABLE ugc_content_creator_profiles DROP COLUMN IF EXISTS user_id;
|
||||
|
||||
-- Drop old columns from portfolio_items
|
||||
ALTER TABLE portfolio_items DROP COLUMN IF EXISTS professional_id;
|
||||
ALTER TABLE portfolio_items DROP COLUMN IF EXISTS user_id;
|
||||
ALTER TABLE portfolio_items DROP COLUMN IF EXISTS profession_key;
|
||||
|
||||
-- Drop old columns from services
|
||||
ALTER TABLE services DROP COLUMN IF EXISTS professional_id;
|
||||
ALTER TABLE services DROP COLUMN IF EXISTS user_id;
|
||||
ALTER TABLE services DROP COLUMN IF EXISTS profession_key;
|
||||
|
||||
-- Drop old columns from lead_requests
|
||||
ALTER TABLE lead_requests DROP COLUMN IF EXISTS professional_id;
|
||||
ALTER TABLE lead_requests DROP COLUMN IF EXISTS requirement_id;
|
||||
|
||||
-- Drop old custom_data columns
|
||||
ALTER TABLE photographer_profiles DROP COLUMN IF EXISTS custom_data;
|
||||
ALTER TABLE tutor_profiles DROP COLUMN IF EXISTS custom_data;
|
||||
ALTER TABLE makeup_artist_profiles DROP COLUMN IF EXISTS custom_data;
|
||||
ALTER TABLE developer_profiles DROP COLUMN IF EXISTS custom_data;
|
||||
ALTER TABLE video_editor_profiles DROP COLUMN IF EXISTS custom_data;
|
||||
ALTER TABLE graphic_designer_profiles DROP COLUMN IF EXISTS custom_data;
|
||||
ALTER TABLE social_media_manager_profiles DROP COLUMN IF EXISTS custom_data;
|
||||
ALTER TABLE fitness_trainer_profiles DROP COLUMN IF EXISTS custom_data;
|
||||
ALTER TABLE catering_service_profiles DROP COLUMN IF EXISTS custom_data;
|
||||
ALTER TABLE ugc_content_creator_profiles DROP COLUMN IF EXISTS custom_data;
|
||||
|
||||
-- Drop old profile columns that are now in user_role_profiles
|
||||
ALTER TABLE photographer_profiles DROP COLUMN IF EXISTS display_name;
|
||||
ALTER TABLE photographer_profiles DROP COLUMN IF EXISTS bio;
|
||||
ALTER TABLE photographer_profiles DROP COLUMN IF EXISTS location;
|
||||
ALTER TABLE photographer_profiles DROP COLUMN IF EXISTS status;
|
||||
ALTER TABLE photographer_profiles DROP COLUMN IF EXISTS rejection_reason;
|
||||
ALTER TABLE photographer_profiles DROP COLUMN IF EXISTS approved_at;
|
||||
|
||||
ALTER TABLE tutor_profiles DROP COLUMN IF EXISTS display_name;
|
||||
ALTER TABLE tutor_profiles DROP COLUMN IF EXISTS bio;
|
||||
ALTER TABLE tutor_profiles DROP COLUMN IF EXISTS location;
|
||||
ALTER TABLE tutor_profiles DROP COLUMN IF EXISTS status;
|
||||
ALTER TABLE tutor_profiles DROP COLUMN IF EXISTS rejection_reason;
|
||||
ALTER TABLE tutor_profiles DROP COLUMN IF EXISTS approved_at;
|
||||
|
||||
ALTER TABLE makeup_artist_profiles DROP COLUMN IF EXISTS display_name;
|
||||
ALTER TABLE makeup_artist_profiles DROP COLUMN IF EXISTS bio;
|
||||
ALTER TABLE makeup_artist_profiles DROP COLUMN IF EXISTS location;
|
||||
ALTER TABLE makeup_artist_profiles DROP COLUMN IF EXISTS status;
|
||||
ALTER TABLE makeup_artist_profiles DROP COLUMN IF EXISTS rejection_reason;
|
||||
ALTER TABLE makeup_artist_profiles DROP COLUMN IF EXISTS approved_at;
|
||||
|
||||
ALTER TABLE developer_profiles DROP COLUMN IF EXISTS display_name;
|
||||
ALTER TABLE developer_profiles DROP COLUMN IF EXISTS bio;
|
||||
ALTER TABLE developer_profiles DROP COLUMN IF EXISTS location;
|
||||
ALTER TABLE developer_profiles DROP COLUMN IF EXISTS status;
|
||||
ALTER TABLE developer_profiles DROP COLUMN IF EXISTS rejection_reason;
|
||||
ALTER TABLE developer_profiles DROP COLUMN IF EXISTS approved_at;
|
||||
|
||||
ALTER TABLE video_editor_profiles DROP COLUMN IF EXISTS display_name;
|
||||
ALTER TABLE video_editor_profiles DROP COLUMN IF EXISTS bio;
|
||||
ALTER TABLE video_editor_profiles DROP COLUMN IF EXISTS location;
|
||||
ALTER TABLE video_editor_profiles DROP COLUMN IF EXISTS status;
|
||||
ALTER TABLE video_editor_profiles DROP COLUMN IF EXISTS rejection_reason;
|
||||
ALTER TABLE video_editor_profiles DROP COLUMN IF EXISTS approved_at;
|
||||
|
||||
ALTER TABLE graphic_designer_profiles DROP COLUMN IF EXISTS display_name;
|
||||
ALTER TABLE graphic_designer_profiles DROP COLUMN IF EXISTS bio;
|
||||
ALTER TABLE graphic_designer_profiles DROP COLUMN IF EXISTS location;
|
||||
ALTER TABLE graphic_designer_profiles DROP COLUMN IF EXISTS status;
|
||||
ALTER TABLE graphic_designer_profiles DROP COLUMN IF EXISTS rejection_reason;
|
||||
ALTER TABLE graphic_designer_profiles DROP COLUMN IF EXISTS approved_at;
|
||||
|
||||
ALTER TABLE social_media_manager_profiles DROP COLUMN IF EXISTS display_name;
|
||||
ALTER TABLE social_media_manager_profiles DROP COLUMN IF EXISTS bio;
|
||||
ALTER TABLE social_media_manager_profiles DROP COLUMN IF EXISTS location;
|
||||
ALTER TABLE social_media_manager_profiles DROP COLUMN IF EXISTS status;
|
||||
ALTER TABLE social_media_manager_profiles DROP COLUMN IF EXISTS rejection_reason;
|
||||
ALTER TABLE social_media_manager_profiles DROP COLUMN IF EXISTS approved_at;
|
||||
|
||||
ALTER TABLE fitness_trainer_profiles DROP COLUMN IF EXISTS display_name;
|
||||
ALTER TABLE fitness_trainer_profiles DROP COLUMN IF EXISTS bio;
|
||||
ALTER TABLE fitness_trainer_profiles DROP COLUMN IF EXISTS location;
|
||||
ALTER TABLE fitness_trainer_profiles DROP COLUMN IF EXISTS status;
|
||||
ALTER TABLE fitness_trainer_profiles DROP COLUMN IF EXISTS rejection_reason;
|
||||
ALTER TABLE fitness_trainer_profiles DROP COLUMN IF EXISTS approved_at;
|
||||
|
||||
ALTER TABLE catering_service_profiles DROP COLUMN IF EXISTS bio;
|
||||
ALTER TABLE catering_service_profiles DROP COLUMN IF EXISTS location;
|
||||
ALTER TABLE catering_service_profiles DROP COLUMN IF EXISTS status;
|
||||
ALTER TABLE catering_service_profiles DROP COLUMN IF EXISTS rejection_reason;
|
||||
ALTER TABLE catering_service_profiles DROP COLUMN IF EXISTS approved_at;
|
||||
|
||||
ALTER TABLE ugc_content_creator_profiles DROP COLUMN IF EXISTS display_name;
|
||||
ALTER TABLE ugc_content_creator_profiles DROP COLUMN IF EXISTS bio;
|
||||
ALTER TABLE ugc_content_creator_profiles DROP COLUMN IF EXISTS location;
|
||||
ALTER TABLE ugc_content_creator_profiles DROP COLUMN IF EXISTS status;
|
||||
ALTER TABLE ugc_content_creator_profiles DROP COLUMN IF EXISTS rejection_reason;
|
||||
ALTER TABLE ugc_content_creator_profiles DROP COLUMN IF EXISTS approved_at;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- ============================================================================
|
||||
-- Migration Complete
|
||||
-- ============================================================================
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
-- Rollback: Drop user_sessions table
|
||||
DROP TABLE IF EXISTS user_sessions;
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
-- Phase 1.1: Create user_sessions table
|
||||
-- Migration: 20260415000001
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_sessions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
session_token TEXT UNIQUE NOT NULL,
|
||||
ip_address TEXT,
|
||||
user_agent TEXT,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_sessions_user_id ON user_sessions(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_sessions_token ON user_sessions(session_token);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_sessions_expires ON user_sessions(expires_at);
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
-- Rollback: Drop user_role_profiles table
|
||||
-- WARNING: This will fail if data exists and FK constraints are in place
|
||||
DROP TABLE IF EXISTS user_role_profiles CASCADE;
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
-- Phase 2.1: Create user_role_profiles root table (CRITICAL)
|
||||
-- Migration: 20260415010001
|
||||
-- This is the ROOT table for all user role profiles
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_role_profiles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
role_key VARCHAR(50) NOT NULL,
|
||||
display_name TEXT,
|
||||
bio TEXT,
|
||||
location TEXT,
|
||||
avatar_url TEXT,
|
||||
phone TEXT,
|
||||
email TEXT,
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE',
|
||||
verification_status VARCHAR(50) DEFAULT 'PENDING',
|
||||
approval_status VARCHAR(50) DEFAULT 'PENDING',
|
||||
rejection_reason TEXT,
|
||||
approved_at TIMESTAMPTZ,
|
||||
verified_at TIMESTAMPTZ,
|
||||
is_profile_public BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(user_id, role_key)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_role_profiles_user_id ON user_role_profiles(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_role_profiles_role_key ON user_role_profiles(role_key);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_role_profiles_status ON user_role_profiles(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_role_profiles_verification ON user_role_profiles(verification_status);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_role_profiles_approval ON user_role_profiles(approval_status);
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
-- Rollback: Clear backfilled data (run before dropping user_role_profiles)
|
||||
DELETE FROM user_role_profiles WHERE created_at > '2024-04-15';
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
-- Rollback: Remove user_role_profile_id columns
|
||||
-- WARNING: This will fail if FK constraints exist
|
||||
ALTER TABLE photographer_profiles DROP COLUMN IF EXISTS user_role_profile_id;
|
||||
ALTER TABLE tutor_profiles DROP COLUMN IF EXISTS user_role_profile_id;
|
||||
ALTER TABLE makeup_artist_profiles DROP COLUMN IF EXISTS user_role_profile_id;
|
||||
ALTER TABLE developer_profiles DROP COLUMN IF EXISTS user_role_profile_id;
|
||||
ALTER TABLE video_editor_profiles DROP COLUMN IF EXISTS user_role_profile_id;
|
||||
ALTER TABLE graphic_designer_profiles DROP COLUMN IF EXISTS user_role_profile_id;
|
||||
ALTER TABLE social_media_manager_profiles DROP COLUMN IF EXISTS user_role_profile_id;
|
||||
ALTER TABLE fitness_trainer_profiles DROP COLUMN IF EXISTS user_role_profile_id;
|
||||
ALTER TABLE catering_service_profiles DROP COLUMN IF EXISTS user_role_profile_id;
|
||||
ALTER TABLE ugc_content_creator_profiles DROP COLUMN IF EXISTS user_role_profile_id;
|
||||
|
||||
DROP INDEX IF EXISTS idx_photographer_profiles_user_role;
|
||||
DROP INDEX IF EXISTS idx_tutor_profiles_user_role;
|
||||
DROP INDEX IF EXISTS idx_makeup_artist_profiles_user_role;
|
||||
DROP INDEX IF EXISTS idx_developer_profiles_user_role;
|
||||
DROP INDEX IF EXISTS idx_video_editor_profiles_user_role;
|
||||
DROP INDEX IF EXISTS idx_graphic_designer_profiles_user_role;
|
||||
DROP INDEX IF EXISTS idx_social_media_manager_profiles_user_role;
|
||||
DROP INDEX IF EXISTS idx_fitness_trainer_profiles_user_role;
|
||||
DROP INDEX IF EXISTS idx_catering_service_profiles_user_role;
|
||||
DROP INDEX IF EXISTS idx_ugc_content_creator_profiles_user_role;
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
-- Phase 2.3: Add user_role_profile_id to extension tables
|
||||
-- Migration: 20260415010003
|
||||
-- This links existing extension tables to the new user_role_profiles root
|
||||
|
||||
-- photographer_profiles
|
||||
ALTER TABLE photographer_profiles ADD COLUMN IF NOT EXISTS user_role_profile_id UUID;
|
||||
UPDATE photographer_profiles p SET user_role_profile_id = urp.id
|
||||
FROM user_role_profiles urp
|
||||
WHERE p.user_id = urp.user_id AND urp.role_key = 'photographer';
|
||||
ALTER TABLE photographer_profiles ALTER COLUMN user_role_profile_id SET NOT NULL;
|
||||
|
||||
-- tutor_profiles
|
||||
ALTER TABLE tutor_profiles ADD COLUMN IF NOT EXISTS user_role_profile_id UUID;
|
||||
UPDATE tutor_profiles p SET user_role_profile_id = urp.id
|
||||
FROM user_role_profiles urp
|
||||
WHERE p.user_id = urp.user_id AND urp.role_key = 'tutor';
|
||||
ALTER TABLE tutor_profiles ALTER COLUMN user_role_profile_id SET NOT NULL;
|
||||
|
||||
-- makeup_artist_profiles
|
||||
ALTER TABLE makeup_artist_profiles ADD COLUMN IF NOT EXISTS user_role_profile_id UUID;
|
||||
UPDATE makeup_artist_profiles p SET user_role_profile_id = urp.id
|
||||
FROM user_role_profiles urp
|
||||
WHERE p.user_id = urp.user_id AND urp.role_key = 'makeup_artist';
|
||||
ALTER TABLE makeup_artist_profiles ALTER COLUMN user_role_profile_id SET NOT NULL;
|
||||
|
||||
-- developer_profiles
|
||||
ALTER TABLE developer_profiles ADD COLUMN IF NOT EXISTS user_role_profile_id UUID;
|
||||
UPDATE developer_profiles p SET user_role_profile_id = urp.id
|
||||
FROM user_role_profiles urp
|
||||
WHERE p.user_id = urp.user_id AND urp.role_key = 'developer';
|
||||
ALTER TABLE developer_profiles ALTER COLUMN user_role_profile_id SET NOT NULL;
|
||||
|
||||
-- video_editor_profiles
|
||||
ALTER TABLE video_editor_profiles ADD COLUMN IF NOT EXISTS user_role_profile_id UUID;
|
||||
UPDATE video_editor_profiles p SET user_role_profile_id = urp.id
|
||||
FROM user_role_profiles urp
|
||||
WHERE p.user_id = urp.user_id AND urp.role_key = 'video_editor';
|
||||
ALTER TABLE video_editor_profiles ALTER COLUMN user_role_profile_id SET NOT NULL;
|
||||
|
||||
-- graphic_designer_profiles
|
||||
ALTER TABLE graphic_designer_profiles ADD COLUMN IF NOT EXISTS user_role_profile_id UUID;
|
||||
UPDATE graphic_designer_profiles p SET user_role_profile_id = urp.id
|
||||
FROM user_role_profiles urp
|
||||
WHERE p.user_id = urp.user_id AND urp.role_key = 'graphic_designer';
|
||||
ALTER TABLE graphic_designer_profiles ALTER COLUMN user_role_profile_id SET NOT NULL;
|
||||
|
||||
-- social_media_manager_profiles
|
||||
ALTER TABLE social_media_manager_profiles ADD COLUMN IF NOT EXISTS user_role_profile_id UUID;
|
||||
UPDATE social_media_manager_profiles p SET user_role_profile_id = urp.id
|
||||
FROM user_role_profiles urp
|
||||
WHERE p.user_id = urp.user_id AND urp.role_key = 'social_media_manager';
|
||||
ALTER TABLE social_media_manager_profiles ALTER COLUMN user_role_profile_id SET NOT NULL;
|
||||
|
||||
-- fitness_trainer_profiles
|
||||
ALTER TABLE fitness_trainer_profiles ADD COLUMN IF NOT EXISTS user_role_profile_id UUID;
|
||||
UPDATE fitness_trainer_profiles p SET user_role_profile_id = urp.id
|
||||
FROM user_role_profiles urp
|
||||
WHERE p.user_id = urp.user_id AND urp.role_key = 'fitness_trainer';
|
||||
ALTER TABLE fitness_trainer_profiles ALTER COLUMN user_role_profile_id SET NOT NULL;
|
||||
|
||||
-- catering_service_profiles
|
||||
ALTER TABLE catering_service_profiles ADD COLUMN IF NOT EXISTS user_role_profile_id UUID;
|
||||
UPDATE catering_service_profiles p SET user_role_profile_id = urp.id
|
||||
FROM user_role_profiles urp
|
||||
WHERE p.user_id = urp.user_id AND urp.role_key = 'catering_service';
|
||||
ALTER TABLE catering_service_profiles ALTER COLUMN user_role_profile_id SET NOT NULL;
|
||||
|
||||
-- ugc_content_creator_profiles
|
||||
ALTER TABLE ugc_content_creator_profiles ADD COLUMN IF NOT EXISTS user_role_profile_id UUID;
|
||||
UPDATE ugc_content_creator_profiles p SET user_role_profile_id = urp.id
|
||||
FROM user_role_profiles urp
|
||||
WHERE p.user_id = urp.user_id AND urp.role_key = 'ugc_content_creator';
|
||||
ALTER TABLE ugc_content_creator_profiles ALTER COLUMN user_role_profile_id SET NOT NULL;
|
||||
|
||||
-- Create indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_photographer_profiles_user_role ON photographer_profiles(user_role_profile_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tutor_profiles_user_role ON tutor_profiles(user_role_profile_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_makeup_artist_profiles_user_role ON makeup_artist_profiles(user_role_profile_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_developer_profiles_user_role ON developer_profiles(user_role_profile_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_video_editor_profiles_user_role ON video_editor_profiles(user_role_profile_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_graphic_designer_profiles_user_role ON graphic_designer_profiles(user_role_profile_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_social_media_manager_profiles_user_role ON social_media_manager_profiles(user_role_profile_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_fitness_trainer_profiles_user_role ON fitness_trainer_profiles(user_role_profile_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_catering_service_profiles_user_role ON catering_service_profiles(user_role_profile_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ugc_content_creator_profiles_user_role ON ugc_content_creator_profiles(user_role_profile_id);
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
-- Rollback: Cannot easily restore removed columns
|
||||
-- This migration is NOT easily reversible
|
||||
-- Only run after full backup and testing
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
-- Phase 2.4: Remove forbidden external portfolio links
|
||||
-- Migration: 20260415010004
|
||||
-- Per source of truth: NO external portfolio links allowed
|
||||
|
||||
-- Remove github_url, portfolio_url from developer_profiles
|
||||
ALTER TABLE developer_profiles DROP COLUMN IF EXISTS github_url;
|
||||
ALTER TABLE developer_profiles DROP COLUMN IF EXISTS portfolio_url;
|
||||
|
||||
-- Remove reel_url from video_editor_profiles
|
||||
ALTER TABLE video_editor_profiles DROP COLUMN IF EXISTS reel_url;
|
||||
|
||||
-- Remove portfolio_url from graphic_designer_profiles
|
||||
ALTER TABLE graphic_designer_profiles DROP COLUMN IF EXISTS portfolio_url;
|
||||
|
||||
-- Remove portfolio_url from photographer_profiles
|
||||
ALTER TABLE photographer_profiles DROP COLUMN IF EXISTS portfolio_url;
|
||||
|
||||
-- Remove custom_data from all extension tables (preserve as JSONB if needed)
|
||||
ALTER TABLE photographer_profiles DROP COLUMN IF EXISTS custom_data;
|
||||
ALTER TABLE tutor_profiles DROP COLUMN IF EXISTS custom_data;
|
||||
ALTER TABLE makeup_artist_profiles DROP COLUMN IF EXISTS custom_data;
|
||||
ALTER TABLE developer_profiles DROP COLUMN IF EXISTS custom_data;
|
||||
ALTER TABLE video_editor_profiles DROP COLUMN IF EXISTS custom_data;
|
||||
ALTER TABLE graphic_designer_profiles DROP COLUMN IF EXISTS custom_data;
|
||||
ALTER TABLE social_media_manager_profiles DROP COLUMN IF EXISTS custom_data;
|
||||
ALTER TABLE fitness_trainer_profiles DROP COLUMN IF EXISTS custom_data;
|
||||
ALTER TABLE catering_service_profiles DROP COLUMN IF EXISTS custom_data;
|
||||
ALTER TABLE ugc_content_creator_profiles DROP COLUMN IF EXISTS custom_data;
|
||||
|
||||
-- Rename inconsistent columns
|
||||
ALTER TABLE tutor_profiles RENAME COLUMN subjects_taught TO subjects;
|
||||
Loading…
Add table
Reference in a new issue