fix: notification insert used wrong column name; log silent email failures
All checks were successful
build-and-release / build (catering-services) (push) Successful in 15s
build-and-release / build (cron) (push) Successful in 13s
build-and-release / build (developers) (push) Successful in 10s
build-and-release / build (employees) (push) Successful in 8s
build-and-release / build (fitness-trainers) (push) Successful in 5s
build-and-release / build (gateway) (push) Successful in 5s
build-and-release / build (graphic-designers) (push) Successful in 5s
build-and-release / build (job-seekers) (push) Successful in 5s
build-and-release / build (jobs) (push) Successful in 6s
build-and-release / build (payments) (push) Successful in 4s
build-and-release / build (makeup-artists) (push) Successful in 6s
build-and-release / build (photographers) (push) Successful in 5s
build-and-release / build (social-media-managers) (push) Successful in 6s
build-and-release / build (tutors) (push) Successful in 6s
build-and-release / build (ugc-content-creators) (push) Successful in 6s
build-and-release / build (video-editors) (push) Successful in 6s
build-and-release / build (companies) (push) Successful in 2m9s
build-and-release / build (customers) (push) Successful in 2m47s
build-and-release / build (users) (push) Successful in 3m16s

Found while investigating "notifications/emails not working on approve
or job posting":

1. Real bug: apps/companies/src/handlers/mod.rs::view_contact (company
   viewing an applicant's contact info) inserted into notifications
   using column name `notification_type`, which has never existed —
   the column is `type`. This INSERT has been failing outright every
   time a company views a contact.

2. Root cause for approvals specifically: verifications/approval_requests
   never existed until earlier this session (see
   20260718200000_create_verifications_and_approvals) — every admin
   approve/reject action was failing at the DB layer before it ever
   reached the notification/email code, so nothing in this area could
   have worked regardless of the email/notification logic itself.

3. Observability gap: every `state.mail.send_*_email(...)` call site
   silently discarded its Result (`let _ = ...`), so if the SMTP/
   Zeptomail provider is unconfigured (crates/email::Mailer already
   logs a clear warning at startup for that, but callers gave no
   per-send signal) or a send fails for any other reason, there was no
   way to see it happen. Added `tracing::error!` logging on failure
   for every job/approval-related email: job submitted, job approved,
   job rejected, requirement approved, profile approval
   approved/rejected, requirement submitted. Doesn't change delivery —
   if the environment has no EMAIL_PROVIDER/SMTP_*/ZEPTOMAIL_*
   configured, sends still fail, but that failure is now visible in
   logs instead of silent.

In-app notifications for approvals were already schema-correct
(job/profile/requirement approve+reject all insert into notifications
with the right columns) — the two real defects were #1 and #2 above.
This commit is contained in:
Ashwin Kumar Sivakumar 2026-07-21 04:30:34 +05:30
parent c570d7df67
commit 92ce2d2a86
4 changed files with 22 additions and 10 deletions

View file

@ -326,9 +326,10 @@ async fn submit_job(
match JobRepository::update_status(&state.pool, job.id, "PENDING_APPROVAL").await {
Ok(updated) => {
// Fire email to company user (ignore failures)
if let Ok(user) = UserRepository::get_by_id(&state.pool, auth.user_id).await {
let _ = state.mail.send_job_submitted_email(&user.email, &format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default()), &updated.title).await;
if let Err(e) = state.mail.send_job_submitted_email(&user.email, &format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default()), &updated.title).await {
tracing::error!("Failed to send job-submitted email to {}: {:?}", user.email, e);
}
}
// Create verification case so the request appears in Verification Management first.
@ -714,7 +715,7 @@ async fn view_contact(
let _ = sqlx::query(
r#"
INSERT INTO notifications (user_id, title, body, notification_type, reference_id)
INSERT INTO notifications (user_id, title, body, type, reference_id)
VALUES ($1, $2, $3, $4, $5)
"#
)

View file

@ -267,9 +267,10 @@ async fn submit_requirement(
match RequirementRepository::update_status(&state.pool, req.id, "PENDING_APPROVAL").await {
Ok(updated) => {
// Fire email to customer (ignore failures)
if let Ok(user) = UserRepository::get_by_id(&state.pool, auth.user_id).await {
let _ = state.mail.send_requirement_submitted_email(&user.email, &format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default()), &updated.title).await;
if let Err(e) = state.mail.send_requirement_submitted_email(&user.email, &format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default()), &updated.title).await {
tracing::error!("Failed to send requirement-submitted email to {}: {:?}", user.email, e);
}
}
// Create verification case so this request enters Verification Management first.

View file

@ -456,7 +456,9 @@ async fn approve_job(
.await;
if let Ok(Some((name, email, user_uuid))) = company_info {
let _ = state.mail.send_job_approved_email(&email, &name, &existing.title).await;
if let Err(e) = state.mail.send_job_approved_email(&email, &name, &existing.title).await {
tracing::error!("Failed to send job-approved email to {}: {:?}", email, e);
}
// Send in-app notification to company
sqlx::query(
@ -521,7 +523,9 @@ async fn reject_job(
if let Ok(Some((name, email, user_uuid))) = company_info {
let r = payload.reason.as_deref().unwrap_or("Rejected by admin");
let _ = state.mail.send_job_rejected_email(&email, &name, &existing.title, r).await;
if let Err(e) = state.mail.send_job_rejected_email(&email, &name, &existing.title, r).await {
tracing::error!("Failed to send job-rejected email to {}: {:?}", email, e);
}
// Send in-app notification to company
sqlx::query(
@ -594,7 +598,9 @@ async fn approve_requirement(
if let Some(user_id) = req.created_by_user_id {
if let Ok(user) = UserRepository::get_by_id(&state.pool, user_id).await {
let name = format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default());
let _ = state.mail.send_requirement_approved_email(&user.email, &name, &req.title).await;
if let Err(e) = state.mail.send_requirement_approved_email(&user.email, &name, &req.title).await {
tracing::error!("Failed to send requirement-approved email to {}: {:?}", user.email, e);
}
}
}

View file

@ -172,7 +172,9 @@ async fn trigger_rejection(
if let Ok(user) = db::models::user::UserRepository::get_by_id(&state.pool, user_id).await {
let display = role_key_to_display(&role_key);
let user_name = format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default());
let _ = state.mail.send_approval_rejected_email(&user.email, &user_name, &display, reason_str).await;
if let Err(e) = state.mail.send_approval_rejected_email(&user.email, &user_name, &display, reason_str).await {
tracing::error!("Failed to send approval-rejected email to {}: {:?}", user.email, e);
}
}
// Send in-app notification
@ -226,7 +228,9 @@ async fn approve_verification(
let display = role_key_to_display(&v.role_key);
let user_name = format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default());
// Use a "verification passed" notification instead of final approval
let _ = state.mail.send_approval_approved_email(&user.email, &user_name, &display).await;
if let Err(e) = state.mail.send_approval_approved_email(&user.email, &user_name, &display).await {
tracing::error!("Failed to send approval-approved email to {}: {:?}", user.email, e);
}
}
// Send in-app notification - profile verified, pending final approval