Fix backend compile errors after schema migrations

- employees.rs: rewrite for new standalone schema (email/password_hash,
  no user_id/role_id FK — matches 20260402030000 migration)
- migration: DROP old employees table before CREATE (old schema incompatible)
- pricing.rs: merge if-else sqlx::query! branches into single nullable param query
- kb.rs: fix target_roles Option<Vec<String>> unwrap, category_id Some() wrapping
- support.rs: fix .or() call with non-optional user_email (use Some())
- roles.rs: fix employees JOIN from role_id (deleted) to role_code

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ashwin Kumar 2026-04-03 02:25:47 +02:00
parent 73629fa935
commit 3935277fb7
6 changed files with 163 additions and 237 deletions

View file

@ -9,11 +9,10 @@ use axum::{
use contracts::auth_middleware::{AuthUser, require_admin}; use contracts::auth_middleware::{AuthUser, require_admin};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::types::Uuid; use sqlx::types::Uuid;
use sqlx::Row; use auth::crypto::hash_password;
pub fn router() -> Router<AppState> { pub fn router() -> Router<AppState> {
Router::new() Router::new()
.route("/provision", axum::routing::post(provision_employee))
.route("/", get(list_employees).post(create_employee)) .route("/", get(list_employees).post(create_employee))
.route("/{id}", get(get_employee).patch(update_employee).delete(delete_employee)) .route("/{id}", get(get_employee).patch(update_employee).delete(delete_employee))
} }
@ -21,27 +20,24 @@ pub fn router() -> Router<AppState> {
#[derive(Deserialize)] #[derive(Deserialize)]
struct ListQuery { struct ListQuery {
q: Option<String>, q: Option<String>,
status: Option<String>, // ACTIVE | INACTIVE | SUSPENDED status: Option<String>,
page: Option<i64>, page: Option<i64>,
per_page: Option<i64>, per_page: Option<i64>,
sort: Option<String>, // name_asc | name_desc | joined_asc | joined_desc
} }
#[derive(Serialize)] #[derive(Serialize)]
struct EmployeeRow { struct EmployeeRow {
id: Uuid, id: Uuid,
user_id: Uuid, first_name: String,
name: String, last_name: String,
email: String, email: String,
phone: Option<String>, employee_code: Option<String>,
employee_id: Option<String>,
department_name: Option<String>, department_name: Option<String>,
designation_name: Option<String>, designation_name: Option<String>,
role_name: Option<String>, role_code: String,
role_key: Option<String>,
joining_date: String,
status: String, status: String,
updated_at: Option<chrono::DateTime<chrono::Utc>>, joined_at: String,
created_at: String,
} }
#[derive(Serialize)] #[derive(Serialize)]
@ -65,82 +61,65 @@ async fn list_employees(
let offset = (page - 1) * per_page; let offset = (page - 1) * per_page;
let search = q.q.unwrap_or_default().to_lowercase(); let search = q.q.unwrap_or_default().to_lowercase();
let status = q.status.unwrap_or_default().to_uppercase(); let status = q.status.unwrap_or_default().to_uppercase();
let sort_sql = match q.sort.as_deref() {
Some("name_asc") => "u.full_name ASC", let rows = sqlx::query!(
Some("name_desc") => "u.full_name DESC", r#"
Some("joined_asc") => "e.created_at ASC",
_ => "e.created_at DESC",
};
let sql = format!(r#"
SELECT SELECT
e.id, e.id,
e.user_id, e.first_name,
e.last_name,
e.email,
e.employee_code, e.employee_code,
e.created_at AS joining_date, e.role_code,
u.full_name, e.status,
u.email, e.joined_at,
u.phone, e.created_at,
u.status AS user_status, d.name AS "department_name?",
r.name AS role_name, des.name AS "designation_name?"
r.key AS role_key,
d.name AS department_name,
des.name AS designation_name,
e.created_at AS updated_at
FROM employees e FROM employees e
JOIN users u ON u.id = e.user_id
LEFT JOIN roles r ON r.id = e.role_id
LEFT JOIN departments d ON d.id = e.department_id LEFT JOIN departments d ON d.id = e.department_id
LEFT JOIN designations des ON des.id = e.designation_id LEFT JOIN designations des ON des.id = e.designation_id
WHERE ($1 = '' OR LOWER(u.full_name) LIKE '%' || $1 || '%' OR LOWER(u.email) LIKE '%' || $1 || '%' OR LOWER(COALESCE(e.employee_code,'')) LIKE '%' || $1 || '%') WHERE ($1 = '' OR LOWER(e.first_name || ' ' || e.last_name) LIKE '%' || $1 || '%'
AND ( OR LOWER(e.email) LIKE '%' || $1 || '%'
$2 = '' OR LOWER(COALESCE(e.employee_code,'')) LIKE '%' || $1 || '%')
OR ($2 = 'ACTIVE' AND u.status = 'ACTIVE') AND ($2 = '' OR e.status = $2)
OR ($2 = 'INACTIVE' AND u.status = 'INACTIVE') ORDER BY e.created_at DESC
OR ($2 = 'SUSPENDED' AND u.status = 'SUSPENDED')
)
ORDER BY {sort}
LIMIT $3 OFFSET $4 LIMIT $3 OFFSET $4
"#, sort = sort_sql); "#,
let rows = sqlx::query(&sql) search,
.bind(&search) status,
.bind(&status) per_page,
.bind(per_page) offset
.bind(offset) )
.fetch_all(&state.pool) .fetch_all(&state.pool)
.await .await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
let employees = rows.into_iter().map(|row| { let employees = rows
let joining: chrono::DateTime<chrono::Utc> = row.get::<chrono::DateTime<chrono::Utc>, _>("joining_date"); .into_iter()
EmployeeRow { .map(|r| EmployeeRow {
id: row.get("id"), id: r.id,
user_id: row.get("user_id"), first_name: r.first_name,
name: row.get::<Option<String>, _>("full_name").unwrap_or_default(), last_name: r.last_name,
email: row.get("email"), email: r.email,
phone: row.get::<Option<String>, _>("phone"), employee_code: r.employee_code,
employee_id: row.get::<Option<String>, _>("employee_code"), department_name: r.department_name,
department_name: row.get::<Option<String>, _>("department_name"), designation_name: r.designation_name,
designation_name: row.get::<Option<String>, _>("designation_name"), role_code: r.role_code,
role_name: row.get::<Option<String>, _>("role_name"), status: r.status,
role_key: row.get::<Option<String>, _>("role_key"), joined_at: r.joined_at.to_string(),
joining_date: joining.to_rfc3339(), created_at: r.created_at.to_rfc3339(),
status: row.get::<Option<String>, _>("user_status").unwrap_or_else(|| "ACTIVE".to_string()), })
updated_at: Some(row.get::<chrono::DateTime<chrono::Utc>, _>("updated_at")), .collect::<Vec<_>>();
}
}).collect::<Vec<_>>();
let total: i64 = sqlx::query_scalar!( let total: i64 = sqlx::query_scalar!(
r#" r#"
SELECT COUNT(*) SELECT COUNT(*)
FROM employees e FROM employees e
JOIN users u ON u.id = e.user_id WHERE ($1 = '' OR LOWER(e.first_name || ' ' || e.last_name) LIKE '%' || $1 || '%'
WHERE ($1 = '' OR LOWER(u.full_name) LIKE '%' || $1 || '%' OR LOWER(u.email) LIKE '%' || $1 || '%' OR LOWER(COALESCE(e.employee_code,'')) LIKE '%' || $1 || '%') OR LOWER(e.email) LIKE '%' || $1 || '%'
AND ( OR LOWER(COALESCE(e.employee_code,'')) LIKE '%' || $1 || '%')
$2 = '' AND ($2 = '' OR e.status = $2)
OR ($2 = 'ACTIVE' AND u.status = 'ACTIVE')
OR ($2 = 'INACTIVE' AND u.status = 'INACTIVE')
OR ($2 = 'SUSPENDED' AND u.status = 'SUSPENDED')
)
"#, "#,
search, search,
status status
@ -161,52 +140,50 @@ async fn get_employee(
if let Err(_e) = require_admin(&auth) { if let Err(_e) = require_admin(&auth) {
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string())); return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
} }
let sql = r#" let r = sqlx::query!(
r#"
SELECT SELECT
e.id, e.user_id, e.employee_code, e.id, e.first_name, e.last_name, e.email, e.employee_code,
e.created_at AS joining_date, e.created_at AS updated_at, e.role_code, e.status, e.joined_at, e.created_at,
u.full_name, u.email, u.phone, u.status AS user_status, d.name AS "department_name?",
r.name AS role_name, r.key AS role_key, des.name AS "designation_name?"
d.name AS department_name,
des.name AS designation_name
FROM employees e FROM employees e
JOIN users u ON u.id = e.user_id
LEFT JOIN roles r ON r.id = e.role_id
LEFT JOIN departments d ON d.id = e.department_id LEFT JOIN departments d ON d.id = e.department_id
LEFT JOIN designations des ON des.id = e.designation_id LEFT JOIN designations des ON des.id = e.designation_id
WHERE e.id = $1 WHERE e.id = $1
"#; "#,
let row = sqlx::query(sql) id
.bind(id) )
.fetch_optional(&state.pool) .fetch_optional(&state.pool)
.await .await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))? .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?
.ok_or((StatusCode::NOT_FOUND, "Employee not found".to_string()))?; .ok_or((StatusCode::NOT_FOUND, "Employee not found".to_string()))?;
let joining: chrono::DateTime<chrono::Utc> = row.get::<chrono::DateTime<chrono::Utc>, _>("joining_date");
Ok(Json(EmployeeRow { Ok(Json(EmployeeRow {
id: row.get("id"), id: r.id,
user_id: row.get("user_id"), first_name: r.first_name,
name: row.get::<Option<String>, _>("full_name").unwrap_or_default(), last_name: r.last_name,
email: row.get("email"), email: r.email,
phone: row.get::<Option<String>, _>("phone"), employee_code: r.employee_code,
employee_id: row.get::<Option<String>, _>("employee_code"), department_name: r.department_name,
department_name: row.get::<Option<String>, _>("department_name"), designation_name: r.designation_name,
designation_name: row.get::<Option<String>, _>("designation_name"), role_code: r.role_code,
role_name: row.get::<Option<String>, _>("role_name"), status: r.status,
role_key: row.get::<Option<String>, _>("role_key"), joined_at: r.joined_at.to_string(),
joining_date: joining.to_rfc3339(), created_at: r.created_at.to_rfc3339(),
status: row.get::<Option<String>, _>("user_status").unwrap_or_else(|| "ACTIVE".to_string()),
updated_at: Some(row.get::<chrono::DateTime<chrono::Utc>, _>("updated_at")),
})) }))
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct CreateEmployeePayload { struct CreateEmployeePayload {
user_id: Uuid, first_name: String,
role_id: Uuid, last_name: String,
email: String,
password: String,
employee_code: Option<String>,
department_id: Option<Uuid>, department_id: Option<Uuid>,
designation_id: Option<Uuid>, designation_id: Option<Uuid>,
employee_code: Option<String>, role_code: Option<String>,
} }
async fn create_employee( async fn create_employee(
@ -217,103 +194,49 @@ async fn create_employee(
if let Err(_e) = require_admin(&auth) { if let Err(_e) = require_admin(&auth) {
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string())); return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
} }
let row = sqlx::query!( let password_hash = hash_password(&p.password)
r#" .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Crypto error: {e}")))?;
INSERT INTO employees (user_id, role_id, department_id, designation_id, employee_code) let role_code = p.role_code.unwrap_or_else(|| "STAFF".to_string());
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (user_id) DO UPDATE SET
role_id = EXCLUDED.role_id,
department_id = EXCLUDED.department_id,
designation_id = EXCLUDED.designation_id,
employee_code = EXCLUDED.employee_code
RETURNING id
"#,
p.user_id, p.role_id, p.department_id, p.designation_id, p.employee_code
)
.fetch_one(&state.pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
get_employee(auth, State(state), Path(row.id)).await
}
#[derive(Deserialize)]
struct ProvisionEmployeePayload {
email: String,
full_name: String,
role_id: Uuid,
department_id: Option<Uuid>,
designation_id: Option<Uuid>,
employee_code: Option<String>,
#[serde(default)]
generate_login: bool,
password: Option<String>,
}
async fn provision_employee(
auth: AuthUser,
State(state): State<AppState>,
Json(p): Json<ProvisionEmployeePayload>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
if let Err(_e) = require_admin(&auth) {
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
}
use db::models::user::{CreateUserPayload, UserRepository};
use auth::crypto::hash_password;
let email = p.email.trim().to_lowercase(); let email = p.email.trim().to_lowercase();
// 1. Resolve User let r = sqlx::query!(
let user_id = match UserRepository::get_by_email(&state.pool, &email).await {
Ok(user) => user.id,
Err(_) => {
if !p.generate_login {
return Err((StatusCode::BAD_REQUEST, "User not found for this email".to_string()));
}
let plain_password = p.password.unwrap_or_else(|| format!("{:08}", rand::random::<u32>() % 100_000_000));
if plain_password.len() < 8 {
return Err((StatusCode::BAD_REQUEST, "Password must be at least 8 characters".to_string()));
}
let password_hash = hash_password(&plain_password)
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Crypto error: {e}")))?;
let new_user = UserRepository::create(&state.pool, CreateUserPayload {
full_name: p.full_name.clone(),
email: email.clone(),
phone: None,
password_hash,
}).await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
new_user.id
}
};
// 2. Link Employee
let row = sqlx::query!(
r#" r#"
INSERT INTO employees (user_id, role_id, department_id, designation_id, employee_code) INSERT INTO employees (first_name, last_name, email, password_hash, employee_code,
VALUES ($1, $2, $3, $4, $5) department_id, designation_id, role_code)
ON CONFLICT (user_id) DO UPDATE SET VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
role_id = EXCLUDED.role_id,
department_id = EXCLUDED.department_id,
designation_id = EXCLUDED.designation_id,
employee_code = EXCLUDED.employee_code
RETURNING id RETURNING id
"#, "#,
user_id, p.role_id, p.department_id, p.designation_id, p.employee_code p.first_name,
p.last_name,
email,
password_hash,
p.employee_code,
p.department_id,
p.designation_id,
role_code,
) )
.fetch_one(&state.pool) .fetch_one(&state.pool)
.await .await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; .map_err(|e| {
if e.to_string().contains("unique") {
(StatusCode::CONFLICT, "Email already exists".to_string())
} else {
(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}"))
}
})?;
get_employee(auth, State(state), Path(row.id)).await get_employee(auth, State(state), Path(r.id)).await
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct UpdateEmployeePayload { struct UpdateEmployeePayload {
role_id: Option<Uuid>, first_name: Option<String>,
last_name: Option<String>,
employee_code: Option<String>,
department_id: Option<Uuid>, department_id: Option<Uuid>,
designation_id: Option<Uuid>, designation_id: Option<Uuid>,
employee_code: Option<String>, role_code: Option<String>,
status: Option<String>,
} }
async fn update_employee( async fn update_employee(
@ -329,17 +252,29 @@ async fn update_employee(
r#" r#"
UPDATE employees UPDATE employees
SET SET
role_id = COALESCE($1, role_id), first_name = COALESCE($1, first_name),
department_id = COALESCE($2, department_id), last_name = COALESCE($2, last_name),
designation_id = COALESCE($3, designation_id), employee_code = COALESCE($3, employee_code),
employee_code = COALESCE($4, employee_code) department_id = COALESCE($4, department_id),
WHERE id = $5 designation_id = COALESCE($5, designation_id),
role_code = COALESCE($6, role_code),
status = COALESCE($7, status),
updated_at = NOW()
WHERE id = $8
"#, "#,
p.role_id, p.department_id, p.designation_id, p.employee_code, id p.first_name,
p.last_name,
p.employee_code,
p.department_id,
p.designation_id,
p.role_code,
p.status,
id,
) )
.execute(&state.pool) .execute(&state.pool)
.await .await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
get_employee(auth, State(state), Path(id)).await get_employee(auth, State(state), Path(id)).await
} }

View file

@ -182,7 +182,7 @@ async fn public_list_articles(
let articles: Vec<_> = rows let articles: Vec<_> = rows
.into_iter() .into_iter()
.map(|r| { .map(|r| {
let role = derive_role(&r.target_roles); let role = derive_role(r.target_roles.as_deref().unwrap_or(&[]));
PublicArticleDto { PublicArticleDto {
id: r.id, id: r.id,
slug: r.slug, slug: r.slug,
@ -245,7 +245,7 @@ async fn public_get_article(
match row { match row {
Ok(Some(r)) => { Ok(Some(r)) => {
let role = derive_role(&r.target_roles); let role = derive_role(r.target_roles.as_deref().unwrap_or(&[]));
let dto = PublicArticleDto { let dto = PublicArticleDto {
id: r.id, id: r.id,
slug: r.slug, slug: r.slug,
@ -528,11 +528,11 @@ async fn admin_list_articles(
title: r.title, title: r.title,
slug: r.slug, slug: r.slug,
summary: r.summary, summary: r.summary,
category_id: r.category_id, category_id: Some(r.category_id),
category: Some(r.category_name), category: Some(r.category_name),
content: r.body, content: r.body,
status: if r.is_published { "PUBLISHED".into() } else { "DRAFT".into() }, status: if r.is_published { "PUBLISHED".into() } else { "DRAFT".into() },
target_roles: r.target_roles, target_roles: r.target_roles.unwrap_or_default(),
tags: r.tags, tags: r.tags,
views: r.views, views: r.views,
created_at: r.created_at.to_rfc3339(), created_at: r.created_at.to_rfc3339(),
@ -606,11 +606,11 @@ async fn admin_create_article(
title: r.title, title: r.title,
slug: r.slug, slug: r.slug,
summary: r.summary, summary: r.summary,
category_id: r.category_id, category_id: Some(r.category_id),
category: None, category: None,
content: r.body, content: r.body,
status: if r.is_published { "PUBLISHED".into() } else { "DRAFT".into() }, status: if r.is_published { "PUBLISHED".into() } else { "DRAFT".into() },
target_roles: r.target_roles, target_roles: r.target_roles.unwrap_or_default(),
tags: r.tags, tags: r.tags,
views: r.views, views: r.views,
created_at: r.created_at.to_rfc3339(), created_at: r.created_at.to_rfc3339(),
@ -668,11 +668,11 @@ async fn admin_get_article(
title: r.title, title: r.title,
slug: r.slug, slug: r.slug,
summary: r.summary, summary: r.summary,
category_id: r.category_id, category_id: Some(r.category_id),
category: Some(r.category_name), category: Some(r.category_name),
content: r.body, content: r.body,
status: if r.is_published { "PUBLISHED".into() } else { "DRAFT".into() }, status: if r.is_published { "PUBLISHED".into() } else { "DRAFT".into() },
target_roles: r.target_roles, target_roles: r.target_roles.unwrap_or_default(),
tags: r.tags, tags: r.tags,
views: r.views, views: r.views,
created_at: r.created_at.to_rfc3339(), created_at: r.created_at.to_rfc3339(),
@ -752,11 +752,11 @@ async fn admin_update_article(
title: r.title, title: r.title,
slug: r.slug, slug: r.slug,
summary: r.summary, summary: r.summary,
category_id: r.category_id, category_id: Some(r.category_id),
category: None, category: None,
content: r.body, content: r.body,
status: if r.is_published { "PUBLISHED".into() } else { "DRAFT".into() }, status: if r.is_published { "PUBLISHED".into() } else { "DRAFT".into() },
target_roles: r.target_roles, target_roles: r.target_roles.unwrap_or_default(),
tags: r.tags, tags: r.tags,
views: r.views, views: r.views,
created_at: r.created_at.to_rfc3339(), created_at: r.created_at.to_rfc3339(),

View file

@ -94,30 +94,18 @@ async fn public_list_packages(
State(state): State<AppState>, State(state): State<AppState>,
Query(params): Query<PackageQuery>, Query(params): Query<PackageQuery>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let rows = if let Some(role) = params.role { let rows = sqlx::query!(
sqlx::query!(
r#"
SELECT id, name, role_key, package_type, tracecoins_amount, price_inr, description, is_active
FROM pricing_packages
WHERE is_active = true AND (role_key = $1 OR role_key = 'ALL')
ORDER BY price_inr
"#,
role
)
.fetch_all(&state.pool)
.await
} else {
sqlx::query!(
r#" r#"
SELECT id, name, role_key, package_type, tracecoins_amount, price_inr, description, is_active SELECT id, name, role_key, package_type, tracecoins_amount, price_inr, description, is_active
FROM pricing_packages FROM pricing_packages
WHERE is_active = true WHERE is_active = true
AND ($1::text IS NULL OR role_key = $1 OR role_key = 'ALL')
ORDER BY role_key, price_inr ORDER BY role_key, price_inr
"# "#,
params.role as Option<String>
) )
.fetch_all(&state.pool) .fetch_all(&state.pool)
.await .await;
};
match rows { match rows {
Ok(rows) => { Ok(rows) => {

View file

@ -124,7 +124,7 @@ async fn list_roles(
COUNT(DISTINCT rp.id) AS permissions_count COUNT(DISTINCT rp.id) AS permissions_count
FROM roles r FROM roles r
LEFT JOIN departments d ON d.id = r.department_id LEFT JOIN departments d ON d.id = r.department_id
LEFT JOIN employees e ON e.role_id = r.id LEFT JOIN employees e ON e.role_code = r.key
LEFT JOIN role_permissions rp ON rp.role_id = r.id LEFT JOIN role_permissions rp ON rp.role_id = r.id
WHERE ($1 = '' OR r.audience = $1) WHERE ($1 = '' OR r.audience = $1)
AND ($2 = '' OR LOWER(r.name) LIKE '%' || $2 || '%' OR LOWER(r.key) LIKE '%' || $2 || '%') AND ($2 = '' OR LOWER(r.name) LIKE '%' || $2 || '%' OR LOWER(r.key) LIKE '%' || $2 || '%')

View file

@ -412,7 +412,7 @@ async fn admin_list_cases(
.map(|r| { .map(|r| {
// Use user info if available, fall back to requester fields // Use user info if available, fall back to requester fields
let requester_name = r.requester_name.or(r.user_name); let requester_name = r.requester_name.or(r.user_name);
let requester_email = r.requester_email.or(r.user_email); let requester_email = r.requester_email.or(Some(r.user_email));
serde_json::json!({ serde_json::json!({
"id": r.id, "id": r.id,
"title": r.subject, "title": r.subject,
@ -562,7 +562,7 @@ async fn admin_get_case(
.collect(); .collect();
let requester_name = t.requester_name.or(t.user_name); let requester_name = t.requester_name.or(t.user_name);
let requester_email = t.requester_email.or(t.user_email); let requester_email = t.requester_email.or(Some(t.user_email));
(StatusCode::OK, Json(serde_json::json!({ (StatusCode::OK, Json(serde_json::json!({
"ticket": { "ticket": {

View file

@ -1,7 +1,10 @@
-- UP: 20260402030000_strict_employee_separation.up.sql -- UP: 20260402030000_strict_employee_separation.up.sql
-- Drop old employees table (was linked to users — replacing with standalone auth)
DROP TABLE IF EXISTS employees CASCADE;
-- 1. EMPLOYEES (Standalone Table - Not Linked to 'users') -- 1. EMPLOYEES (Standalone Table - Not Linked to 'users')
CREATE TABLE IF NOT EXISTS employees ( CREATE TABLE employees (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
first_name VARCHAR(100) NOT NULL, first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL, last_name VARCHAR(100) NOT NULL,
@ -10,14 +13,14 @@ CREATE TABLE IF NOT EXISTS employees (
employee_code VARCHAR(50) UNIQUE, employee_code VARCHAR(50) UNIQUE,
department_id UUID REFERENCES departments(id) ON DELETE SET NULL, department_id UUID REFERENCES departments(id) ON DELETE SET NULL,
designation_id UUID REFERENCES designations(id) ON DELETE SET NULL, designation_id UUID REFERENCES designations(id) ON DELETE SET NULL,
role_code VARCHAR(50) NOT NULL DEFAULT 'STAFF', -- ADMIN, MANAGER, STAFF role_code VARCHAR(50) NOT NULL DEFAULT 'STAFF',
status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE', -- ACTIVE, INACTIVE, SUSPENDED status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE',
joined_at DATE NOT NULL DEFAULT CURRENT_DATE, joined_at DATE NOT NULL DEFAULT CURRENT_DATE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
); );
-- 4. EMPLOYEE SESSIONS (Standalone Auth) -- 2. EMPLOYEE SESSIONS (Standalone Auth)
CREATE TABLE IF NOT EXISTS employee_sessions ( CREATE TABLE IF NOT EXISTS employee_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
employee_id UUID NOT NULL REFERENCES employees(id) ON DELETE CASCADE, employee_id UUID NOT NULL REFERENCES employees(id) ON DELETE CASCADE,