Following the reference_numbers fix, audited every active (non .skip)
migration for the same failure shape — ALTER/CREATE TRIGGER against
lead_requests, tracecoin_wallets, tracecoin_ledger, or job_applications
without ever creating them — since this codebase has a repeated pattern
of table-creation migrations getting silently disabled (renamed .skip)
after the code that depends on them was already written. Found three
more, ALL already pushed to origin, meaning they've likely been
breaking the migration chain since the date each was deployed:
- 20260317195000_profession_specific_profiles.up.sql (Mar 17) — ALTERs
lead_requests twice. Earliest failure point found for the lead_requests
chain. Now self-creates a minimal lead_requests table first (no FK to
`leads`, which isn't created until June 10 — well after this migration).
- 20260318233000_tracecoin_ledger_immutable.up.sql (Mar 18) — creates
immutability triggers ON tracecoin_ledger, assuming it exists. Now
self-creates tracecoin_wallets/tracecoin_ledger first, matching the
column names (transaction_type/reference_type) the Rust code actually
uses — the only schema that ever existed for these tables (in a
disabled .skip migration) used stale names (type/reason).
- 20260425000000_ai_usage.up.sql (Apr 25) — ALTERs job_applications
inside a BEGIN/COMMIT block, so this failure was also rolling back
company_ai_usage/job_seeker_ai_usage creation in the same file. Now
self-creates job_applications first.
Each later migration that also touches these tables (this session's
customer/job-seeker/lead_requests fixes) now uses ADD COLUMN IF NOT
EXISTS instead of assuming its own CREATE TABLE ran, so the schema
converges to the same end state regardless of which migration actually
created the table first.
Every touched migration uses IF NOT EXISTS / idempotent guards
throughout, so this is safe to apply whether or not any of these tables
already exist in the real database.
Tracing "can a professional send a request to contact a customer"
surfaced two more breaks in the same family as everything else fixed
this session:
1. crates/db/migrations/20260719120000_reference_numbers.up.sql (not
yet pushed) unconditionally ran `ALTER TABLE job_applications ...`
and `ALTER TABLE lead_requests ...`. Neither table was ever created
by any active (non .skip) migration, so this migration would fail
outright on a clean apply and block every migration after it in the
chain — including all of this session's other fixes. Moved the
reference_number column/trigger/backfill logic for job_applications
and lead_requests into their own table-creation migrations instead,
where the tables are guaranteed to exist.
2. lead_requests (LeadRequestRepository, send_lead_request in
profession_shared.rs, list_requests/approve_request/reject_request
in apps/customers/src/handlers.rs) was only ever defined in disabled
.skip migrations, and even those didn't match the columns the
current Rust code reads/writes (missing professional_user_id and
reference_number; `message`/`customer_user_id`-only shape instead of
`remarks`/`requested_at`/`resolved_at`). Added a migration with the
schema the live code actually needs.
3. Same root cause found for tracecoin_wallets/tracecoin_ledger — only
ever defined in .skip migrations, and with stale column names
(`type`/`reason` vs. the `transaction_type`/`reference_type` the
Tracecoin reservation code in TracecoinWalletRepository actually
uses. These back the credit reservation that happens when a lead
request is sent. Created them with CREATE TABLE IF NOT EXISTS (safe
no-op if they already exist under any name/history) plus a
best-effort ADD COLUMN IF NOT EXISTS fallback for the stale-name
scenario.
NOTE: an already-deployed migration (20260318233000_tracecoin_ledger_immutable,
already on origin) creates triggers directly on tracecoin_ledger,
assuming it already exists. That migration was NOT touched here since
editing already-pushed migration history is out of scope for a blind
fix — if tracecoin_ledger doesn't already exist in the real database,
that migration has been failing since it was deployed and needs a
human to check `_sqlx_migrations` / actual schema state directly.
customer_profiles already had custom_data but never got a `status`
column, so — same root cause already fixed for job_seeker_profiles —
the generic profile save/get/submit handlers failed outright for the
CUSTOMER role, and the admin final-approval path couldn't write a
verified status either.
- Migration: add customer_profiles.status (default 'DRAFT').
- Special-case CUSTOMER in the generic profile.rs handlers (get_profile,
save_profile, fetch_saved_profile, set_profile_status), mirroring the
existing JOB_SEEKER special-case, storing basic-tab fields under
custom_data.basic_info.
- Re-enable the CUSTOMER branch in activate_profile_after_final_approval
now that the status column exists.
- Add the missing POST /api/customers/profile/documents upload endpoint
— the frontend's document upload (required: Aadhar/Government ID)
targets this exact path for CUSTOMER and previously 404'd since no
such route was ever registered. Mirrors the B2-upload-only pattern
used by the profession apps' shared upload_document handler.
Verified separately: requirement posting (POST /api/customers/requirements)
and requirement submission-for-verification (POST
/api/customers/requirements/:id/submit) already work correctly — both
use the `leads` table (an active migration, despite the model's
"Requirement" naming) and properly create a verification record
(case_type REQUIREMENT_APPROVAL) that lands in admin Verification
Management via the existing approve_requirement/reject_requirement
handlers. No changes needed there.
The generic profile save/get/submit handlers (apps/users/src/handlers/profile.rs)
have always read and written every non-COMPANY/JOB_SEEKER role's table via
`... WHERE user_role_profile_id = $1`, but that linking column was never
migrated onto any of the 10 profession tables (photographer_profiles,
tutor_profiles, makeup_artist_profiles, developer_profiles,
video_editor_profiles, graphic_designer_profiles,
social_media_manager_profiles, fitness_trainer_profiles,
catering_service_profiles, ugc_content_creator_profiles) — it only existed
in a migration that was disabled (20260415010003, .skip). Every basic-info
save, document upload persistence, and verification submission for these
professions has therefore always failed with "column
user_role_profile_id does not exist".
Adds the column (+ backfill from existing user_role_profiles rows, +
index) to all 10 tables. No application code changes needed — the Rust
handlers were already written correctly for this schema.
- Company approval wrote profile status to 'ACTIVE' (company_profiles'
own pre-verification default) using an id column that never matched
any row, so create_job's APPROVED check always rejected newly
approved companies. Match on user_id for user_id-keyed tables and
write the canonical 'APPROVED' status.
- job_seeker_profiles was missing columns the job-seeker app has
always queried (full_name, location, summary, skills,
active_application_count, status), and the job_applications /
job_seeker_documents tables it depends on were never migrated in —
job seeker profile save/submit and job applications failed outright
with "column/relation does not exist".
- Renamed the job_seeker first_name/last_name split to full_name to
match what the frontend has always sent.
- Special-cased JOB_SEEKER in the generic profile.rs handlers (mirrors
the existing COMPANY special-case) so the shared ProfilePage save/
submit flow, which was routed through a user_role_profile_id-based
path job_seeker_profiles never had, now persists correctly.
- Fixed apply_to_job's company notification query joining a
nonexistent "companies" table instead of company_profiles.
- Fixed auto-apply cron's company status filter to match the
corrected 'APPROVED' status.
Adds a DB-trigger-generated reference_number (NXT-{TYPE}-{YY}-{000001}) to
verifications, support_tickets, payments, job_applications, lead_requests,
and users, replacing raw UUIDs shown to customers/admins. Also fixes
verification-status endpoint to return uploaded documents (previously
omitted, so documents never appeared after submission), and adds a
reference-number lookup endpoint for the AI support assistant.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CRITICAL: 20260402030000_strict_employee_separation.up.sql contained an
unconditional DROP TABLE IF EXISTS employees CASCADE followed by a bare
CREATE TABLE, written as a one-time schema transformation back when it was
authored. The db-migrate tool has no applied-migrations tracking table - it
replays every .sql file on every run - which turned that one-time DROP into
a destructive operation that wipes every employee account (including admin
accounts) on every single migration job run. This is what caused today's
"db error while logging in": the phone column (never present in any tracked
migration, added out-of-band in production at some point) was gone after the
recreate, and every employee row - including the account in use this session
- was deleted.
Fix: make the table creation a plain idempotent CREATE TABLE IF NOT EXISTS
(the standalone-schema transition it performed already happened in
production long ago, so the drop was never needed for correctness going
forward). Add a proper migration for the phone column so it's tracked
instead of relying on an undocumented manual ALTER.
Confirmed via kubectl-verified row count (0) and a pre-incident backup
(2026-07-18T21:00:03Z, predates the destructive run) that the deleted row
is recoverable; restoring it separately as a one-time data fix, not part of
this schema migration.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same class of bug as the ai_plans_and_limits fix: ai_credit_packages and
users_litellm_key used plain CREATE TABLE/ADD COLUMN with no re-run guard,
and payu_rename_columns did a bare RENAME COLUMN that fails outright on any
second run ("column razorpay_order_id does not exist"). All three were
discovered by actually running the db-migrate job end to end for the first
time and fixed in the same pass as the verification_logs FK fix - already
baked into the db-migrate image that was built and run manually, this
commit just brings the source in the repo in sync with what's deployed.
Also: apps/employees login handler's DB error was being discarded via
.map_err(|_| ...) with zero logging, making the reported "db error while
logging in" impossible to diagnose from pod logs. Log the real error.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The db-migrate job (crates/db-migrate) has no applied-migrations tracking
table - it replays every .sql file on every run, relying on each file being
idempotent (IF NOT EXISTS / ON CONFLICT), which is the pattern virtually
every other migration in this directory follows. This one wasn't: plain
CREATE TABLE/CREATE INDEX and two INSERTs with no ON CONFLICT guard.
Since the tables/data already exist from this file's one successful run,
every subsequent migration job run failed immediately on
"relation ai_plans already exists" - before ever reaching any migration
after it, including ones genuinely needed (see the verification_logs FK fix
two commits back). Confirmed via a live job run: this was the actual reason
db-migrate had never completed successfully before.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
scripts/init-db.sql created verification_logs.verification_request_id with a
foreign key against verification_requests(id) - an unrelated legacy table.
VerificationRepository::update_status inserts the verifications.id (the row
actually being approved/rejected) into that column on every status change,
which has been violating the FK on every single call:
"insert or update on table verification_logs violates foreign key
constraint verification_logs_verification_request_id_fkey"
This made every Approve/Reject click in Verification Management 500,
confirmed via the browser's actual response body. Drop and recreate the
constraint to point at verifications(id), which is what the code has always
actually been logging against.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Admin access tokens expire after 15 minutes with no way to renew one, so
active admins got logged out mid-work with no warning (silent 401s, now
surfaced by admin-solid's session-expired dialog). Add a refresh endpoint
that exchanges the HttpOnly nxtgauge_admin_token cookie for a new 15-minute
access token, rotating the employee_sessions row (revoke old, store new) -
mirrors the existing pattern in apps/users/src/handlers/auth.rs, but against
the DB-backed employee_sessions table instead of Redis.
Add EmployeeRepository::get_by_id / get_valid_session_by_token / revoke_session
to support it.
The admin-solid frontend calls this on a timer while the admin is active and
skips it once idle for 15 minutes, so the session now extends while active
and expires on inactivity as intended, instead of on a fixed wall-clock timer.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- companies/admin: add proper POST /jobs/{id}/approve and /jobs/{id}/reject
endpoints (sets status=LIVE, not direct SQL bypass)
- customers: fix list_requests to query by customer_user_id (not professional),
add optional lead_id filter; fix debit to use professional_user_id
- payments: switch razorpay_order_id column to payu_txnid (PayU migration)
- users/auth: fix role resolution to not inject phantom roles for professionals
- contracts/profession_shared: fix my_requests SQL to join leads+users instead
of nonexistent requirements table
- db/job_seeker: fix INSERT VALUES placeholder count (add missing $10)
- storage: add MOCK_STORAGE=true mode for local dev without real B2 creds
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Several migrations reference a professionals table that was replaced by
per-profession profile tables in 20260317195000. Rename to .skip so
sqlx migrate run succeeds on a fresh local dev database. Affected:
- portfolio_payments (references professionals FK)
- reviews and reviews_admin_fields (same)
- create_verifications_table (duplicate, conflicts with existing table)
- complete_migration (data migration referencing professionals)
- add_user_role_profile_id (NOT NULL violation on empty tables)
- remove_external_links (column subjects_taught missing)
- external_role_management_phase1/2 (persona_type_id missing)
- tracecoin_security_hardening and related (column type vs transaction_type)
- ai_credits_wallet, ai_credit_packages (relation already exists)
- Various ai refund/coupon/lifecycle migrations (ai_credit_ledger missing)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
register() now generates a per-account LiteLLM key (best-effort, non-blocking)
and stores it on the user. New internal endpoint GET /internal/users/{id}/llm-key
lets other services fetch (or lazily backfill) an account's key, authenticated
via the existing X-AI-Service-Key shared secret.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
tracing::error!("...: {}", e) on an anyhow::Error only prints the outermost
.context() message ("B2 upload failed") — the actual root cause (auth
failure, DNS, timeout, bad bucket, etc.) from the AWS SDK is swallowed.
Switching to {:?} prints the full chain so the real failure is visible in
logs instead of a message that just repeats itself.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
All three queries selected role_key AS profession_key, but the Professional
struct's field is named role_key — sqlx's FromRow derive matches by column
name, so every call (get_by_user_id, submit_for_verification, and its
UPDATE...RETURNING) failed at runtime with "no column found for name:
role_key". This is the professional-profile existence check used by
document upload, portfolio, and submission endpoints across all 10
profession services — document upload was 500ing for every professional
role because of this.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Employees (internal admin/staff) had no self-service password reset —
only /login, /logout, /session existed. Adds /api/admin/auth/forgot-password
and /api/admin/auth/reset-password, mirroring the existing users-table flow
but against EmployeeRepository and a distinct Redis key namespace
(reset:employee:*) so a code for one identity store can never be consumed
against the other.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The admin/employee login handler had no brute-force protection, unlike
the regular user login path. Given these accounts hold internal/
super-admin privileges, add a tighter limit (5 attempts/15min vs 10
for regular users) using the existing sliding-window Redis limiter.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Restore deleted module declarations (tutor, ugc_content_creator, user,
user_role_profile, verification, video_editor) in db/models/mod.rs
- Add mod ai; to users/src/main.rs (fixes crate::ai import)
- Add pub mod ai_auto; to handlers/mod.rs
- Add rust_decimal dependency to payments/Cargo.toml
- Fix missing PgPool import in payments/src/ai_credits.rs
- Make LiteLLMUsage fields public in users/src/litellm.rs
- Add get_llm_base_url() and get_llm_model() helper functions
- Remove leading underscore from variables that are used
Partial fix for high-performance branch build issues.
- Add AI plans, credits, model routing, LiteLLM client, and orchestrator services
- Add AI management endpoints, auto-apply/auto-request handlers, and log endpoints
- Add cron jobs for daily action reset and monthly credit reset
- Add AI credit purchase flow in payments service
- Add ai_credit_packages migration with seed data
- Update Dockerfile build tooling across services
- Auto-verifies emails for accounts ending with @demo.com
- Auto-approves COMPANY role for demo accounts
- Skips email verification and OTP for demo accounts
- Auto-approves profile verification for demo accounts
- Allows login without email verification for demo accounts
This enables payment gateway companies to login directly and view packages.
DB:
- Add niche_tags column to ugc_content_creator_profiles (was blocking UGC service)
- Add turnaround_days and fix user_role_profile_id NOT NULL for UGC
- leads/lead_requests tables (already created in session 1)
Code:
- Add UGC_CONTENT_CREATOR to is_professional_role() to auto-create user_role_profiles
- Fix onboarding INSERT to include user_id for photographer_profiles
- Fix send_lead_request_ai to use correct customer_user_id (was self-notifying)
- Add PATCH /api/leads/:id support + mount leads at /api/* for gateway compatibility
- Fix admin_list_cases query (WHERE was using wrong params)
- Fix admin_get_case query (was using list query instead of fetch-by-id)
- Add GET /api/me in profile.rs (moved from onboarding)
- Add KB articles by ID route /api/kb/articles/id/{id}
- Rewrite reviews handlers to match actual reviews table schema
- Add public reviews router GET /api/reviews
Gateway:
- Add /api/reviews route to users service
- payments/src/main.rs: fail-fast on BEECEPTOR_URL and DATABASE_URL
- gateway/src/main.rs: fail-fast on all service URLs and CORS URLs
- users/src/handlers/ai.rs: fail-fast on LEADS_SERVICE_URL
- leads/src/main.rs: fail-fast on OLLAMA_BASE_URL and OLLAMA_CHAT_MODEL
- storage/Cargo.toml: replace rustls-aws-lc with rustls for aws-config/aws-sdk-s3
- Update jsonwebtoken from 9.3 to 10.3 in crates/auth/Cargo.toml and crates/contracts/Cargo.toml
- Create .cargo/audit.toml to ignore false positives for local workspace crates 'cache' and 'users'
- Fix pre-existing compile errors in crates/cache/src/ollama.rs (missing reqwest dep, broken format! string literals)
- Add reqwest workspace dependency to crates/cache/Cargo.toml
- Add AI credit management endpoints for companies
- Add AI usage history tracking
- Add AI content generation with Ollama integration
- Add Ollama client for generating job descriptions, resume analysis, and cover letters
- Integrate AI router into companies service
- Generate 6-digit code instead of UUID token for password reset
- Store in Redis with 15 min TTL (was 1 hour)
- Update email template to show code instead of reset link
- Update ResetPasswordPayload to accept code instead of token
- Update send_password_reset_email to accept code parameter
- Add cache::ai module with Redis rate limiting for AI generations
- Add functions: check_ai_rate_limit, get_ai_usage, cache_ai_response,
get_cached_ai_response, invalidate_ai_cache, reset_daily_usage
- Update check_and_increment_usage to use Redis fast-path before DB
- Redis key pattern: ai:rate:{user_id} for 24hr sliding window counter
- Fix gateway: add /api/ai route to users_url
- Add AI job field generation endpoints (generate-job-field, generate-cover-letter, tailor-resume, auto-apply)
- Add AI usage tracking and rate limiting
- Add professional auto-respond-to-lead endpoint (30 tracecoins)
- Add DB migrations for AI usage tracking tables
- Update leads service with AI auto-respond functionality
- models/user.rs: ORDER BY ur.created_at DESC so most recently assigned role is returned first
- handlers/auth.rs: resolve_signup_role_candidates returns empty vec instead of JOB_SEEKER when no valid intent
- Change company name from 'Nxtgauge Technologies Pvt. Ltd.' to 'Traceworks Technologies LLP'
- Update address from Bangalore to: 13th main road, Anna nagar west, Chennai - 600040
- Remove GSTIN field from footer
- Replace text 'NXTGAUGE' with actual logo image in email header
- Use hosted logo URL: https://nxtgauge.com/nxtgauge-logo.png
- Copy logo to email/public directory for future use
- Remove phone from INSERT INTO users (users table has no phone column)
- Remove phone from User struct and CreateUserPayload
- Return null for phone in API responses
- Keep phone field in RegisterPayload for backward compat (just not persisted)
- companies: user.name in email and contact queries
- customers: user.name in email
- job_seekers: u.name in company user query
- cron tasks (jobs/leads/requirements): use u.name instead of u.full_name
- contracts/profession_shared: u.name for customer_name fields