feat(emails): complete email system with 35 branded templates and full wiring

- Add 35 branded HTML email templates with Nxtgauge styling

- Create email template engine with base template system

- Add email management API for admin panel

- Wire email triggers from all services

- All services compile successfully
This commit is contained in:
Ashwin Kumar 2026-04-10 04:49:39 +02:00
parent ff4e23d991
commit b4f714f43f
52 changed files with 3038 additions and 339 deletions

View file

@ -33,37 +33,38 @@ RAZORPAY_KEY_ID=rzp_test_...
RAZORPAY_KEY_SECRET=...
# ── Frontend ──────────────────────────────────────────────────────────────────
FRONTEND_URL=http://localhost:3000
FRONTEND_URL=http://localhost:9201
ADMIN_URL=http://localhost:9202
# ── Service Ports (local development, for running services individually) ──────
GATEWAY_PORT=8000
USERS_PORT=8080
COMPANIES_PORT=8081
JOB_SEEKERS_PORT=8082
CUSTOMERS_PORT=8083
PHOTOGRAPHERS_PORT=8085
MAKEUP_ARTISTS_PORT=8086
TUTORS_PORT=8087
DEVELOPERS_PORT=8088
VIDEO_EDITORS_PORT=8089
GRAPHIC_DESIGNERS_PORT=8090
SOCIAL_MEDIA_MANAGERS_PORT=8091
FITNESS_TRAINERS_PORT=8092
CATERING_SERVICES_PORT=8093
PAYMENTS_PORT=8094
GATEWAY_PORT=9100
USERS_PORT=9101
COMPANIES_PORT=9102
JOB_SEEKERS_PORT=9104
CUSTOMERS_PORT=9105
PHOTOGRAPHERS_PORT=9107
MAKEUP_ARTISTS_PORT=9109
TUTORS_PORT=9108
DEVELOPERS_PORT=9110
VIDEO_EDITORS_PORT=9111
GRAPHIC_DESIGNERS_PORT=9112
SOCIAL_MEDIA_MANAGERS_PORT=9113
FITNESS_TRAINERS_PORT=9114
CATERING_SERVICES_PORT=9115
PAYMENTS_PORT=9116
# ── Service URLs (used by gateway — override only for non-Docker dev) ─────────
USERS_SERVICE_URL=http://localhost:8080
COMPANIES_SERVICE_URL=http://localhost:8081
JOB_SEEKERS_SERVICE_URL=http://localhost:8082
CUSTOMERS_SERVICE_URL=http://localhost:8083
PHOTOGRAPHERS_SERVICE_URL=http://localhost:8085
MAKEUP_ARTISTS_SERVICE_URL=http://localhost:8086
TUTORS_SERVICE_URL=http://localhost:8087
DEVELOPERS_SERVICE_URL=http://localhost:8088
VIDEO_EDITORS_SERVICE_URL=http://localhost:8089
GRAPHIC_DESIGNERS_SERVICE_URL=http://localhost:8090
SOCIAL_MEDIA_MANAGERS_SERVICE_URL=http://localhost:8091
FITNESS_TRAINERS_SERVICE_URL=http://localhost:8092
CATERING_SERVICES_SERVICE_URL=http://localhost:8093
PAYMENTS_SERVICE_URL=http://localhost:8094
USERS_SERVICE_URL=http://localhost:9101
COMPANIES_SERVICE_URL=http://localhost:9102
JOB_SEEKERS_SERVICE_URL=http://localhost:9104
CUSTOMERS_SERVICE_URL=http://localhost:9105
PHOTOGRAPHERS_SERVICE_URL=http://localhost:9107
MAKEUP_ARTISTS_SERVICE_URL=http://localhost:9109
TUTORS_SERVICE_URL=http://localhost:9108
DEVELOPERS_SERVICE_URL=http://localhost:9110
VIDEO_EDITORS_SERVICE_URL=http://localhost:9111
GRAPHIC_DESIGNERS_SERVICE_URL=http://localhost:9112
SOCIAL_MEDIA_MANAGERS_SERVICE_URL=http://localhost:9113
FITNESS_TRAINERS_SERVICE_URL=http://localhost:9114
CATERING_SERVICES_SERVICE_URL=http://localhost:9115
PAYMENTS_SERVICE_URL=http://localhost:9116

2
Cargo.lock generated
View file

@ -1183,6 +1183,7 @@ name = "email"
version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"lettre",
"tracing",
]
@ -2041,6 +2042,7 @@ dependencies = [
"chrono",
"contracts",
"db",
"email",
"serde",
"serde_json",
"sqlx",

View file

@ -197,9 +197,9 @@ impl Services {
fn build_cors() -> CorsLayer {
let frontend_url = std::env::var("FRONTEND_URL")
.unwrap_or_else(|_| "http://localhost:3000".to_string());
.unwrap_or_else(|_| "http://localhost:9201".to_string());
let admin_url = std::env::var("ADMIN_URL")
.unwrap_or_else(|_| "http://localhost:3001".to_string());
.unwrap_or_else(|_| "http://localhost:9202".to_string());
let allowed_origins: Vec<HeaderValue> = vec![
frontend_url.parse().expect("Invalid FRONTEND_URL"),

View file

@ -17,5 +17,6 @@ db = { path = "../../crates/db" }
auth = { path = "../../crates/auth" }
contracts = { path = "../../crates/contracts" }
storage = { path = "../../crates/storage" }
email = { path = "../../crates/email" }
serde_json = { workspace = true }

View file

@ -242,6 +242,26 @@ async fn apply_to_job(
match ApplicationRepository::create(&state.pool, db_payload).await {
Ok(app) => {
let _ = JobSeekerRepository::update_active_application_count(&state.pool, seeker.id, 1).await;
// Send email notification to company
// Get company user details via raw query
let company_user = sqlx::query_as::<_, (String, Option<String>)>(
"SELECT u.email, u.full_name FROM users u INNER JOIN companies c ON c.user_id = u.id WHERE c.id = $1"
)
.bind(job.company_id)
.fetch_optional(&state.pool)
.await;
if let Ok(Some((email, full_name))) = company_user {
let seeker_name = seeker.full_name.as_deref().unwrap_or("A candidate");
let _ = state.mail.send_new_application_email(
&email,
full_name.as_deref().unwrap_or("Company"),
&job.title,
seeker_name
).await;
}
(StatusCode::CREATED, Json(app)).into_response()
}
Err(e) => {

View file

@ -9,6 +9,7 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
pub struct AppState {
pub pool: sqlx::PgPool,
pub storage: Arc<storage::StorageClient>,
pub mail: Arc<email::Mailer>,
}
#[tokio::main]
@ -30,8 +31,9 @@ async fn main() {
tracing::info!("Job Seekers service — connected to database");
let storage = Arc::new(storage::StorageClient::from_env().await);
let mailer = Arc::new(email::Mailer::new());
let state = AppState { pool, storage };
let state = AppState { pool, storage, mail: mailer };
let app = Router::new()
.nest("/api/jobseeker", handlers::router())

View file

@ -0,0 +1,415 @@
use axum::{
extract::{Json, Path, State},
http::StatusCode,
response::IntoResponse,
routing::{get, post},
Router,
};
use serde::{Deserialize, Serialize};
use crate::AppState;
pub fn router() -> Router<AppState> {
Router::new()
.route("/templates", get(list_templates))
.route("/templates/:name/preview", get(preview_template))
.route("/templates/:name/test", post(send_test_email))
}
#[derive(Serialize)]
struct TemplateInfo {
name: String,
subject: String,
category: String,
description: String,
}
#[derive(Serialize)]
struct TemplateListResponse {
templates: Vec<TemplateInfo>,
}
async fn list_templates() -> impl IntoResponse {
let templates = vec![
TemplateInfo {
name: "welcome".to_string(),
subject: "Welcome to Nxtgauge!".to_string(),
category: "Auth & Account".to_string(),
description: "Sent when user registers successfully".to_string(),
},
TemplateInfo {
name: "verify-email".to_string(),
subject: "Verify Your Email Address".to_string(),
category: "Auth & Account".to_string(),
description: "OTP verification email".to_string(),
},
TemplateInfo {
name: "password-reset".to_string(),
subject: "Reset Your Password".to_string(),
category: "Auth & Account".to_string(),
description: "Password reset link".to_string(),
},
TemplateInfo {
name: "password-changed".to_string(),
subject: "Password Changed Successfully".to_string(),
category: "Auth & Account".to_string(),
description: "Confirmation after password change".to_string(),
},
TemplateInfo {
name: "account-suspended".to_string(),
subject: "Account Suspended".to_string(),
category: "Auth & Account".to_string(),
description: "Account suspension notice".to_string(),
},
TemplateInfo {
name: "new-device-login".to_string(),
subject: "New Login Detected".to_string(),
category: "Auth & Account".to_string(),
description: "Suspicious login alert".to_string(),
},
TemplateInfo {
name: "onboarding-submitted".to_string(),
subject: "Profile Submitted for Review".to_string(),
category: "Onboarding".to_string(),
description: "Profile submitted confirmation".to_string(),
},
TemplateInfo {
name: "profile-verified".to_string(),
subject: "Your Profile is Verified!".to_string(),
category: "Onboarding".to_string(),
description: "Profile approval confirmation".to_string(),
},
TemplateInfo {
name: "profile-rejected".to_string(),
subject: "Profile Verification Update".to_string(),
category: "Onboarding".to_string(),
description: "Profile rejection with reason".to_string(),
},
TemplateInfo {
name: "documents-requested".to_string(),
subject: "Additional Documents Required".to_string(),
category: "Onboarding".to_string(),
description: "Request for additional documents".to_string(),
},
TemplateInfo {
name: "job-pending".to_string(),
subject: "Job Posted Successfully".to_string(),
category: "Jobs".to_string(),
description: "Job submitted for approval".to_string(),
},
TemplateInfo {
name: "job-approved".to_string(),
subject: "Your Job is Now Live!".to_string(),
category: "Jobs".to_string(),
description: "Job approval confirmation".to_string(),
},
TemplateInfo {
name: "job-rejected".to_string(),
subject: "Job Posting Needs Updates".to_string(),
category: "Jobs".to_string(),
description: "Job rejection with reason".to_string(),
},
TemplateInfo {
name: "job-expired".to_string(),
subject: "Job Posting Expired".to_string(),
category: "Jobs".to_string(),
description: "Job posting expiry notice".to_string(),
},
TemplateInfo {
name: "application-received".to_string(),
subject: "New Application Received".to_string(),
category: "Applications".to_string(),
description: "New candidate application".to_string(),
},
TemplateInfo {
name: "application-status".to_string(),
subject: "Application Status Update".to_string(),
category: "Applications".to_string(),
description: "Application status changed".to_string(),
},
TemplateInfo {
name: "requirement-pending".to_string(),
subject: "Requirement Submitted Successfully".to_string(),
category: "Requirements".to_string(),
description: "Requirement submitted for approval".to_string(),
},
TemplateInfo {
name: "requirement-approved".to_string(),
subject: "Your Requirement is Now Live!".to_string(),
category: "Requirements".to_string(),
description: "Requirement approval confirmation".to_string(),
},
TemplateInfo {
name: "requirement-expired".to_string(),
subject: "Requirement Expired".to_string(),
category: "Requirements".to_string(),
description: "Requirement expiry notice".to_string(),
},
TemplateInfo {
name: "new-lead-request".to_string(),
subject: "New Request Received".to_string(),
category: "Leads".to_string(),
description: "Customer receives new lead request".to_string(),
},
TemplateInfo {
name: "lead-request-sent".to_string(),
subject: "Lead Request Sent".to_string(),
category: "Leads".to_string(),
description: "Professional confirmation of lead request".to_string(),
},
TemplateInfo {
name: "lead-request-accepted".to_string(),
subject: "Lead Request Accepted!".to_string(),
category: "Leads".to_string(),
description: "Professional's request was accepted".to_string(),
},
TemplateInfo {
name: "lead-request-rejected".to_string(),
subject: "Lead Request Update".to_string(),
category: "Leads".to_string(),
description: "Professional's request was rejected".to_string(),
},
TemplateInfo {
name: "lead-expired".to_string(),
subject: "Lead Request Expired".to_string(),
category: "Leads".to_string(),
description: "Lead request expired (coins returned)".to_string(),
},
TemplateInfo {
name: "new-matched-lead".to_string(),
subject: "New Lead Available!".to_string(),
category: "Leads".to_string(),
description: "New requirement matching expertise".to_string(),
},
TemplateInfo {
name: "payment-success".to_string(),
subject: "Payment Successful!".to_string(),
category: "Payments".to_string(),
description: "Tracecoin purchase confirmation".to_string(),
},
TemplateInfo {
name: "invoice-generated".to_string(),
subject: "Invoice Generated".to_string(),
category: "Payments".to_string(),
description: "Invoice ready for download".to_string(),
},
TemplateInfo {
name: "manual-credit".to_string(),
subject: "Tracecoins Credited to Your Account".to_string(),
category: "Payments".to_string(),
description: "Admin manual credit notification".to_string(),
},
TemplateInfo {
name: "credit-usage".to_string(),
subject: "Tracecoins Used".to_string(),
category: "Payments".to_string(),
description: "Credit usage confirmation".to_string(),
},
TemplateInfo {
name: "low-credit-balance".to_string(),
subject: "Low Tracecoin Balance".to_string(),
category: "Payments".to_string(),
description: "Low balance warning".to_string(),
},
TemplateInfo {
name: "support-ticket-created".to_string(),
subject: "Support Ticket Received".to_string(),
category: "Support".to_string(),
description: "Ticket creation confirmation".to_string(),
},
TemplateInfo {
name: "support-ticket-replied".to_string(),
subject: "New Response on Your Ticket".to_string(),
category: "Support".to_string(),
description: "Admin replied to ticket".to_string(),
},
TemplateInfo {
name: "support-ticket-resolved".to_string(),
subject: "Ticket Resolved".to_string(),
category: "Support".to_string(),
description: "Ticket resolution confirmation".to_string(),
},
TemplateInfo {
name: "policy-update".to_string(),
subject: "Important Policy Update".to_string(),
category: "Policy".to_string(),
description: "Terms/privacy policy update".to_string(),
},
];
(StatusCode::OK, Json(TemplateListResponse { templates }))
}
#[derive(Serialize)]
struct PreviewResponse {
html: String,
subject: String,
variables: Vec<String>,
}
async fn preview_template(Path(name): Path<String>) -> impl IntoResponse {
// Return sample preview with embedded template content
// In production, this would render the actual template with sample data
let subject = match name.as_str() {
"welcome" => "Welcome to Nxtgauge!",
"verify-email" => "Verify Your Email Address",
"password-reset" => "Reset Your Password",
"password-changed" => "Password Changed Successfully",
"account-suspended" => "Account Suspended",
"new-device-login" => "New Login Detected",
"onboarding-submitted" => "Profile Submitted for Review",
"profile-verified" => "Your Profile is Verified!",
"profile-rejected" => "Profile Verification Update",
"documents-requested" => "Additional Documents Required",
"job-pending" => "Job Posted Successfully",
"job-approved" => "Your Job is Now Live!",
"job-rejected" => "Job Posting Needs Updates",
"job-expired" => "Job Posting Expired",
"application-received" => "New Application Received",
"application-status" => "Application Status Update",
"requirement-pending" => "Requirement Submitted Successfully",
"requirement-approved" => "Your Requirement is Now Live!",
"requirement-expired" => "Requirement Expired",
"new-lead-request" => "New Request Received",
"lead-request-sent" => "Lead Request Sent",
"lead-request-accepted" => "Lead Request Accepted!",
"lead-request-rejected" => "Lead Request Update",
"lead-expired" => "Lead Request Expired",
"new-matched-lead" => "New Lead Available!",
"payment-success" => "Payment Successful!",
"invoice-generated" => "Invoice Generated",
"manual-credit" => "Tracecoins Credited to Your Account",
"credit-usage" => "Tracecoins Used",
"low-credit-balance" => "Low Tracecoin Balance",
"support-ticket-created" => "Support Ticket Received",
"support-ticket-replied" => "New Response on Your Ticket",
"support-ticket-resolved" => "Ticket Resolved",
"policy-update" => "Important Policy Update",
_ => "Email Preview",
};
let sample_html = format!(
r##"<!DOCTYPE html>
<html>
<head>
<style>
body {{ font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }}
.email-wrapper {{ max-width: 600px; margin: 0 auto; background: white; }}
.email-header {{ background: linear-gradient(135deg, #f97316 0%, #ea580c 100%); padding: 40px; text-align: center; }}
.logo {{ color: white; font-size: 28px; font-weight: bold; }}
.email-content {{ padding: 40px; color: #374151; }}
.email-title {{ color: #111827; font-size: 24px; margin-bottom: 20px; }}
.btn-cta {{ display: inline-block; background: linear-gradient(135deg, #f97316 0%, #ea580c 100%); color: white; padding: 14px 32px; border-radius: 8px; text-decoration: none; margin: 20px 0; }}
.detail-card {{ background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px; padding: 20px; margin: 20px 0; }}
.detail-row {{ display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #e5e7eb; }}
.info-box {{ background: #fff7ed; border-left: 4px solid #f97316; padding: 20px; margin: 20px 0; }}
</style>
</head>
<body>
<div class="email-wrapper">
<div class="email-header">
<div class="logo">NXT<span style="font-weight:300">GAUGE</span></div>
</div>
<div class="email-content">
<h1 class="email-title">{}</h1>
<p>Hi John,</p>
<p>This is a sample preview of the <strong>{}</strong> email template.</p>
<div class="detail-card">
<div class="detail-row">
<span style="color: #6b7280;">Template</span>
<span style="font-weight: 600;">{}</span>
</div>
<div class="detail-row">
<span style="color: #6b7280;">Status</span>
<span style="color: #059669; font-weight: 600;">Active</span>
</div>
</div>
<div class="info-box">
<p style="margin: 0; color: #9a3412;"><strong>Sample Data:</strong> This preview uses placeholder values.</p>
</div>
<a href="#" class="btn-cta">Sample Action Button</a>
<p>Best regards,<br><strong>The Nxtgauge Team</strong></p>
</div>
</div>
</body>
</html>"##,
subject, name, name
);
let vars = match name.as_str() {
"welcome" => vec!["first_name", "dashboard_url"],
"verify-email" => vec!["first_name", "otp_code"],
"password-reset" => vec!["first_name", "reset_url"],
"password-changed" => vec!["first_name", "email", "changed_at", "security_url"],
"account-suspended" => vec!["first_name", "suspension_reason"],
"new-device-login" => vec!["first_name", "device", "location", "ip_address", "login_time", "security_url"],
"profile-verified" => vec!["first_name", "role_name", "verified_at", "dashboard_url"],
"profile-rejected" => vec!["first_name", "role_name", "rejection_reason", "profile_url"],
"payment-success" => vec!["first_name", "package_name", "amount_paid", "tracecoins_amount", "transaction_id", "payment_date", "invoice_url", "wallet_url"],
_ => vec!["first_name"],
};
(StatusCode::OK, Json(PreviewResponse {
html: sample_html,
subject: subject.to_string(),
variables: vars.iter().map(|s| s.to_string()).collect(),
}))
}
#[derive(Deserialize)]
struct TestEmailRequest {
to_email: String,
template_name: String,
variables: serde_json::Value,
}
async fn send_test_email(
State(state): State<AppState>,
Json(req): Json<TestEmailRequest>,
) -> impl IntoResponse {
// Extract first_name from variables or use default
let first_name = req.variables
.get("first_name")
.and_then(|v| v.as_str())
.unwrap_or("Test User");
// Send test email based on template
let result = match req.template_name.as_str() {
"welcome" => {
state.mail.send_welcome_email(&req.to_email, first_name).await
}
"verify-email" => {
state.mail.send_verification_email(&req.to_email, first_name, "123456").await
}
"password-reset" => {
state.mail.send_password_reset_email(&req.to_email, first_name, "sample-token").await
}
"profile-verified" => {
state.mail.send_profile_verified_email(&req.to_email, first_name, "Photographer").await
}
"payment-success" => {
state.mail.send_payment_success_email(
&req.to_email,
first_name,
"Starter Pack",
99900,
100,
"TXN123456"
).await
}
_ => {
return (StatusCode::BAD_REQUEST, Json(serde_json::json!({
"error": "Template not supported for test emails"
})));
}
};
match result {
Ok(_) => (StatusCode::OK, Json(serde_json::json!({ "message": "Test email sent" }))),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() }))),
}
}

View file

@ -467,6 +467,11 @@ async fn verify_email(
.await
.map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string(), "DB_ERROR"))?;
// Get user details for welcome email
if let Ok(user) = UserRepository::get_by_id(&state.pool, user_id).await {
let _ = state.mail.send_welcome_email(&user.email, &user.full_name.unwrap_or_default()).await;
}
Ok((StatusCode::OK, Json(serde_json::json!({ "message": "Email verified successfully" }))))
}

View file

@ -1,4 +1,5 @@
pub mod admin;
pub mod admin_email;
pub mod activity_logs;
pub mod approvals;
pub mod auth;

View file

@ -127,23 +127,43 @@ async fn user_create_ticket(
.await;
match result {
Ok(r) => (
StatusCode::CREATED,
Json(TicketDto {
id: r.id,
title: r.subject,
description: r.description,
ticket_type: r.category,
priority: r.priority,
status: r.status,
requester_name: r.requester_name,
requester_email: r.requester_email,
assigned_to: r.assigned_to,
created_at: r.created_at.to_rfc3339(),
updated_at: r.updated_at.to_rfc3339(),
}),
)
.into_response(),
Ok(r) => {
// Send confirmation email to user
if let Ok(user) = db::models::user::UserRepository::get_by_id(&state.pool, auth.user_id).await {
let response_time = match priority.as_str() {
"high" => "2-4 hours",
"medium" => "12-24 hours",
_ => "24-48 hours",
};
let _ = state.mail.send_support_ticket_created_email(
&user.email,
user.full_name.as_deref().unwrap_or_default(),
&r.id.to_string(),
&body.subject,
&category,
&priority,
response_time
).await;
}
(
StatusCode::CREATED,
Json(TicketDto {
id: r.id,
title: r.subject,
description: r.description,
ticket_type: r.category,
priority: r.priority,
status: r.status,
requester_name: r.requester_name,
requester_email: r.requester_email,
assigned_to: r.assigned_to,
created_at: r.created_at.to_rfc3339(),
updated_at: r.updated_at.to_rfc3339(),
}),
)
.into_response()
}
Err(e) => {
tracing::error!("Failed to create support ticket: {}", e);
(
@ -689,22 +709,36 @@ async fn admin_update_case(
.await;
match result {
Ok(Some(r)) => (
StatusCode::OK,
Json(serde_json::json!({
"id": r.id,
"title": r.subject,
"type": r.category,
"priority": r.priority,
"status": r.status,
"requesterName": r.requester_name,
"requesterEmail": r.requester_email,
"assignedTo": r.assigned_to,
"createdAt": r.created_at.to_rfc3339(),
"updatedAt": r.updated_at.to_rfc3339(),
})),
)
.into_response(),
Ok(Some(r)) => {
// Send email notification if ticket was resolved
if body.status.as_deref() == Some("resolved") {
if let Some(user_email) = &r.requester_email {
let user_name = r.requester_name.clone().unwrap_or_default();
let _ = state.mail.send_support_ticket_resolved_email(
user_email,
&user_name,
&r.subject
).await;
}
}
(
StatusCode::OK,
Json(serde_json::json!({
"id": r.id,
"title": r.subject,
"type": r.category,
"priority": r.priority,
"status": r.status,
"requesterName": r.requester_name,
"requesterEmail": r.requester_email,
"assignedTo": r.assigned_to,
"createdAt": r.created_at.to_rfc3339(),
"updatedAt": r.updated_at.to_rfc3339(),
})),
)
.into_response()
}
Ok(None) => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Case not found" })),
@ -785,18 +819,47 @@ async fn admin_add_message(
}
match result {
Ok(m) => (
StatusCode::CREATED,
Json(serde_json::json!({
"id": m.id,
"ticketId": m.ticket_id,
"senderId": m.sender_id,
"body": m.body,
"isInternal": m.is_internal,
"createdAt": m.created_at.to_rfc3339(),
})),
)
.into_response(),
Ok(m) => {
// Send email notification to user if this is a non-internal reply
if !is_internal {
if let Ok(ticket) = sqlx::query_as::<_, TicketRow>(
"SELECT id, subject, description, category, priority, status, requester_name, requester_email, assigned_to, created_at, updated_at FROM support_tickets WHERE id = $1"
)
.bind(id)
.fetch_one(&state.pool)
.await
{
if let Some(user_email) = ticket.requester_email {
// Try to get user name from user table
let user_name = if let Ok(user) = db::models::user::UserRepository::get_by_email(&state.pool, &user_email).await {
user.full_name.unwrap_or_default()
} else {
ticket.requester_name.unwrap_or_default()
};
let _ = state.mail.send_support_ticket_replied_email(
&user_email,
&user_name,
&ticket.subject,
&body.body
).await;
}
}
}
(
StatusCode::CREATED,
Json(serde_json::json!({
"id": m.id,
"ticketId": m.ticket_id,
"senderId": m.sender_id,
"body": m.body,
"isInternal": m.is_internal,
"createdAt": m.created_at.to_rfc3339(),
})),
)
.into_response()
}
Err(e) => {
tracing::error!("Failed to add message to case {}: {}", id, e);
(

View file

@ -164,7 +164,18 @@ async fn approve_verification(
)
.await
{
Ok(v) => (StatusCode::OK, Json(v)).into_response(),
Ok(v) => {
// Send approval email
if let Ok(user) = db::models::user::UserRepository::get_by_id(&state.pool, v.user_id).await {
let display = role_key_to_display(&v.role_key);
let _ = state.mail.send_approval_approved_email(
&user.email,
user.full_name.as_deref().unwrap_or_default(),
&display
).await;
}
(StatusCode::OK, Json(v)).into_response()
}
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
}
}
@ -254,7 +265,7 @@ async fn request_documents(
.await
{
Ok(v) => {
// Notify the user
// Notify the user via in-app notification
sqlx::query(
r#"INSERT INTO notifications (user_id, title, body, type, reference_id)
VALUES ($1, $2, $3, $4, $5)"#,
@ -268,6 +279,17 @@ async fn request_documents(
.await
.ok();
// Send email notification
if let Ok(user) = db::models::user::UserRepository::get_by_id(&state.pool, v.user_id).await {
let display = role_key_to_display(&v.role_key);
let _ = state.mail.send_documents_requested_email(
&user.email,
user.full_name.as_deref().unwrap_or_default(),
&display,
&payload.message
).await;
}
(StatusCode::OK, Json(v)).into_response()
}
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),

View file

@ -102,6 +102,8 @@ async fn main() {
// ── Tracecoin Packages & Reports (admin) ──────────────────────────
.nest("/api/admin/tracecoin-packages", handlers::pricing::packages_router())
.nest("/api/admin/reports", handlers::pricing::reports_router())
// ── Email Management (admin) ──────────────────────────────────────
.nest("/api/admin/email", handlers::admin_email::router())
.route("/health", get(|| async { "Users OK" }))
.with_state(state);

View file

@ -7,3 +7,4 @@ edition = "2021"
lettre = { workspace = true }
anyhow = { workspace = true }
tracing = { workspace = true }
chrono = { workspace = true }

View file

@ -1,21 +1,115 @@
use anyhow::Result;
use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport,
Message, Tokio1Executor,
message::{header::ContentType, Mailbox, MultiPart, SinglePart},
transport::smtp::authentication::Credentials,
AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor,
};
use std::collections::HashMap;
use std::env;
// ── Template Engine ───────────────────────────────────────────────────────────
pub struct TemplateEngine;
impl TemplateEngine {
pub fn new() -> Self {
Self
}
pub fn render(&self, template_name: &str, vars: HashMap<&str, &str>) -> Result<String> {
let base = include_str!("../templates/base.html");
let content = self.load_template(template_name)?;
// Replace content in base template
let mut html = base.replace("{{content}}", content);
// Replace all variables
for (key, value) in vars {
html = html.replace(&format!("{{{{{}}}}}", key), value);
}
Ok(html)
}
fn load_template(&self, name: &str) -> Result<&str> {
match name {
// Auth & Account
"welcome" => Ok(include_str!("../templates/welcome.html")),
"verify-email" => Ok(include_str!("../templates/verify-email.html")),
"password-reset" => Ok(include_str!("../templates/password-reset.html")),
"password-changed" => Ok(include_str!("../templates/password-changed.html")),
"account-suspended" => Ok(include_str!("../templates/account-suspended.html")),
// Onboarding & Verification
"onboarding-submitted" => Ok(include_str!("../templates/onboarding-submitted.html")),
"profile-verified" => Ok(include_str!("../templates/profile-verified.html")),
"profile-rejected" => Ok(include_str!("../templates/profile-rejected.html")),
"documents-requested" => Ok(include_str!("../templates/documents-requested.html")),
// Jobs
"job-pending" => Ok(include_str!("../templates/job-pending.html")),
"job-approved" => Ok(include_str!("../templates/job-approved.html")),
"job-rejected" => Ok(include_str!("../templates/job-rejected.html")),
"job-expired" => Ok(include_str!("../templates/job-expired.html")),
// Applications
"application-received" => Ok(include_str!("../templates/application-received.html")),
"application-status" => Ok(include_str!("../templates/application-status.html")),
// Requirements
"requirement-pending" => Ok(include_str!("../templates/requirement-pending.html")),
"requirement-approved" => Ok(include_str!("../templates/requirement-approved.html")),
"requirement-expired" => Ok(include_str!("../templates/requirement-expired.html")),
// Leads
"new-lead-request" => Ok(include_str!("../templates/new-lead-request.html")),
"lead-request-sent" => Ok(include_str!("../templates/lead-request-sent.html")),
"lead-request-accepted" => Ok(include_str!("../templates/lead-request-accepted.html")),
"lead-request-rejected" => Ok(include_str!("../templates/lead-request-rejected.html")),
"lead-expired" => Ok(include_str!("../templates/lead-expired.html")),
// Payments
"payment-success" => Ok(include_str!("../templates/payment-success.html")),
"invoice-generated" => Ok(include_str!("../templates/invoice-generated.html")),
"manual-credit" => Ok(include_str!("../templates/manual-credit.html")),
"credit-usage" => Ok(include_str!("../templates/credit-usage.html")),
"low-credit-balance" => Ok(include_str!("../templates/low-credit-balance.html")),
// Security
"new-device-login" => Ok(include_str!("../templates/new-device-login.html")),
// Support
"support-ticket-created" => Ok(include_str!("../templates/support-ticket-created.html")),
"support-ticket-replied" => Ok(include_str!("../templates/support-ticket-replied.html")),
"support-ticket-resolved" => Ok(include_str!("../templates/support-ticket-resolved.html")),
// Marketplace
"new-matched-lead" => Ok(include_str!("../templates/new-matched-lead.html")),
// Policy
"policy-update" => Ok(include_str!("../templates/policy-update.html")),
_ => Err(anyhow::anyhow!("Template not found: {}", name)),
}
}
}
impl Default for TemplateEngine {
fn default() -> Self {
Self::new()
}
}
// ── Mailer ────────────────────────────────────────────────────────────────────
pub struct Mailer {
transport: Option<AsyncSmtpTransport<Tokio1Executor>>,
from_email: String,
from_name: String,
template_engine: TemplateEngine,
}
impl Mailer {
/// Build from environment variables. SMTP is optional — if vars are missing emails are
/// silently skipped so development works without a real SMTP server.
pub fn new() -> Self {
let smtp_host = env::var("SMTP_HOST").ok();
let smtp_user = env::var("SMTP_USER").ok();
@ -36,7 +130,7 @@ impl Mailer {
match AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&host) {
Ok(builder) => {
let t = builder.port(smtp_port).credentials(creds).build();
tracing::info!("SMTP transport configured (host={}:{})", host, smtp_port);
tracing::info!("SMTP transport configured (host={} port={})", host, smtp_port);
Some(t)
}
Err(e) => {
@ -46,315 +140,475 @@ impl Mailer {
}
}
_ => {
tracing::warn!("SMTP_HOST/SMTP_USER/SMTP_PASS not all set — email disabled");
tracing::warn!("SMTP not configured — emails disabled");
None
}
};
Self { transport, from_email, from_name }
Self {
transport,
from_email,
from_name,
template_engine: TemplateEngine::new(),
}
}
async fn send(&self, to: &str, subject: &str, body: String) -> Result<()> {
async fn send_html(&self, to: &str, subject: &str, html_body: String) -> Result<()> {
let Some(transport) = &self.transport else {
tracing::debug!("SMTP disabled — skipping email to {} (subject: {})", to, subject);
tracing::debug!("SMTP disabled — skipping email to {}", to);
return Ok(());
};
let from: Mailbox = format!("{} <{}>", self.from_name, self.from_email).parse()?;
let to: Mailbox = to.parse()?;
let email = Message::builder()
.from(format!("{} <{}>", self.from_name, self.from_email).parse()?)
.to(to.parse()?)
.from(from)
.to(to)
.subject(subject)
.body(body)?;
.header(ContentType::TEXT_HTML)
.body(html_body)?;
transport.send(email).await?;
Ok(())
}
// ── Auth ──────────────────────────────────────────────────────────────────
// ── Auth & Account ────────────────────────────────────────────────────────
pub async fn send_verification_email(&self, to: &str, name: &str, otp: &str) -> Result<()> {
self.send(
to,
"Verify your NXTGAUGE account",
format!(
"Hello {},\n\nYour verification code is: {}\n\nThis code expires in 15 minutes.\n\nRegards,\nThe NXTGAUGE Team",
name, otp
),
).await
let vars = HashMap::from([
("first_name", name),
("otp_code", otp),
]);
let html = self.template_engine.render("verify-email", vars)?;
self.send_html(to, "Verify Your Email Address", html).await
}
pub async fn send_password_reset_email(&self, to: &str, name: &str, token: &str) -> Result<()> {
let frontend_url = env::var("FRONTEND_URL")
.unwrap_or_else(|_| "http://localhost:3000".to_string());
self.send(
to,
"Reset your NXTGAUGE password",
format!(
"Hello {},\n\nClick to reset your password:\n{}/reset-password?token={}\n\nIf you did not request this, ignore this email.\n\nRegards,\nThe NXTGAUGE Team",
name, frontend_url, token
),
).await
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
let reset_url = format!("{}/reset-password?token={}", frontend_url, token);
let vars = HashMap::from([
("first_name", name),
("reset_url", &reset_url),
]);
let html = self.template_engine.render("password-reset", vars)?;
self.send_html(to, "Reset Your Password", html).await
}
pub async fn send_password_changed_email(&self, to: &str, name: &str) -> Result<()> {
self.send(
to,
"Your NXTGAUGE password was changed",
format!(
"Hello {},\n\nYour password was successfully changed. If you did not do this, contact support immediately.\n\nRegards,\nThe NXTGAUGE Team",
name
),
).await
pub async fn send_welcome_email(&self, to: &str, name: &str) -> Result<()> {
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
let dashboard_url = format!("{}/dashboard", frontend_url);
let vars = HashMap::from([
("first_name", name),
("dashboard_url", &dashboard_url),
]);
let html = self.template_engine.render("welcome", vars)?;
self.send_html(to, "Welcome to Nxtgauge!", html).await
}
// ── Profile Verification ───────────────────────────────────────────────────
pub async fn send_profile_verified_email(&self, to: &str, name: &str, role_name: &str) -> Result<()> {
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
let now = chrono::Local::now().format("%B %d, %Y").to_string();
let vars = HashMap::from([
("first_name", name),
("role_name", role_name),
("verified_at", &now),
("dashboard_url", &frontend_url),
]);
let html = self.template_engine.render("profile-verified", vars)?;
self.send_html(to, "Your Profile is Verified!", html).await
}
pub async fn send_profile_rejected_email(&self, to: &str, name: &str, role_name: &str, reason: &str) -> Result<()> {
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
let profile_url = format!("{}/dashboard/profile", frontend_url);
let vars = HashMap::from([
("first_name", name),
("role_name", role_name),
("rejection_reason", reason),
("profile_url", &profile_url),
]);
let html = self.template_engine.render("profile-rejected", vars)?;
self.send_html(to, "Profile Verification Update", html).await
}
// ── Jobs ───────────────────────────────────────────────────────────────────
pub async fn send_job_pending_email(&self, to: &str, company_name: &str, job_title: &str) -> Result<()> {
let now = chrono::Local::now().format("%B %d, %Y").to_string();
let vars = HashMap::from([
("first_name", company_name),
("job_title", job_title),
("posted_at", &now),
]);
let html = self.template_engine.render("job-pending", vars)?;
self.send_html(to, "Job Posted Successfully", html).await
}
pub async fn send_job_approved_email(&self, to: &str, company_name: &str, job_title: &str) -> Result<()> {
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
let now = chrono::Local::now().format("%B %d, %Y").to_string();
let expires = (chrono::Local::now() + chrono::Duration::days(30)).format("%B %d, %Y").to_string();
let job_url = format!("{}/dashboard/jobs", frontend_url);
let vars = HashMap::from([
("first_name", company_name),
("job_title", job_title),
("approved_at", &now),
("expires_at", &expires),
("job_url", &job_url),
]);
let html = self.template_engine.render("job-approved", vars)?;
self.send_html(to, "Your Job is Now Live!", html).await
}
pub async fn send_job_rejected_email(&self, to: &str, company_name: &str, job_title: &str, reason: &str) -> Result<()> {
let vars = HashMap::from([
("first_name", company_name),
("job_title", job_title),
("rejection_reason", reason),
]);
let html = self.template_engine.render("job-rejected", vars)?;
self.send_html(to, "Job Posting Needs Updates", html).await
}
pub async fn send_application_received_email(&self, to: &str, company_name: &str, job_title: &str, applicant_name: &str, total_apps: i64) -> Result<()> {
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
let apps_url = format!("{}/dashboard/jobs", frontend_url);
let total_str = total_apps.to_string();
let vars = HashMap::from([
("first_name", company_name),
("job_title", job_title),
("applicant_name", applicant_name),
("total_applications", &total_str),
("applications_url", &apps_url),
]);
let html = self.template_engine.render("application-received", vars)?;
self.send_html(to, &format!("New Application: {}", job_title), html).await
}
// ── Leads ──────────────────────────────────────────────────────────────────
pub async fn send_lead_request_sent_email(&self, to: &str, name: &str, requirement_title: &str, location: &str, reserved: i32) -> Result<()> {
let reserved_str = reserved.to_string();
let vars = HashMap::from([
("first_name", name),
("requirement_title", requirement_title),
("location", location),
("tracecoins_reserved", &reserved_str),
]);
let html = self.template_engine.render("lead-request-sent", vars)?;
self.send_html(to, "Lead Request Sent", html).await
}
pub async fn send_lead_request_accepted_email(&self, to: &str, professional_name: &str, customer_name: &str, requirement_title: &str, deducted: i32) -> Result<()> {
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
let now = chrono::Local::now().format("%B %d, %Y").to_string();
let lead_url = format!("{}/dashboard/leads/accepted", frontend_url);
let deducted_str = deducted.to_string();
let vars = HashMap::from([
("first_name", professional_name),
("customer_name", customer_name),
("requirement_title", requirement_title),
("tracecoins_deducted", &deducted_str),
("accepted_at", &now),
("lead_url", &lead_url),
]);
let html = self.template_engine.render("lead-request-accepted", vars)?;
self.send_html(to, "Lead Request Accepted!", html).await
}
pub async fn send_new_lead_request_email(&self, to: &str, customer_name: &str, requirement_title: &str, professional_name: &str, profession: &str) -> Result<()> {
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
let now = chrono::Local::now().format("%B %d, %Y at %I:%M %p").to_string();
let req_url = format!("{}/dashboard/requirements", frontend_url);
let vars = HashMap::from([
("first_name", customer_name),
("requirement_title", requirement_title),
("professional_name", professional_name),
("profession_type", profession),
("requested_at", &now),
("requirement_url", &req_url),
]);
let html = self.template_engine.render("new-lead-request", vars)?;
self.send_html(to, &format!("New Request: {}", requirement_title), html).await
}
// ── Payments ───────────────────────────────────────────────────────────────
pub async fn send_payment_success_email(&self, to: &str, name: &str, package_name: &str, amount: i32, tracecoins: i32, transaction_id: &str) -> Result<()> {
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
let invoice_url = format!("{}/dashboard/wallet/invoices", frontend_url);
let wallet_url = format!("{}/dashboard/wallet", frontend_url);
let amount_str = format!("{:.2}", amount as f32 / 100.0);
let tc_str = tracecoins.to_string();
let now = chrono::Local::now().format("%B %d, %Y").to_string();
let vars = HashMap::from([
("first_name", name),
("package_name", package_name),
("amount_paid", &amount_str),
("tracecoins_amount", &tc_str),
("transaction_id", transaction_id),
("payment_date", &now),
("invoice_url", &invoice_url),
("wallet_url", &wallet_url),
]);
let html = self.template_engine.render("payment-success", vars)?;
self.send_html(to, "Payment Successful!", html).await
}
// ── Account ─────────────────────────────────────────────────────────────────
pub async fn send_account_suspended_email(&self, to: &str, name: &str, reason: &str) -> Result<()> {
self.send(
to,
"Your NXTGAUGE account has been suspended",
format!(
"Hello {},\n\nYour account has been suspended.\n\nReason: {}\n\nIf you believe this is a mistake, contact support.\n\nRegards,\nThe NXTGAUGE Team",
name, reason
),
).await
let vars = HashMap::from([
("first_name", name),
("suspension_reason", reason),
]);
let html = self.template_engine.render("account-suspended", vars)?;
self.send_html(to, "Account Suspended", html).await
}
// ── Cron / Expiry ───────────────────────────────────────────────────────────
pub async fn send_lead_expired_email(&self, to: &str, name: &str, tracecoins: i32) -> Result<()> {
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
let tc_str = tracecoins.to_string();
let marketplace_url = format!("{}/dashboard/marketplace", frontend_url);
let vars = HashMap::from([
("first_name", name),
("tracecoins_returned", &tc_str),
("marketplace_url", &marketplace_url),
]);
let html = self.template_engine.render("lead-expired", vars)?;
self.send_html(to, "Lead Request Expired", html).await
}
pub async fn send_requirement_expired_email(&self, to: &str, name: &str, title: &str) -> Result<()> {
let vars = HashMap::from([
("first_name", name),
("requirement_title", title),
]);
let html = self.template_engine.render("requirement-expired", vars)?;
self.send_html(to, "Requirement Expired", html).await
}
pub async fn send_job_expired_email(&self, to: &str, company_name: &str, title: &str) -> Result<()> {
let vars = HashMap::from([
("first_name", company_name),
("job_title", title),
]);
let html = self.template_engine.render("job-expired", vars)?;
self.send_html(to, "Job Posting Expired", html).await
}
// ── Missing Methods Added ───────────────────────────────────────────────────
pub async fn send_password_changed_email(&self, to: &str, name: &str) -> Result<()> {
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
let now = chrono::Local::now().format("%B %d, %Y at %I:%M %p").to_string();
let security_url = format!("{}/dashboard/settings", frontend_url);
let vars = HashMap::from([
("first_name", name),
("email", to),
("changed_at", &now),
("security_url", &security_url),
]);
let html = self.template_engine.render("password-changed", vars)?;
self.send_html(to, "Password Changed Successfully", html).await
}
pub async fn send_account_deleted_email(&self, to: &str, name: &str) -> Result<()> {
self.send(
to,
"Your NXTGAUGE account has been deleted",
format!(
"Hello {},\n\nYour account has been deleted as requested. If this was not you, please contact support immediately.\n\nRegards,\nThe NXTGAUGE Team",
name
),
).await
}
// ── Onboarding & Approvals ────────────────────────────────────────────────
pub async fn send_onboarding_submitted_email(&self, to: &str, name: &str, role: &str) -> Result<()> {
self.send(
to,
"Your NXTGAUGE profile is under review",
format!(
"Hello {},\n\nThank you for submitting your {} profile on NXTGAUGE. Our team will review it within 12 business days.\n\nYou will receive an email once your profile is approved.\n\nRegards,\nThe NXTGAUGE Team",
name, role
),
let vars = HashMap::from([
("first_name", name),
]);
// Use account-suspended template as base or create a simple message
self.send_html(
to,
"Account Deleted",
format!("<html><body><h1>Account Deleted</h1><p>Hi {},</p><p>Your account has been deleted.</p></body></html>", name)
).await
}
pub async fn send_approval_approved_email(&self, to: &str, name: &str, role: &str) -> Result<()> {
let frontend_url = env::var("FRONTEND_URL")
.unwrap_or_else(|_| "http://localhost:3000".to_string());
self.send(
to,
"Your NXTGAUGE profile is approved!",
format!(
"Hello {},\n\nGreat news! Your {} profile on NXTGAUGE has been approved. You can now access your full dashboard.\n\n{}/dashboard\n\nRegards,\nThe NXTGAUGE Team",
name, role, frontend_url
),
).await
self.send_profile_verified_email(to, name, role).await
}
pub async fn send_approval_rejected_email(&self, to: &str, name: &str, role: &str, reason: &str) -> Result<()> {
self.send(
to,
"Update required on your NXTGAUGE profile",
format!(
"Hello {},\n\nUnfortunately, we were unable to approve your {} profile at this time.\n\nReason: {}\n\nPlease update your profile and resubmit. If you have questions, contact support.\n\nRegards,\nThe NXTGAUGE Team",
name, role, reason
),
).await
self.send_profile_rejected_email(to, name, role, reason).await
}
// ── Jobs (Company) ────────────────────────────────────────────────────────
pub async fn send_job_submitted_email(&self, to: &str, company_name: &str, job_title: &str) -> Result<()> {
self.send(
to,
"Your job posting is under review",
format!(
"Hello {},\n\nYour job posting \"{}\" has been submitted for review. It will go live once our team approves it (usually within 24 hours).\n\nRegards,\nThe NXTGAUGE Team",
company_name, job_title
),
).await
}
pub async fn send_job_approved_email(&self, to: &str, company_name: &str, job_title: &str) -> Result<()> {
let frontend_url = env::var("FRONTEND_URL")
.unwrap_or_else(|_| "http://localhost:3000".to_string());
self.send(
to,
"Your job posting is now live!",
format!(
"Hello {},\n\nYour job posting \"{}\" has been approved and is now live on NXTGAUGE.\n\n{}/dashboard/jobs\n\nRegards,\nThe NXTGAUGE Team",
company_name, job_title, frontend_url
),
).await
}
pub async fn send_job_rejected_email(&self, to: &str, company_name: &str, job_title: &str, reason: &str) -> Result<()> {
self.send(
to,
"Your job posting needs updates",
format!(
"Hello {},\n\nYour job posting \"{}\" could not be approved.\n\nReason: {}\n\nPlease update and resubmit from your dashboard.\n\nRegards,\nThe NXTGAUGE Team",
company_name, job_title, reason
),
).await
self.send_job_pending_email(to, company_name, job_title).await
}
pub async fn send_new_application_email(&self, to: &str, company_name: &str, job_title: &str, applicant_name: &str) -> Result<()> {
let frontend_url = env::var("FRONTEND_URL")
.unwrap_or_else(|_| "http://localhost:3000".to_string());
self.send(
to,
&format!("New application for \"{}\"", job_title),
format!(
"Hello {},\n\n{} has applied for your job posting \"{}\".\n\nReview the application:\n{}/dashboard/jobs\n\nRegards,\nThe NXTGAUGE Team",
company_name, applicant_name, job_title, frontend_url
),
).await
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
let apps_url = format!("{}/dashboard/jobs", frontend_url);
let vars = HashMap::from([
("first_name", company_name),
("job_title", job_title),
("applicant_name", applicant_name),
("total_applications", "1"),
("applications_url", &apps_url),
]);
let html = self.template_engine.render("application-received", vars)?;
self.send_html(to, &format!("New Application: {}", job_title), html).await
}
pub async fn send_application_status_email(&self, to: &str, applicant_name: &str, job_title: &str, status: &str) -> Result<()> {
let frontend_url = env::var("FRONTEND_URL")
.unwrap_or_else(|_| "http://localhost:3000".to_string());
let status_label = match status {
"SHORTLISTED" => "shortlisted",
"INTERVIEW" => "selected for an interview",
"OFFERED" => "offered the position",
"HIRED" => "hired",
"REJECTED" => "not selected at this time",
_ => status,
};
self.send(
to,
&format!("Update on your application for \"{}\"", job_title),
format!(
"Hello {},\n\nYour application for \"{}\" has been updated: you have been {}.\n\nView details:\n{}/dashboard/applications\n\nRegards,\nThe NXTGAUGE Team",
applicant_name, job_title, status_label, frontend_url
),
).await
}
// ── Requirements (Customer) ───────────────────────────────────────────────
pub async fn send_requirement_submitted_email(&self, to: &str, name: &str, title: &str) -> Result<()> {
self.send(
to,
"Your requirement is under review",
format!(
"Hello {},\n\nYour requirement \"{}\" has been submitted for review and will go live once approved.\n\nRegards,\nThe NXTGAUGE Team",
name, title
),
).await
}
pub async fn send_requirement_approved_email(&self, to: &str, name: &str, title: &str) -> Result<()> {
let frontend_url = env::var("FRONTEND_URL")
.unwrap_or_else(|_| "http://localhost:3000".to_string());
self.send(
to,
"Your requirement is now live!",
format!(
"Hello {},\n\nYour requirement \"{}\" is now live and professionals can send you requests.\n\n{}/dashboard/requirements\n\nRegards,\nThe NXTGAUGE Team",
name, title, frontend_url
),
).await
}
pub async fn send_lead_request_received_email(&self, to: &str, customer_name: &str, requirement_title: &str, professional_name: &str) -> Result<()> {
let frontend_url = env::var("FRONTEND_URL")
.unwrap_or_else(|_| "http://localhost:3000".to_string());
self.send(
to,
&format!("New request for \"{}\"", requirement_title),
format!(
"Hello {},\n\n{} is interested in your requirement \"{}\" and has sent a request to connect.\n\nReview and accept/reject:\n{}/dashboard/requirements\n\nRegards,\nThe NXTGAUGE Team",
customer_name, professional_name, requirement_title, frontend_url
),
).await
}
pub async fn send_lead_accepted_professional_email(&self, to: &str, professional_name: &str, customer_name: &str, customer_email: &str, customer_phone: &str) -> Result<()> {
self.send(
to,
"Your lead request was accepted!",
format!(
"Hello {},\n\n{} has accepted your request. Here are their contact details:\n\nEmail: {}\nPhone: {}\n\nPlease reach out to them directly.\n\nRegards,\nThe NXTGAUGE Team",
professional_name, customer_name, customer_email, customer_phone
),
).await
let now = chrono::Local::now().format("%B %d, %Y").to_string();
let vars = HashMap::from([
("first_name", name),
("requirement_title", title),
("profession_type", "Service"),
("posted_at", &now),
]);
let html = self.template_engine.render("requirement-pending", vars)?;
self.send_html(to, "Requirement Submitted Successfully", html).await
}
pub async fn send_lead_accepted_customer_email(&self, to: &str, customer_name: &str, professional_name: &str, professional_email: &str, professional_phone: &str) -> Result<()> {
self.send(
let vars = HashMap::from([
("first_name", customer_name),
("professional_name", professional_name),
("professional_email", professional_email),
("professional_phone", professional_phone),
]);
// Use a simple template or new-lead-request as base
self.send_html(
to,
"You accepted a professional request",
format!(
"Hello {},\n\nYou accepted {}'s request. Here are their contact details:\n\nEmail: {}\nPhone: {}\n\nRegards,\nThe NXTGAUGE Team",
customer_name, professional_name, professional_email, professional_phone
),
"Professional Contact Details",
format!("<html><body><h1>Professional Contact</h1><p>Hi {},</p><p>You accepted {}'s request.</p><p>Email: {}</p><p>Phone: {}</p></body></html>",
customer_name, professional_name, professional_email, professional_phone)
).await
}
pub async fn send_lead_rejected_email(&self, to: &str, professional_name: &str, requirement_title: &str) -> Result<()> {
self.send(
to,
"Your lead request was not accepted",
format!(
"Hello {},\n\nYour request for the requirement \"{}\" was not accepted this time. Your Tracecoins have been returned to your wallet.\n\nKeep exploring the marketplace for more opportunities!\n\n{}/dashboard/marketplace\n\nRegards,\nThe NXTGAUGE Team",
professional_name,
requirement_title,
env::var("FRONTEND_URL").unwrap_or_else(|_| "http://localhost:3000".to_string())
),
).await
}
// ── Tracecoins ────────────────────────────────────────────────────────────
pub async fn send_manual_credit_email(&self, to: &str, name: &str, amount: i32, reason: &str) -> Result<()> {
self.send(
to,
"Tracecoins credited to your account",
format!(
"Hello {},\n\n{} Tracecoins have been credited to your NXTGAUGE wallet.\n\nReason: {}\n\nRegards,\nThe NXTGAUGE Team",
name, amount, reason
),
).await
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
let wallet_url = format!("{}/dashboard/wallet", frontend_url);
let amount_str = amount.to_string();
let now = chrono::Local::now().format("%B %d, %Y").to_string();
let vars = HashMap::from([
("first_name", name),
("tracecoins_amount", &amount_str),
("credited_at", &now),
("reason", reason),
("wallet_url", &wallet_url),
]);
let html = self.template_engine.render("manual-credit", vars)?;
self.send_html(to, "Tracecoins Credited to Your Account", html).await
}
// ── Expiry (Cron) ─────────────────────────────────────────────────────────
// ── Support Tickets ─────────────────────────────────────────────────────────
pub async fn send_lead_expired_email(&self, to: &str, name: &str, tracecoins_returned: i32) -> Result<()> {
self.send(
to,
"Your lead request has expired",
format!(
"Hello {},\n\nYour lead request has expired because it wasn't accepted within 24 hours.\n\nWe have refunded your {} reserved Tracecoins back to your wallet.\n\nRegards,\nThe NXTGAUGE Team",
name, tracecoins_returned
),
).await
pub async fn send_support_ticket_created_email(&self, to: &str, name: &str, ticket_id: &str, subject: &str, category: &str, priority: &str, response_time: &str) -> Result<()> {
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
let ticket_url = format!("{}/dashboard/support/{}", frontend_url, ticket_id);
let vars = HashMap::from([
("first_name", name),
("ticket_id", ticket_id),
("subject", subject),
("category", category),
("priority", priority),
("response_time", response_time),
("ticket_url", &ticket_url),
]);
let html = self.template_engine.render("support-ticket-created", vars)?;
self.send_html(to, "Support Ticket Received", html).await
}
pub async fn send_requirement_expired_email(&self, to: &str, name: &str, title: &str) -> Result<()> {
self.send(
to,
"Your requirement has expired",
format!(
"Hello {},\n\nYour requirement \"{}\" has expired and is no longer visible to professionals.\n\nRegards,\nThe NXTGAUGE Team",
name, title
),
).await
pub async fn send_support_ticket_replied_email(&self, to: &str, name: &str, subject: &str, message: &str) -> Result<()> {
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
let ticket_url = format!("{}/dashboard/support", frontend_url);
let vars = HashMap::from([
("first_name", name),
("subject", subject),
("latest_message", message),
("support_agent_name", "Nxtgauge Support Team"),
("ticket_url", &ticket_url),
]);
let html = self.template_engine.render("support-ticket-replied", vars)?;
self.send_html(to, "New Response on Your Ticket", html).await
}
pub async fn send_job_expired_email(&self, to: &str, company_name: &str, title: &str) -> Result<()> {
self.send(
to,
"Your job posting has expired",
format!(
"Hello {},\n\nYour job posting \"{}\" has expired and is no longer accepting applications.\n\nRegards,\nThe NXTGAUGE Team",
company_name, title
),
).await
pub async fn send_support_ticket_resolved_email(&self, to: &str, name: &str, subject: &str) -> Result<()> {
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
let now = chrono::Local::now().format("%B %d, %Y").to_string();
let vars = HashMap::from([
("first_name", name),
("subject", subject),
("resolved_at", &now),
("support_agent_name", "Nxtgauge Support Team"),
]);
let html = self.template_engine.render("support-ticket-resolved", vars)?;
self.send_html(to, "Ticket Resolved", html).await
}
// ── Documents Requested ─────────────────────────────────────────────────────
pub async fn send_documents_requested_email(&self, to: &str, name: &str, role_name: &str, document_request: &str) -> Result<()> {
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
let profile_url = format!("{}/dashboard/profile", frontend_url);
let vars = HashMap::from([
("first_name", name),
("role_name", role_name),
("document_request", document_request),
("profile_url", &profile_url),
]);
let html = self.template_engine.render("documents-requested", vars)?;
self.send_html(to, "Additional Documents Required", html).await
}
// ── Application Status ──────────────────────────────────────────────────────
pub async fn send_application_status_email(&self, to: &str, name: &str, job_title: &str, status: &str) -> Result<()> {
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
let apps_url = format!("{}/dashboard/applications", frontend_url);
let (status_class, status_color, next_steps) = match status {
"SHORTLISTED" => ("status-pending", "#d97706", "The employer has shortlisted you. They may contact you soon for an interview."),
"INTERVIEW" => ("status-pending", "#d97706", "Congratulations! You've been selected for an interview. Check your email for scheduling details."),
"OFFERED" => ("status-approved", "#059669", "Great news! You've received a job offer. Please review the offer details in your dashboard."),
"HIRED" => ("status-approved", "#059669", "Congratulations on your new job! The employer will contact you with next steps."),
"REJECTED" => ("status-rejected", "#dc2626", "Unfortunately, you were not selected for this position. Don't give up - apply to other jobs!"),
_ => ("status-pending", "#6b7280", "Your application status has been updated."),
};
let now = chrono::Local::now().format("%B %d, %Y").to_string();
let vars = HashMap::from([
("first_name", name),
("job_title", job_title),
("company_name", "The employer"),
("status", status),
("status_class", status_class),
("status_color", status_color),
("next_steps", next_steps),
("note", ""),
("updated_at", &now),
("applications_url", &apps_url),
]);
let html = self.template_engine.render("application-status", vars)?;
self.send_html(to, &format!("Application Update: {}", job_title), html).await
}
}

View file

@ -0,0 +1,38 @@
<!-- Account Suspended -->
<h1 class="email-title">Account Suspended</h1>
<div style="text-align: center; margin: 20px 0">
<span class="status-badge status-rejected">Suspended</span>
</div>
<p>Hi {{first_name}},</p>
<p>
Your Nxtgauge account has been suspended due to a violation of our terms of
service.
</p>
<div
class="info-box"
style="background-color: #fee2e2; border-left-color: #dc2626"
>
<p class="info-box-title" style="color: #991b1b">Reason:</p>
<p style="margin: 0; color: #7f1d1d">{{suspension_reason}}</p>
</div>
<p><strong>What this means:</strong></p>
<ul style="margin: 10px 0; padding-left: 20px">
<li>You cannot log in to your account</li>
<li>Your listings are no longer visible</li>
<li>Pending transactions may be cancelled</li>
</ul>
<p>
If you believe this was done in error, please contact our support team
immediately.
</p>
<div style="text-align: center">
<a href="mailto:support@nxtgauge.com" class="cta-button">Contact Support</a>
</div>
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>

View file

@ -0,0 +1,33 @@
<!-- Application Received -->
<h1 class="email-title">📨 New Application Received</h1>
<p>Hi {{first_name}},</p>
<p>You have received a new application for your job posting:</p>
<div class="detail-card">
<div class="detail-row">
<span class="detail-label">Job Title</span>
<span class="detail-value">{{job_title}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Candidate</span>
<span class="detail-value">{{applicant_name}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Applied On</span>
<span class="detail-value">{{applied_at}}</span>
</div>
</div>
<div style="text-align: center">
<a href="{{applications_url}}" class="cta-button">View Application</a>
</div>
<div class="info-box">
<p class="info-box-title">📋 Quick Stats</p>
<p style="margin: 0">
Total applications for this job: <strong>{{total_applications}}</strong>
</p>
</div>
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>

View file

@ -0,0 +1,52 @@
<!-- Application Status Updated -->
<h1 class="email-title">Application Status Update</h1>
<div style="text-align: center; margin: 20px 0">
<span class="status-badge {{status_class}}">{{status}}</span>
</div>
<p>Hi {{first_name}},</p>
<p>There has been an update on your application for the position:</p>
<div class="detail-card">
<div class="detail-row">
<span class="detail-label">Job Title</span>
<span class="detail-value">{{job_title}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Company</span>
<span class="detail-value">{{company_name}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Status</span>
<span class="detail-value" style="color: {{status_color}};"
>{{status}}</span
>
</div>
<div class="detail-row">
<span class="detail-label">Updated On</span>
<span class="detail-value">{{updated_at}}</span>
</div>
</div>
<Show when="{{note}}">
<div
class="info-box"
style="background-color: #f3f4f6; border-left-color: #6b7280"
>
<p class="info-box-title" style="color: #374151">📝 Note from Employer:</p>
<p style="margin: 0; color: #1f2937">{{note}}</p>
</div>
</Show>
<div style="text-align: center">
<a href="{{applications_url}}" class="cta-button">View Application</a>
</div>
<div class="info-box">
<p class="info-box-title">💡 What's Next?</p>
<p style="margin: 0">{{next_steps}}</p>
</div>
<p>Best of luck with your application!</p>
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>

View file

@ -0,0 +1,329 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{subject}}</title>
<style>
/* Reset styles */
body,
table,
td,
p,
a,
li,
blockquote {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
-ms-interpolation-mode: bicubic;
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
}
/* Base styles */
body {
margin: 0 !important;
padding: 0 !important;
background-color: #f5f5f5;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
Arial, sans-serif;
}
/* Container */
.email-wrapper {
width: 100%;
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
}
/* Header */
.email-header {
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
padding: 40px 30px;
text-align: center;
}
.logo {
color: #ffffff;
font-size: 28px;
font-weight: bold;
text-decoration: none;
letter-spacing: -0.5px;
}
.logo span {
font-weight: 300;
}
/* Content */
.email-content {
padding: 40px 30px;
color: #374151;
font-size: 16px;
line-height: 1.6;
}
.email-title {
color: #111827;
font-size: 24px;
font-weight: bold;
margin: 0 0 20px 0;
}
.email-subtitle {
color: #6b7280;
font-size: 18px;
margin: 0 0 30px 0;
}
/* Status Badge */
.status-badge {
display: inline-block;
padding: 8px 16px;
border-radius: 20px;
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-approved {
background-color: #d1fae5;
color: #065f46;
}
.status-rejected {
background-color: #fee2e2;
color: #991b1b;
}
.status-pending {
background-color: #fef3c7;
color: #92400e;
}
.status-expired {
background-color: #f3f4f6;
color: #4b5563;
}
/* CTA Button */
.cta-button {
display: inline-block;
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
color: #ffffff !important;
text-decoration: none;
padding: 14px 32px;
border-radius: 8px;
font-weight: 600;
font-size: 16px;
margin: 20px 0;
}
/* Info Box */
.info-box {
background-color: #fff7ed;
border-left: 4px solid #f97316;
padding: 20px;
margin: 20px 0;
border-radius: 0 8px 8px 0;
}
.info-box-title {
color: #c2410c;
font-weight: 600;
margin: 0 0 10px 0;
}
/* Detail Cards */
.detail-card {
background-color: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
}
.detail-row {
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid #e5e7eb;
}
.detail-row:last-child {
border-bottom: none;
}
.detail-label {
color: #6b7280;
font-size: 14px;
}
.detail-value {
color: #111827;
font-weight: 600;
font-size: 14px;
}
/* Amount Display */
.amount-display {
text-align: center;
padding: 30px;
background: linear-gradient(135deg, #fff7ed 0%, #ffedd5 100%);
border-radius: 12px;
margin: 20px 0;
}
.amount-label {
color: #9a3412;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 8px;
}
.amount-value {
color: #c2410c;
font-size: 48px;
font-weight: bold;
line-height: 1;
}
.amount-currency {
color: #ea580c;
font-size: 24px;
vertical-align: super;
}
/* Footer */
.email-footer {
background-color: #f9fafb;
padding: 30px;
text-align: center;
border-top: 1px solid #e5e7eb;
}
.footer-links {
margin-bottom: 20px;
}
.footer-links a {
color: #6b7280;
text-decoration: none;
margin: 0 15px;
font-size: 14px;
}
.footer-links a:hover {
color: #f97316;
}
.social-links {
margin: 20px 0;
}
.social-links a {
display: inline-block;
width: 40px;
height: 40px;
background-color: #e5e7eb;
border-radius: 50%;
margin: 0 8px;
text-align: center;
line-height: 40px;
color: #6b7280;
text-decoration: none;
}
.footer-text {
color: #9ca3af;
font-size: 12px;
line-height: 1.5;
}
.footer-address {
color: #9ca3af;
font-size: 12px;
margin-top: 10px;
}
/* Responsive */
@media screen and (max-width: 600px) {
.email-content {
padding: 30px 20px;
}
.email-title {
font-size: 20px;
}
.amount-value {
font-size: 36px;
}
}
</style>
</head>
<body>
<table
role="presentation"
cellspacing="0"
cellpadding="0"
border="0"
width="100%"
>
<tr>
<td align="center" style="padding: 20px 0">
<div class="email-wrapper">
<!-- Header -->
<div class="email-header">
<a href="https://nxtgauge.com" class="logo"
>NXT<span>GAUGE</span></a
>
</div>
<!-- Dynamic Content -->
<div class="email-content">{{content}}</div>
<!-- Footer -->
<div class="email-footer">
<div class="footer-links">
<a href="https://nxtgauge.com/help">Help Center</a>
<a href="https://nxtgauge.com/contact">Contact Us</a>
<a href="https://nxtgauge.com/privacy">Privacy Policy</a>
</div>
<div class="footer-text">
<p>
You're receiving this email because you have an account on
Nxtgauge.
</p>
<p>
© 2026 Nxtgauge Technologies Pvt. Ltd. All rights reserved.
</p>
</div>
<div class="footer-address">
<p>Nxtgauge Technologies Pvt. Ltd.</p>
<p>Bangalore, Karnataka, India</p>
<p>GSTIN: 27AABCU9603R1ZX</p>
</div>
</div>
</div>
</td>
</tr>
</table>
</body>
</html>

View file

@ -0,0 +1,66 @@
<!-- Credit Usage Confirmation -->
<h1 class="email-title">Tracecoins Used</h1>
<p>Hi {{first_name}},</p>
<p>
This is a confirmation that Tracecoins have been deducted from your wallet.
</p>
<div
class="amount-display"
style="background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%)"
>
<div class="amount-label" style="color: #991b1b">Tracecoins Deducted</div>
<div class="amount-value" style="color: #dc2626">
<span class="amount-currency">-</span>{{amount_deducted}}
</div>
</div>
<div class="detail-card">
<div class="detail-row">
<span class="detail-label">Transaction Type</span>
<span class="detail-value">{{transaction_type}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Description</span>
<span class="detail-value">{{description}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Date & Time</span>
<span class="detail-value">{{transaction_date}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Reference ID</span>
<span class="detail-value" style="font-family: monospace; font-size: 12px"
>{{reference_id}}</span
>
</div>
</div>
<div
class="detail-card"
style="background-color: #f0fdf4; border-color: #86efac"
>
<div class="detail-row" style="border-color: #86efac">
<span class="detail-label" style="color: #166534">Remaining Balance</span>
<span class="detail-value" style="color: #15803d; font-size: 18px"
>{{remaining_balance}} TC</span
>
</div>
</div>
<div style="text-align: center">
<a href="{{wallet_url}}" class="cta-button">View Wallet</a>
<a
href="{{ledger_url}}"
style="margin-left: 10px; background: #6b7280"
class="cta-button"
>View Ledger</a
>
</div>
<p style="margin-top: 20px; font-size: 14px; color: #6b7280">
Questions? Contact our support team for assistance.
</p>
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>

View file

@ -0,0 +1,52 @@
<!-- Additional Documents Requested -->
<h1 class="email-title">Additional Documents Required</h1>
<p>Hi {{first_name}},</p>
<p>
Our review team needs additional documents to complete the verification of
your {{role_name}} profile.
</p>
<div
class="info-box"
style="background-color: #fef3c7; border-left-color: #d97706"
>
<p class="info-box-title" style="color: #92400e">📋 Documents Requested:</p>
<p style="margin: 0; color: #78350f; white-space: pre-line">
{{document_request}}
</p>
</div>
<div class="detail-card">
<div class="detail-row">
<span class="detail-label">Profile Type</span>
<span class="detail-value">{{role_name}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Status</span>
<span class="detail-value" style="color: #d97706">Documents Requested</span>
</div>
</div>
<p><strong>How to upload:</strong></p>
<ol style="margin: 10px 0; padding-left: 20px">
<li>Go to your profile page</li>
<li>Click on the "Documents" tab</li>
<li>Upload the requested documents</li>
<li>Click "Resubmit for Verification"</li>
</ol>
<div style="text-align: center">
<a href="{{profile_url}}" class="cta-button">Upload Documents</a>
</div>
<div class="info-box">
<p class="info-box-title">⏱️ Response Time</p>
<p style="margin: 0">
Please upload the documents within <strong>7 days</strong> to avoid delays
in your verification.
</p>
</div>
<p>Need help? Contact our support team.</p>
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>

View file

@ -0,0 +1,54 @@
<!-- Invoice Generated -->
<h1 class="email-title">Invoice Generated</h1>
<p>Hi {{first_name}},</p>
<p>An invoice has been generated for your recent Tracecoin purchase.</p>
<div class="detail-card">
<div class="detail-row">
<span class="detail-label">Invoice Number</span>
<span class="detail-value" style="font-family: monospace"
>{{invoice_number}}</span
>
</div>
<div class="detail-row">
<span class="detail-label">Package</span>
<span class="detail-value">{{package_name}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Tracecoins</span>
<span class="detail-value">{{tracecoins_amount}} TC</span>
</div>
<div class="detail-row">
<span class="detail-label">Subtotal</span>
<span class="detail-value">₹{{subtotal}}</span>
</div>
<div class="detail-row">
<span class="detail-label">GST (18%)</span>
<span class="detail-value">₹{{gst_amount}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Total</span>
<span class="detail-value" style="font-weight: bold; color: #111827"
>₹{{total}}</span
>
</div>
<div class="detail-row">
<span class="detail-label">Issued On</span>
<span class="detail-value">{{issued_at}}</span>
</div>
</div>
<div style="text-align: center">
<a href="{{invoice_url}}" class="cta-button">Download Invoice</a>
</div>
<div class="info-box">
<p class="info-box-title">📄 Tax Information</p>
<p style="margin: 0">
This is a GST-compliant invoice for your records. You can download it
anytime from your account.
</p>
</div>
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>

View file

@ -0,0 +1,40 @@
<!-- Job Approved -->
<h1 class="email-title">🎉 Your Job is Now Live!</h1>
<div style="text-align: center; margin: 20px 0">
<span class="status-badge status-approved">✓ Live</span>
</div>
<p>Hi {{first_name}},</p>
<p>
Great news! Your job posting has been approved and is now live on Nxtgauge.
</p>
<div class="detail-card">
<div class="detail-row">
<span class="detail-label">Job Title</span>
<span class="detail-value">{{job_title}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Approved On</span>
<span class="detail-value">{{approved_at}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Expires On</span>
<span class="detail-value">{{expires_at}}</span>
</div>
</div>
<div style="text-align: center">
<a href="{{job_url}}" class="cta-button">View Job Posting</a>
</div>
<div class="info-box">
<p class="info-box-title">💡 Pro Tip</p>
<p style="margin: 0">
You'll receive email notifications when candidates apply. Make sure to
review applications promptly!
</p>
</div>
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>

View file

@ -0,0 +1,33 @@
<!-- Job Expired -->
<h1 class="email-title">Job Posting Expired</h1>
<div style="text-align: center; margin: 20px 0">
<span class="status-badge status-expired">Expired</span>
</div>
<p>Hi {{first_name}},</p>
<p>
Your job posting has expired after 30 days and is no longer accepting
applications.
</p>
<div class="detail-card">
<div class="detail-row">
<span class="detail-label">Job Title</span>
<span class="detail-value">{{job_title}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Status</span>
<span class="detail-value" style="color: #6b7280">Expired</span>
</div>
</div>
<div class="info-box">
<p class="info-box-title">💡 Want to Repost?</p>
<p style="margin: 0">
You can create a new job posting anytime. Reposting helps reach fresh
candidates!
</p>
</div>
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>

View file

@ -0,0 +1,34 @@
<!-- Job Posted - Pending Approval -->
<h1 class="email-title">Job Posted Successfully</h1>
<div style="text-align: center; margin: 20px 0">
<span class="status-badge status-pending">⏳ Pending Approval</span>
</div>
<p>Hi {{first_name}},</p>
<p>Your job posting has been submitted and is now under review by our team.</p>
<div class="detail-card">
<div class="detail-row">
<span class="detail-label">Job Title</span>
<span class="detail-value">{{job_title}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Posted On</span>
<span class="detail-value">{{posted_at}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Status</span>
<span class="detail-value" style="color: #d97706">Pending Review</span>
</div>
</div>
<div class="info-box">
<p class="info-box-title">⏱️ What happens next?</p>
<p style="margin: 0">
Our team typically reviews job postings within 24-48 hours. You'll receive
an email once your job is approved and live.
</p>
</div>
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>

View file

@ -0,0 +1,41 @@
<!-- Job Rejected -->
<h1 class="email-title">Job Posting Needs Updates</h1>
<div style="text-align: center; margin: 20px 0">
<span class="status-badge status-rejected">Needs Changes</span>
</div>
<p>Hi {{first_name}},</p>
<p>We reviewed your job posting and couldn't approve it at this time.</p>
<div
class="info-box"
style="background-color: #fee2e2; border-left-color: #dc2626"
>
<p class="info-box-title" style="color: #991b1b">Reason for Rejection:</p>
<p style="margin: 0; color: #7f1d1d">{{rejection_reason}}</p>
</div>
<div class="detail-card">
<div class="detail-row">
<span class="detail-label">Job Title</span>
<span class="detail-value">{{job_title}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Status</span>
<span class="detail-value" style="color: #dc2626">Rejected</span>
</div>
</div>
<p><strong>What you can do:</strong></p>
<ul style="margin: 10px 0; padding-left: 20px">
<li>Review the feedback above</li>
<li>Update your job posting accordingly</li>
<li>Resubmit for approval</li>
</ul>
<div style="text-align: center">
<a href="{{jobs_url}}" class="cta-button">Edit Job Posting</a>
</div>
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>

View file

@ -0,0 +1,44 @@
<!-- Lead Request Expired -->
<h1 class="email-title">Lead Request Expired</h1>
<div style="text-align: center; margin: 20px 0">
<span class="status-badge status-expired">Expired</span>
</div>
<p>Hi {{first_name}},</p>
<p>
Your lead request has expired because the customer didn't respond within 24
hours.
</p>
<div class="detail-card">
<div class="detail-row">
<span class="detail-label">Status</span>
<span class="detail-value" style="color: #6b7280">Expired</span>
</div>
<div class="detail-row">
<span class="detail-label">Tracecoins Returned</span>
<span class="detail-value" style="color: #059669"
>{{tracecoins_returned}} TC</span
>
</div>
</div>
<div
class="info-box"
style="background-color: #d1fae5; border-left-color: #059669"
>
<p class="info-box-title" style="color: #065f46">💰 Good News!</p>
<p style="margin: 0; color: #064e3b">
Your <strong>{{tracecoins_returned}} Tracecoins</strong> have been returned
to your wallet. You can use them for other lead requests.
</p>
</div>
<div style="text-align: center">
<a href="{{marketplace_url}}" class="cta-button">Browse Marketplace</a>
</div>
<p>Don't worry! There are many more opportunities waiting for you.</p>
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>

View file

@ -0,0 +1,50 @@
<!-- Lead Request Accepted -->
<h1 class="email-title">🎉 Lead Request Accepted!</h1>
<div style="text-align: center; margin: 20px 0">
<span class="status-badge status-approved">✓ Accepted</span>
</div>
<p>Hi {{first_name}},</p>
<p>
Great news! The customer has accepted your lead request. You can now view
their contact details.
</p>
<div class="detail-card">
<div class="detail-row">
<span class="detail-label">Requirement</span>
<span class="detail-value">{{requirement_title}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Customer</span>
<span class="detail-value">{{customer_name}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Tracecoins Deducted</span>
<span class="detail-value" style="color: #dc2626"
>{{tracecoins_deducted}} TC</span
>
</div>
<div class="detail-row">
<span class="detail-label">Accepted On</span>
<span class="detail-value">{{accepted_at}}</span>
</div>
</div>
<div style="text-align: center">
<a href="{{lead_url}}" class="cta-button">View Contact Details</a>
</div>
<div
class="info-box"
style="background-color: #d1fae5; border-left-color: #059669"
>
<p class="info-box-title" style="color: #065f46">💡 Next Steps</p>
<p style="margin: 0; color: #064e3b">
Contact the customer promptly to discuss the project details. Professional
communication is key!
</p>
</div>
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>

View file

@ -0,0 +1,48 @@
<!-- Lead Request Rejected -->
<h1 class="email-title">Lead Request Update</h1>
<div style="text-align: center; margin: 20px 0">
<span class="status-badge status-rejected">Not Accepted</span>
</div>
<p>Hi {{first_name}},</p>
<p>
The customer has decided not to proceed with your request for this
requirement.
</p>
<div class="detail-card">
<div class="detail-row">
<span class="detail-label">Requirement</span>
<span class="detail-value">{{requirement_title}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Customer</span>
<span class="detail-value">{{customer_name}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Tracecoins Returned</span>
<span class="detail-value" style="color: #059669"
>{{tracecoins_returned}} TC</span
>
</div>
</div>
<div
class="info-box"
style="background-color: #d1fae5; border-left-color: #059669"
>
<p class="info-box-title" style="color: #065f46">💰 Tracecoins Refunded</p>
<p style="margin: 0; color: #064e3b">
Your <strong>{{tracecoins_returned}} Tracecoins</strong> have been returned
to your wallet and are available for other opportunities.
</p>
</div>
<div style="text-align: center">
<a href="{{marketplace_url}}" class="cta-button">Browse More Leads</a>
</div>
<p>Don't be discouraged! There are many other opportunities waiting for you.</p>
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>

View file

@ -0,0 +1,42 @@
<!-- Lead Request Sent -->
<h1 class="email-title">Lead Request Sent</h1>
<p>Hi {{first_name}},</p>
<p>Your lead request has been sent successfully. Here's what happens next:</p>
<div class="detail-card">
<div class="detail-row">
<span class="detail-label">Requirement</span>
<span class="detail-value">{{requirement_title}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Location</span>
<span class="detail-value">{{location}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Reserved</span>
<span class="detail-value" style="color: #ea580c"
>{{tracecoins_reserved}} TC</span
>
</div>
<div class="detail-row">
<span class="detail-label">Expires In</span>
<span class="detail-value">24 hours</span>
</div>
</div>
<div class="info-box">
<p class="info-box-title">💰 Tracecoin Policy</p>
<p style="margin: 0">
<strong>{{tracecoins_reserved}} Tracecoins</strong> have been reserved from
your wallet. They will be:
</p>
<ul style="margin: 10px 0; padding-left: 20px">
<li><strong>Deducted</strong> if the customer accepts your request</li>
<li><strong>Returned</strong> if rejected or expired</li>
</ul>
</div>
<p>You'll receive an email when the customer responds.</p>
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>

View file

@ -0,0 +1,46 @@
<!-- Low Credit Balance Warning -->
<h1 class="email-title">⚠️ Low Tracecoin Balance</h1>
<p>Hi {{first_name}},</p>
<p>
Your Tracecoin balance is running low. Don't miss out on lead opportunities!
</p>
<div
class="amount-display"
style="background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%)"
>
<div class="amount-label" style="color: #991b1b">Current Balance</div>
<div class="amount-value" style="color: #dc2626">
<span class="amount-currency"></span>{{current_balance}}
</div>
<div style="color: #7f1d1d; font-size: 14px; margin-top: 8px">
Tracecoins remaining
</div>
</div>
<div
class="info-box"
style="background-color: #fef3c7; border-left-color: #d97706"
>
<p class="info-box-title" style="color: #92400e">💡 What you can do:</p>
<ul style="margin: 10px 0; padding-left: 20px; color: #78350f">
<li>Most leads cost <strong>25 Tracecoins</strong> per request</li>
<li>Purchase more Tracecoins to continue accessing leads</li>
<li>New requirements are posted daily</li>
</ul>
</div>
<div style="text-align: center">
<a href="{{buy_url}}" class="cta-button">Buy Tracecoins Now</a>
</div>
<p
style="margin-top: 20px; font-size: 14px; color: #6b7280; text-align: center"
>
<strong>Popular Package:</strong> {{popular_package}} -
{{popular_package_price}}<br />
Good for ~{{leads_count}} lead requests
</p>
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>

View file

@ -0,0 +1,49 @@
<!-- Manual Tracecoin Credit -->
<h1 class="email-title">Tracecoins Credited to Your Account</h1>
<div class="amount-display">
<div class="amount-label">Tracecoins Added</div>
<div class="amount-value">
<span class="amount-currency">+</span>{{tracecoins_amount}}
</div>
</div>
<p>Hi {{first_name}},</p>
<p>Tracecoins have been manually credited to your wallet by our admin team.</p>
<div class="detail-card">
<div class="detail-row">
<span class="detail-label">Amount Credited</span>
<span class="detail-value" style="color: #059669; font-weight: bold"
>{{tracecoins_amount}} TC</span
>
</div>
<div class="detail-row">
<span class="detail-label">Date</span>
<span class="detail-value">{{credited_at}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Reason</span>
<span class="detail-value">{{reason}}</span>
</div>
</div>
<div
class="info-box"
style="background-color: #dbeafe; border-left-color: #2563eb"
>
<p class="info-box-title" style="color: #1e40af">
Why did I receive this?
</p>
<p style="margin: 0; color: #1e3a8a">{{reason}}</p>
</div>
<div style="text-align: center">
<a href="{{wallet_url}}" class="cta-button">View Wallet</a>
</div>
<p>
If you have any questions about this credit, please contact our support team.
</p>
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>

View file

@ -0,0 +1,64 @@
<!-- Login from New Device -->
<h1 class="email-title">🔐 New Login Detected</h1>
<div style="text-align: center; margin: 20px 0">
<div
style="
display: inline-block;
width: 80px;
height: 80px;
background-color: #fef3c7;
border-radius: 50%;
text-align: center;
line-height: 80px;
font-size: 40px;
"
>
⚠️
</div>
</div>
<p>Hi {{first_name}},</p>
<p>
We detected a login to your Nxtgauge account from a new device or location.
</p>
<div class="detail-card">
<div class="detail-row">
<span class="detail-label">Device</span>
<span class="detail-value">{{device}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Location</span>
<span class="detail-value">{{location}}</span>
</div>
<div class="detail-row">
<span class="detail-label">IP Address</span>
<span class="detail-value">{{ip_address}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Time</span>
<span class="detail-value">{{login_time}}</span>
</div>
</div>
<div
class="info-box"
style="background-color: #fee2e2; border-left-color: #dc2626"
>
<p class="info-box-title" style="color: #991b1b">🚨 Wasn't you?</p>
<p style="margin: 0; color: #7f1d1d">
If you didn't log in, please
<strong>secure your account immediately</strong> by changing your password.
</p>
</div>
<div style="text-align: center">
<a href="{{security_url}}" class="cta-button">Secure My Account</a>
</div>
<p style="margin-top: 20px; font-size: 14px; color: #6b7280">
If this was you, you can safely ignore this email.
</p>
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>

View file

@ -0,0 +1,38 @@
<!-- New Lead Request Received (Customer) -->
<h1 class="email-title">📨 New Lead Request</h1>
<p>Hi {{first_name}},</p>
<p>You have received a new lead request for your requirement:</p>
<div class="detail-card">
<div class="detail-row">
<span class="detail-label">Your Requirement</span>
<span class="detail-value">{{requirement_title}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Professional</span>
<span class="detail-value">{{professional_name}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Profession</span>
<span class="detail-value">{{profession_type}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Received On</span>
<span class="detail-value">{{requested_at}}</span>
</div>
</div>
<div style="text-align: center">
<a href="{{requirement_url}}" class="cta-button">Review Request</a>
</div>
<div class="info-box">
<p class="info-box-title">⏱️ Response Needed</p>
<p style="margin: 0">
This request will expire in <strong>24 hours</strong>. Please respond
promptly to connect with the professional.
</p>
</div>
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>

View file

@ -0,0 +1,56 @@
<!-- New Matched Lead Available -->
<h1 class="email-title">🔔 New Lead Available!</h1>
<p>Hi {{first_name}},</p>
<p>A new requirement matching your expertise has been posted on Nxtgauge.</p>
<div class="detail-card">
<div class="detail-row">
<span class="detail-label">Requirement</span>
<span class="detail-value">{{requirement_title}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Service Type</span>
<span class="detail-value">{{profession_type}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Location</span>
<span class="detail-value">{{location}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Budget</span>
<span class="detail-value">{{budget}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Posted</span>
<span class="detail-value">{{posted_at}}</span>
</div>
</div>
<div
class="info-box"
style="background-color: #fff7ed; border-left-color: #f97316"
>
<p class="info-box-title" style="color: #c2410c">⚡ Act Fast!</p>
<p style="margin: 0; color: #7c2d12">
This requirement will receive up to <strong>20 requests</strong> from
professionals. Send your request early to increase your chances!
</p>
</div>
<div class="amount-display" style="padding: 20px">
<div class="amount-label">Tracecoins Required</div>
<div class="amount-value" style="font-size: 36px">
{{tracecoins_required}} TC
</div>
</div>
<div style="text-align: center">
<a href="{{requirement_url}}" class="cta-button">View & Request Lead</a>
</div>
<p style="margin-top: 20px; font-size: 14px; color: #6b7280">
You have <strong>{{current_balance}} Tracecoins</strong> in your wallet.
</p>
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>

View file

@ -0,0 +1,48 @@
<!-- Onboarding Submitted -->
<h1 class="email-title">Profile Submitted for Review</h1>
<div style="text-align: center; margin: 20px 0">
<span class="status-badge status-pending">⏳ Under Review</span>
</div>
<p>Hi {{first_name}},</p>
<p>
Your {{role_name}} profile has been submitted for verification. Our team will
review it shortly.
</p>
<div class="detail-card">
<div class="detail-row">
<span class="detail-label">Profile Type</span>
<span class="detail-value">{{role_name}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Submitted On</span>
<span class="detail-value">{{submitted_at}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Status</span>
<span class="detail-value" style="color: #d97706">Pending Review</span>
</div>
</div>
<div class="info-box">
<p class="info-box-title">⏱️ What's Next?</p>
<p style="margin: 0">
Our team typically reviews profiles within <strong>24-48 hours</strong>.
You'll receive an email once your profile is approved.
</p>
</div>
<div
class="info-box"
style="background-color: #dbeafe; border-left-color: #2563eb"
>
<p class="info-box-title" style="color: #1e40af">💡 While You Wait</p>
<p style="margin: 0; color: #1e3a8a">
You can explore the platform and familiarize yourself with the dashboard.
Full features will be unlocked once verified!
</p>
</div>
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>

View file

@ -0,0 +1,54 @@
<!-- Password Changed -->
<h1 class="email-title">Password Changed Successfully</h1>
<div style="text-align: center; margin: 20px 0">
<div
style="
display: inline-block;
width: 64px;
height: 64px;
background-color: #d1fae5;
border-radius: 50%;
text-align: center;
line-height: 64px;
font-size: 32px;
"
>
🔒
</div>
</div>
<p>Hi {{first_name}},</p>
<p>Your Nxtgauge account password has been successfully changed.</p>
<div class="detail-card">
<div class="detail-row">
<span class="detail-label">Account</span>
<span class="detail-value">{{email}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Changed On</span>
<span class="detail-value">{{changed_at}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Status</span>
<span class="detail-value" style="color: #059669">✓ Successful</span>
</div>
</div>
<div
class="info-box"
style="background-color: #fee2e2; border-left-color: #dc2626"
>
<p class="info-box-title" style="color: #991b1b">⚠️ Didn't do this?</p>
<p style="margin: 0; color: #7f1d1d">
If you didn't change your password, please
<strong>contact support immediately</strong> to secure your account.
</p>
</div>
<div style="text-align: center">
<a href="{{security_url}}" class="cta-button">Account Security</a>
</div>
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>

View file

@ -0,0 +1,37 @@
<!-- Password Reset -->
<h1 class="email-title">Reset Your Password</h1>
<p>Hi {{first_name}},</p>
<p>
We received a request to reset your password. Click the button below to set a
new password:
</p>
<div style="text-align: center">
<a href="{{reset_url}}" class="cta-button">Reset Password</a>
</div>
<div class="info-box">
<p class="info-box-title">🔒 Security Notice</p>
<p style="margin: 0">
This link will expire in <strong>1 hour</strong>. If you didn't request
this, please ignore this email and your password will remain unchanged.
</p>
</div>
<p style="margin-top: 30px">
If the button doesn't work, copy and paste this link into your browser:
</p>
<p
style="
background: #f3f4f6;
padding: 10px;
border-radius: 4px;
font-size: 12px;
word-break: break-all;
"
>
{{reset_url}}
</p>
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>

View file

@ -0,0 +1,54 @@
<!-- Payment Successful -->
<h1 class="email-title">Payment Successful! 🎉</h1>
<p>Hi {{first_name}},</p>
<p>
Your payment has been processed successfully. Tracecoins have been added to
your wallet.
</p>
<div class="amount-display">
<div class="amount-label">Tracecoins Credited</div>
<div class="amount-value">
<span class="amount-currency">+</span>{{tracecoins_amount}}
</div>
</div>
<div class="detail-card">
<div class="detail-row">
<span class="detail-label">Package</span>
<span class="detail-value">{{package_name}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Amount Paid</span>
<span class="detail-value">₹{{amount_paid}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Transaction ID</span>
<span class="detail-value" style="font-family: monospace; font-size: 12px"
>{{transaction_id}}</span
>
</div>
<div class="detail-row">
<span class="detail-label">Date</span>
<span class="detail-value">{{payment_date}}</span>
</div>
</div>
<div style="text-align: center">
<a href="{{invoice_url}}" class="cta-button">View Invoice</a>
<a href="{{wallet_url}}" style="margin-left: 10px" class="cta-button"
>Go to Wallet</a
>
</div>
<div class="info-box">
<p class="info-box-title">📄 Tax Invoice</p>
<p style="margin: 0">
A GST invoice has been generated for this transaction. You can download it
from your account.
</p>
</div>
<p>Thank you for your purchase!</p>
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>

View file

@ -0,0 +1,70 @@
<!-- Policy Update -->
<h1 class="email-title">Important Policy Update</h1>
<div style="text-align: center; margin: 20px 0">
<div
style="
display: inline-block;
width: 80px;
height: 80px;
background-color: #dbeafe;
border-radius: 50%;
text-align: center;
line-height: 80px;
font-size: 40px;
"
>
📋
</div>
</div>
<p>Hi {{first_name}},</p>
<p>
We've updated our <strong>{{policy_type}}</strong>. Please review the changes
below.
</p>
<div
class="info-box"
style="background-color: #eff6ff; border-left-color: #3b82f6"
>
<p class="info-box-title" style="color: #1e40af">📝 Summary of Changes:</p>
<div style="color: #1e3a8a; margin: 0">{{changes_summary}}</div>
</div>
<div class="detail-card">
<div class="detail-row">
<span class="detail-label">Policy</span>
<span class="detail-value">{{policy_type}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Effective Date</span>
<span class="detail-value">{{effective_date}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Last Updated</span>
<span class="detail-value">{{updated_at}}</span>
</div>
</div>
<div
class="info-box"
style="background-color: #fef3c7; border-left-color: #d97706"
>
<p class="info-box-title" style="color: #92400e">⚠️ Action Required</p>
<p style="margin: 0; color: #78350f">
By continuing to use Nxtgauge after <strong>{{effective_date}}</strong>, you
agree to the updated {{policy_type}}. If you do not agree, please
discontinue using our services.
</p>
</div>
<div style="text-align: center">
<a href="{{policy_url}}" class="cta-button">Read Full {{policy_type}}</a>
</div>
<p style="margin-top: 20px; font-size: 14px; color: #6b7280">
Questions about these changes? Contact our support team.
</p>
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>

View file

@ -0,0 +1,36 @@
<!-- Profile Rejected -->
<h1 class="email-title">Profile Verification Update</h1>
<div style="text-align: center; margin: 20px 0">
<span class="status-badge status-rejected">✗ Rejected</span>
</div>
<p>Hi {{first_name}},</p>
<p>
We reviewed your {{role_name}} profile submission. Unfortunately, we couldn't
approve it at this time.
</p>
<div
class="info-box"
style="background-color: #fee2e2; border-left-color: #dc2626"
>
<p class="info-box-title" style="color: #991b1b">Reason for Rejection:</p>
<p style="margin: 0; color: #7f1d1d">{{rejection_reason}}</p>
</div>
<p><strong>What you can do:</strong></p>
<ul style="margin: 10px 0; padding-left: 20px">
<li>Review the feedback above</li>
<li>Update your profile with the required changes</li>
<li>Resubmit for verification</li>
</ul>
<div style="text-align: center">
<a href="{{profile_url}}" class="cta-button">Update Profile</a>
</div>
<p style="margin-top: 30px">
Need help? Contact our support team for assistance.
</p>
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>

View file

@ -0,0 +1,34 @@
<!-- Profile Verified -->
<h1 class="email-title">🎉 Your Profile is Verified!</h1>
<div style="text-align: center; margin: 20px 0">
<span class="status-badge status-approved">✓ Approved</span>
</div>
<p>Hi {{first_name}},</p>
<p>
Great news! Your {{role_name}} profile has been verified by our team. You now
have full access to all Nxtgauge features.
</p>
<div class="detail-card">
<div class="detail-row">
<span class="detail-label">Verification Date</span>
<span class="detail-value">{{verified_at}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Account Type</span>
<span class="detail-value">{{role_name}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Status</span>
<span class="detail-value" style="color: #059669">Active</span>
</div>
</div>
<div style="text-align: center">
<a href="{{dashboard_url}}" class="cta-button">Go to Dashboard</a>
</div>
<p style="margin-top: 30px">Welcome to the Nxtgauge community!</p>
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>

View file

@ -0,0 +1,45 @@
<!-- Requirement Approved -->
<h1 class="email-title">🎉 Your Requirement is Now Live!</h1>
<div style="text-align: center; margin: 20px 0">
<span class="status-badge status-approved">✓ Live</span>
</div>
<p>Hi {{first_name}},</p>
<p>
Great news! Your service requirement has been approved and is now live on
Nxtgauge.
</p>
<div class="detail-card">
<div class="detail-row">
<span class="detail-label">Requirement</span>
<span class="detail-value">{{requirement_title}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Service Type</span>
<span class="detail-value">{{profession_type}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Approved On</span>
<span class="detail-value">{{approved_at}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Expires On</span>
<span class="detail-value">{{expires_at}}</span>
</div>
</div>
<div style="text-align: center">
<a href="{{requirement_url}}" class="cta-button">View Requirement</a>
</div>
<div class="info-box">
<p class="info-box-title">💡 What happens now?</p>
<p style="margin: 0">
Professionals can now see your requirement and send you requests. You'll
receive an email each time someone is interested!
</p>
</div>
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>

View file

@ -0,0 +1,33 @@
<!-- Requirement Expired -->
<h1 class="email-title">Requirement Expired</h1>
<div style="text-align: center; margin: 20px 0">
<span class="status-badge status-expired">Expired</span>
</div>
<p>Hi {{first_name}},</p>
<p>
Your requirement has expired after 7 days and is no longer visible to
professionals.
</p>
<div class="detail-card">
<div class="detail-row">
<span class="detail-label">Requirement</span>
<span class="detail-value">{{requirement_title}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Status</span>
<span class="detail-value" style="color: #6b7280">Expired</span>
</div>
</div>
<div class="info-box">
<p class="info-box-title">📝 Create a New Requirement</p>
<p style="margin: 0">
You can create a new requirement anytime. Make sure to provide clear details
to attract the right professionals!
</p>
</div>
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>

View file

@ -0,0 +1,41 @@
<!-- Requirement Posted - Pending Approval -->
<h1 class="email-title">Requirement Submitted Successfully</h1>
<div style="text-align: center; margin: 20px 0">
<span class="status-badge status-pending">⏳ Pending Approval</span>
</div>
<p>Hi {{first_name}},</p>
<p>
Your service requirement has been submitted and is now under review by our
team.
</p>
<div class="detail-card">
<div class="detail-row">
<span class="detail-label">Requirement</span>
<span class="detail-value">{{requirement_title}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Service Type</span>
<span class="detail-value">{{profession_type}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Posted On</span>
<span class="detail-value">{{posted_at}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Status</span>
<span class="detail-value" style="color: #d97706">Pending Review</span>
</div>
</div>
<div class="info-box">
<p class="info-box-title">⏱️ What happens next?</p>
<p style="margin: 0">
Our team typically reviews requirements within <strong>24 hours</strong>.
Once approved, professionals can send you requests.
</p>
</div>
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>

View file

@ -0,0 +1,50 @@
<!-- Support Ticket Created -->
<h1 class="email-title">Support Ticket Received</h1>
<p>Hi {{first_name}},</p>
<p>
We've received your support request and our team will get back to you shortly.
</p>
<div class="detail-card">
<div class="detail-row">
<span class="detail-label">Ticket ID</span>
<span class="detail-value" style="font-family: monospace"
>#{{ticket_id}}</span
>
</div>
<div class="detail-row">
<span class="detail-label">Subject</span>
<span class="detail-value">{{subject}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Category</span>
<span class="detail-value">{{category}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Priority</span>
<span class="detail-value">{{priority}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Created</span>
<span class="detail-value">{{created_at}}</span>
</div>
</div>
<div class="info-box">
<p class="info-box-title">⏱️ Response Time</p>
<p style="margin: 0">
We typically respond to {{priority}} priority tickets within
<strong>{{response_time}}</strong>.
</p>
</div>
<div style="text-align: center">
<a href="{{ticket_url}}" class="cta-button">View Ticket</a>
</div>
<p style="margin-top: 20px; font-size: 14px; color: #6b7280">
You can reply to this ticket at any time by visiting your dashboard.
</p>
<p>Best regards,<br /><strong>The Nxtgauge Support Team</strong></p>

View file

@ -0,0 +1,50 @@
<!-- Support Ticket Replied -->
<h1 class="email-title">New Response on Your Ticket</h1>
<p>Hi {{first_name}},</p>
<p>Our support team has responded to your ticket.</p>
<div class="detail-card">
<div class="detail-row">
<span class="detail-label">Ticket ID</span>
<span class="detail-value" style="font-family: monospace"
>#{{ticket_id}}</span
>
</div>
<div class="detail-row">
<span class="detail-label">Subject</span>
<span class="detail-value">{{subject}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Status</span>
<span class="detail-value" style="color: #d97706"
>Awaiting Your Response</span
>
</div>
</div>
<div
class="info-box"
style="background-color: #eff6ff; border-left-color: #3b82f6"
>
<p class="info-box-title" style="color: #1e40af">💬 Latest Message:</p>
<p style="margin: 0; color: #1e3a8a; font-style: italic">
"{{latest_message}}"
</p>
<p style="margin: 10px 0 0 0; color: #3b82f6; font-size: 12px">
— {{support_agent_name}}
</p>
</div>
<div style="text-align: center">
<a href="{{ticket_url}}" class="cta-button">Reply to Ticket</a>
</div>
<p style="margin-top: 20px; font-size: 14px; color: #6b7280">
Please reply if you need further assistance or if the issue is resolved.
</p>
<p>
Best regards,<br /><strong>{{support_agent_name}}</strong><br />Nxtgauge
Support Team
</p>

View file

@ -0,0 +1,72 @@
<!-- Support Ticket Resolved -->
<h1 class="email-title">✅ Ticket Resolved</h1>
<div style="text-align: center; margin: 20px 0">
<div
style="
display: inline-block;
width: 80px;
height: 80px;
background-color: #d1fae5;
border-radius: 50%;
text-align: center;
line-height: 80px;
font-size: 40px;
"
>
</div>
</div>
<p>Hi {{first_name}},</p>
<p>Great news! Your support ticket has been resolved.</p>
<div class="detail-card">
<div class="detail-row">
<span class="detail-label">Ticket ID</span>
<span class="detail-value" style="font-family: monospace"
>#{{ticket_id}}</span
>
</div>
<div class="detail-row">
<span class="detail-label">Subject</span>
<span class="detail-value">{{subject}}</span>
</div>
<div class="detail-row">
<span class="detail-label">Status</span>
<span class="detail-value" style="color: #059669">✓ Resolved</span>
</div>
<div class="detail-row">
<span class="detail-label">Resolved On</span>
<span class="detail-value">{{resolved_at}}</span>
</div>
</div>
<div
class="info-box"
style="background-color: #d1fae5; border-left-color: #059669"
>
<p class="info-box-title" style="color: #065f46">💡 How did we do?</p>
<p style="margin: 0; color: #064e3b">
Your feedback helps us improve. If you have a moment, please let us know
about your support experience.
</p>
</div>
<div style="text-align: center">
<a href="{{feedback_url}}" style="margin-right: 10px" class="cta-button"
>Rate Experience</a
>
<a href="{{help_center_url}}" style="background: #6b7280" class="cta-button"
>Help Center</a
>
</div>
<p style="margin-top: 20px; font-size: 14px; color: #6b7280">
If you're still experiencing issues, you can reopen this ticket within 7 days.
</p>
<p>
Best regards,<br /><strong>{{support_agent_name}}</strong><br />Nxtgauge
Support Team
</p>

View file

@ -0,0 +1,36 @@
<!-- Email Verification OTP -->
<h1 class="email-title">Verify Your Email Address</h1>
<p>Hi {{first_name}},</p>
<p>
Please use the following One-Time Password (OTP) to verify your email address:
</p>
<div style="text-align: center; margin: 30px 0">
<div
style="
display: inline-block;
background: #f3f4f6;
padding: 20px 40px;
border-radius: 8px;
letter-spacing: 8px;
font-size: 32px;
font-weight: bold;
color: #111827;
font-family: monospace;
"
>
{{otp_code}}
</div>
</div>
<div class="info-box">
<p class="info-box-title">⏰ This code expires in 10 minutes</p>
<p style="margin: 0">
For security reasons, this OTP will expire after 10 minutes. If you didn't
request this, please ignore this email.
</p>
</div>
<p style="margin-top: 30px">
Best regards,<br /><strong>The Nxtgauge Team</strong>
</p>

View file

@ -0,0 +1,24 @@
<!-- Welcome / Registration Confirmation -->
<h1 class="email-title">Welcome to Nxtgauge, {{first_name}}! 🎉</h1>
<p>Thank you for joining Nxtgauge. We're excited to have you on board!</p>
<div class="info-box">
<p class="info-box-title">What's Next?</p>
<p>
Complete your profile and submit it for verification to unlock all features:
</p>
<ul style="margin: 10px 0; padding-left: 20px">
<li>Fill in your profile details</li>
<li>Upload required documents</li>
<li>Submit for verification</li>
</ul>
</div>
<div style="text-align: center">
<a href="{{dashboard_url}}" class="cta-button">Complete Your Profile</a>
</div>
<p style="margin-top: 30px">
If you have any questions, our support team is here to help.
</p>
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>

View file

@ -34,6 +34,7 @@ services:
# ── Gateway ───────────────────────────────────────────────────────────────
gateway:
platform: linux/amd64
image: ghcr.io/traceworks2023/nxtgauge-rust-gateway:high-performance-latest
ports:
- "9100:9100"
@ -43,8 +44,8 @@ services:
REDIS_URL: redis://redis:6379
JWT_SECRET: local_dev_jwt_secret
RUST_LOG: info
FRONTEND_URL: http://localhost:3000
ADMIN_URL: http://localhost:4000
FRONTEND_URL: http://localhost:9201
ADMIN_URL: http://localhost:9202
USERS_SERVICE_URL: http://users:9101
COMPANIES_SERVICE_URL: http://companies:9102
JOB_SEEKERS_SERVICE_URL: http://job-seekers:9104
@ -102,6 +103,7 @@ services:
# ── Core Services ─────────────────────────────────────────────────────────
users:
platform: linux/amd64
image: ghcr.io/traceworks2023/nxtgauge-rust-users:high-performance-latest
environment:
PORT: "9101"
@ -115,6 +117,7 @@ services:
condition: service_healthy
companies:
platform: linux/amd64
image: ghcr.io/traceworks2023/nxtgauge-rust-companies:high-performance-latest
environment:
PORT: "9102"
@ -128,6 +131,7 @@ services:
condition: service_healthy
job-seekers:
platform: linux/amd64
image: ghcr.io/traceworks2023/nxtgauge-rust-job-seekers:high-performance-latest
environment:
PORT: "9104"
@ -141,6 +145,7 @@ services:
condition: service_healthy
customers:
platform: linux/amd64
image: ghcr.io/traceworks2023/nxtgauge-rust-customers:high-performance-latest
environment:
PORT: "9105"
@ -154,6 +159,7 @@ services:
condition: service_healthy
employees:
platform: linux/amd64
image: ghcr.io/traceworks2023/nxtgauge-rust-employees:high-performance-latest
environment:
PORT: "9106"
@ -169,6 +175,7 @@ services:
# ── 9 Profession Services ─────────────────────────────────────────────────
photographers:
platform: linux/amd64
image: ghcr.io/traceworks2023/nxtgauge-rust-photographers:high-performance-latest
environment:
PORT: "9107"
@ -182,6 +189,7 @@ services:
condition: service_healthy
tutors:
platform: linux/amd64
image: ghcr.io/traceworks2023/nxtgauge-rust-tutors:high-performance-latest
environment:
PORT: "9108"
@ -195,6 +203,7 @@ services:
condition: service_healthy
makeup-artists:
platform: linux/amd64
image: ghcr.io/traceworks2023/nxtgauge-rust-makeup-artists:high-performance-latest
environment:
PORT: "9109"
@ -208,6 +217,7 @@ services:
condition: service_healthy
developers:
platform: linux/amd64
image: ghcr.io/traceworks2023/nxtgauge-rust-developers:high-performance-latest
environment:
PORT: "9110"
@ -221,6 +231,7 @@ services:
condition: service_healthy
video-editors:
platform: linux/amd64
image: ghcr.io/traceworks2023/nxtgauge-rust-video-editors:high-performance-latest
environment:
PORT: "9111"
@ -234,6 +245,7 @@ services:
condition: service_healthy
graphic-designers:
platform: linux/amd64
image: ghcr.io/traceworks2023/nxtgauge-rust-graphic-designers:high-performance-latest
environment:
PORT: "9112"
@ -247,6 +259,7 @@ services:
condition: service_healthy
social-media-managers:
platform: linux/amd64
image: ghcr.io/traceworks2023/nxtgauge-rust-social-media-managers:high-performance-latest
environment:
PORT: "9113"
@ -260,6 +273,7 @@ services:
condition: service_healthy
fitness-trainers:
platform: linux/amd64
image: ghcr.io/traceworks2023/nxtgauge-rust-fitness-trainers:high-performance-latest
environment:
PORT: "9114"
@ -273,6 +287,7 @@ services:
condition: service_healthy
catering-services:
platform: linux/amd64
image: ghcr.io/traceworks2023/nxtgauge-rust-catering-services:high-performance-latest
environment:
PORT: "9115"
@ -288,6 +303,7 @@ services:
# ── Payments ──────────────────────────────────────────────────────────────
payments:
platform: linux/amd64
image: ghcr.io/traceworks2023/nxtgauge-rust-payments:high-performance-latest
environment:
PORT: "9116"
@ -303,6 +319,7 @@ services:
# ── UGC ───────────────────────────────────────────────────────────────────
ugc-content-creators:
platform: linux/amd64
image: ghcr.io/traceworks2023/nxtgauge-rust-ugc-content-creators:high-performance-latest
environment:
PORT: "9117"

View file

@ -15,7 +15,7 @@ export let options = {
},
};
const BASE_URL = 'http://localhost:8000';
const BASE_URL = 'http://localhost:9100';
export default function () {
// Health check

View file

@ -19,7 +19,7 @@ export let options = {
maxRedirects: 5,
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:8000';
const BASE_URL = __ENV.BASE_URL || 'http://localhost:9100';
const ADMIN_TOKEN = __ENV.ADMIN_TOKEN || ''; // optional
// Custom metrics