From 70e63c6affc8d35959181a2b271e773adf4d24c1 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Sivakumar Date: Tue, 21 Jul 2026 02:00:23 +0530 Subject: [PATCH] fix: lead_requests table never existed, and reference_numbers migration was unconditionally broken MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../20260719120000_reference_numbers.down.sql | 8 -- .../20260719120000_reference_numbers.up.sql | 57 +--------- ...b_seeker_profile_and_applications.down.sql | 2 + ...job_seeker_profile_and_applications.up.sql | 39 +++++++ ...260721030000_create_lead_requests.down.sql | 7 ++ ...20260721030000_create_lead_requests.up.sql | 102 ++++++++++++++++++ 6 files changed, 155 insertions(+), 60 deletions(-) create mode 100644 crates/db/migrations/20260721030000_create_lead_requests.down.sql create mode 100644 crates/db/migrations/20260721030000_create_lead_requests.up.sql diff --git a/crates/db/migrations/20260719120000_reference_numbers.down.sql b/crates/db/migrations/20260719120000_reference_numbers.down.sql index 6008698..124ffbb 100644 --- a/crates/db/migrations/20260719120000_reference_numbers.down.sql +++ b/crates/db/migrations/20260719120000_reference_numbers.down.sql @@ -1,27 +1,19 @@ DROP TRIGGER IF EXISTS trg_verifications_reference_number ON verifications; DROP TRIGGER IF EXISTS trg_support_tickets_reference_number ON support_tickets; DROP TRIGGER IF EXISTS trg_payments_reference_number ON payments; -DROP TRIGGER IF EXISTS trg_job_applications_reference_number ON job_applications; -DROP TRIGGER IF EXISTS trg_lead_requests_reference_number ON lead_requests; DROP TRIGGER IF EXISTS trg_users_reference_number ON users; DROP FUNCTION IF EXISTS set_verification_reference_number(); DROP FUNCTION IF EXISTS set_ticket_reference_number(); DROP FUNCTION IF EXISTS set_payment_reference_number(); -DROP FUNCTION IF EXISTS set_job_application_reference_number(); -DROP FUNCTION IF EXISTS set_lead_request_reference_number(); DROP FUNCTION IF EXISTS set_user_reference_number(); ALTER TABLE verifications DROP COLUMN IF EXISTS reference_number; ALTER TABLE support_tickets DROP COLUMN IF EXISTS reference_number; ALTER TABLE payments DROP COLUMN IF EXISTS reference_number; -ALTER TABLE job_applications DROP COLUMN IF EXISTS reference_number; -ALTER TABLE lead_requests DROP COLUMN IF EXISTS reference_number; ALTER TABLE users DROP COLUMN IF EXISTS reference_number; DROP SEQUENCE IF EXISTS verification_number_seq; DROP SEQUENCE IF EXISTS ticket_number_seq; DROP SEQUENCE IF EXISTS payment_number_seq; -DROP SEQUENCE IF EXISTS job_application_number_seq; -DROP SEQUENCE IF EXISTS lead_request_number_seq; DROP SEQUENCE IF EXISTS user_number_seq; diff --git a/crates/db/migrations/20260719120000_reference_numbers.up.sql b/crates/db/migrations/20260719120000_reference_numbers.up.sql index 92b8e6a..3126c4b 100644 --- a/crates/db/migrations/20260719120000_reference_numbers.up.sql +++ b/crates/db/migrations/20260719120000_reference_numbers.up.sql @@ -2,6 +2,11 @@ -- Format: NXT-{TYPE}-{YY}-{000001}. Generated via BEFORE INSERT trigger so every -- existing and future insert path (there are many, scattered across apps) picks -- one up automatically without needing per-call-site application changes. +-- +-- job_applications and lead_requests are handled in their own creation +-- migrations instead of here — this file used to ALTER those tables +-- unconditionally, which broke the whole migration chain because neither +-- table was ever actually created by any active (non .skip) migration. -- ============ verifications ============ CREATE SEQUENCE IF NOT EXISTS verification_number_seq; @@ -57,42 +62,6 @@ CREATE TRIGGER trg_payments_reference_number BEFORE INSERT ON payments FOR EACH ROW EXECUTE FUNCTION set_payment_reference_number(); --- ============ job_applications ============ -CREATE SEQUENCE IF NOT EXISTS job_application_number_seq; -ALTER TABLE job_applications ADD COLUMN IF NOT EXISTS reference_number VARCHAR; - -CREATE OR REPLACE FUNCTION set_job_application_reference_number() RETURNS TRIGGER AS $$ -BEGIN - IF NEW.reference_number IS NULL THEN - NEW.reference_number := 'NXT-JOB-' || to_char(NOW(), 'YY') || '-' || lpad(nextval('job_application_number_seq')::text, 6, '0'); - END IF; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -DROP TRIGGER IF EXISTS trg_job_applications_reference_number ON job_applications; -CREATE TRIGGER trg_job_applications_reference_number - BEFORE INSERT ON job_applications - FOR EACH ROW EXECUTE FUNCTION set_job_application_reference_number(); - --- ============ lead_requests ============ -CREATE SEQUENCE IF NOT EXISTS lead_request_number_seq; -ALTER TABLE lead_requests ADD COLUMN IF NOT EXISTS reference_number VARCHAR; - -CREATE OR REPLACE FUNCTION set_lead_request_reference_number() RETURNS TRIGGER AS $$ -BEGIN - IF NEW.reference_number IS NULL THEN - NEW.reference_number := 'NXT-LED-' || to_char(NOW(), 'YY') || '-' || lpad(nextval('lead_request_number_seq')::text, 6, '0'); - END IF; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -DROP TRIGGER IF EXISTS trg_lead_requests_reference_number ON lead_requests; -CREATE TRIGGER trg_lead_requests_reference_number - BEFORE INSERT ON lead_requests - FOR EACH ROW EXECUTE FUNCTION set_lead_request_reference_number(); - -- ============ users ============ CREATE SEQUENCE IF NOT EXISTS user_number_seq; ALTER TABLE users ADD COLUMN IF NOT EXISTS reference_number VARCHAR; @@ -127,14 +96,6 @@ BEGIN UPDATE payments SET reference_number = 'NXT-PAY-' || to_char(r.created_at, 'YY') || '-' || lpad(nextval('payment_number_seq')::text, 6, '0') WHERE id = r.id; END LOOP; - FOR r IN SELECT id, created_at FROM job_applications WHERE reference_number IS NULL ORDER BY created_at, id LOOP - UPDATE job_applications SET reference_number = 'NXT-JOB-' || to_char(r.created_at, 'YY') || '-' || lpad(nextval('job_application_number_seq')::text, 6, '0') WHERE id = r.id; - END LOOP; - - FOR r IN SELECT id, created_at FROM lead_requests WHERE reference_number IS NULL ORDER BY created_at, id LOOP - UPDATE lead_requests SET reference_number = 'NXT-LED-' || to_char(r.created_at, 'YY') || '-' || lpad(nextval('lead_request_number_seq')::text, 6, '0') WHERE id = r.id; - END LOOP; - FOR r IN SELECT id, created_at FROM users WHERE reference_number IS NULL ORDER BY created_at, id LOOP UPDATE users SET reference_number = 'NXT-USR-' || to_char(r.created_at, 'YY') || '-' || lpad(nextval('user_number_seq')::text, 6, '0') WHERE id = r.id; END LOOP; @@ -144,8 +105,6 @@ END $$; ALTER TABLE verifications ALTER COLUMN reference_number SET NOT NULL; ALTER TABLE support_tickets ALTER COLUMN reference_number SET NOT NULL; ALTER TABLE payments ALTER COLUMN reference_number SET NOT NULL; -ALTER TABLE job_applications ALTER COLUMN reference_number SET NOT NULL; -ALTER TABLE lead_requests ALTER COLUMN reference_number SET NOT NULL; ALTER TABLE users ALTER COLUMN reference_number SET NOT NULL; DO $$ @@ -159,12 +118,6 @@ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'payments_reference_number_key') THEN ALTER TABLE payments ADD CONSTRAINT payments_reference_number_key UNIQUE (reference_number); END IF; - IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'job_applications_reference_number_key') THEN - ALTER TABLE job_applications ADD CONSTRAINT job_applications_reference_number_key UNIQUE (reference_number); - END IF; - IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'lead_requests_reference_number_key') THEN - ALTER TABLE lead_requests ADD CONSTRAINT lead_requests_reference_number_key UNIQUE (reference_number); - END IF; IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'users_reference_number_key') THEN ALTER TABLE users ADD CONSTRAINT users_reference_number_key UNIQUE (reference_number); END IF; diff --git a/crates/db/migrations/20260721000000_fix_job_seeker_profile_and_applications.down.sql b/crates/db/migrations/20260721000000_fix_job_seeker_profile_and_applications.down.sql index 323322c..2077e1b 100644 --- a/crates/db/migrations/20260721000000_fix_job_seeker_profile_and_applications.down.sql +++ b/crates/db/migrations/20260721000000_fix_job_seeker_profile_and_applications.down.sql @@ -1,5 +1,7 @@ DROP TABLE IF EXISTS job_seeker_documents; DROP TABLE IF EXISTS job_applications; +DROP FUNCTION IF EXISTS set_job_application_reference_number(); +DROP SEQUENCE IF EXISTS job_application_number_seq; ALTER TABLE job_seeker_profiles DROP COLUMN IF EXISTS full_name, diff --git a/crates/db/migrations/20260721000000_fix_job_seeker_profile_and_applications.up.sql b/crates/db/migrations/20260721000000_fix_job_seeker_profile_and_applications.up.sql index d8e07df..e5e4413 100644 --- a/crates/db/migrations/20260721000000_fix_job_seeker_profile_and_applications.up.sql +++ b/crates/db/migrations/20260721000000_fix_job_seeker_profile_and_applications.up.sql @@ -29,6 +29,45 @@ CREATE INDEX IF NOT EXISTS idx_job_applications_job_id ON job_applications(job_i CREATE INDEX IF NOT EXISTS idx_job_applications_applicant_user_id ON job_applications(applicant_user_id); CREATE INDEX IF NOT EXISTS idx_job_applications_status ON job_applications(status); +-- Human-readable reference number (NXT-JOB-YY-000001), same scheme as +-- verifications/payments/etc — kept here instead of in +-- 20260719120000_reference_numbers.up.sql because that migration ran before +-- this table existed and unconditionally ALTERing a nonexistent table broke +-- the whole migration chain. +CREATE SEQUENCE IF NOT EXISTS job_application_number_seq; +ALTER TABLE job_applications ADD COLUMN IF NOT EXISTS reference_number VARCHAR; + +CREATE OR REPLACE FUNCTION set_job_application_reference_number() RETURNS TRIGGER AS $$ +BEGIN + IF NEW.reference_number IS NULL THEN + NEW.reference_number := 'NXT-JOB-' || to_char(NOW(), 'YY') || '-' || lpad(nextval('job_application_number_seq')::text, 6, '0'); + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS trg_job_applications_reference_number ON job_applications; +CREATE TRIGGER trg_job_applications_reference_number + BEFORE INSERT ON job_applications + FOR EACH ROW EXECUTE FUNCTION set_job_application_reference_number(); + +DO $$ +DECLARE r RECORD; +BEGIN + FOR r IN SELECT id, created_at FROM job_applications WHERE reference_number IS NULL ORDER BY created_at, id LOOP + UPDATE job_applications SET reference_number = 'NXT-JOB-' || to_char(r.created_at, 'YY') || '-' || lpad(nextval('job_application_number_seq')::text, 6, '0') WHERE id = r.id; + END LOOP; +END $$; + +ALTER TABLE job_applications ALTER COLUMN reference_number SET NOT NULL; + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'job_applications_reference_number_key') THEN + ALTER TABLE job_applications ADD CONSTRAINT job_applications_reference_number_key UNIQUE (reference_number); + END IF; +END $$; + -- job_seeker_documents backs JobSeekerRepository::create_document/list_documents/ -- delete_document, referenced from apps/job_seekers but never migrated in. CREATE TABLE IF NOT EXISTS job_seeker_documents ( diff --git a/crates/db/migrations/20260721030000_create_lead_requests.down.sql b/crates/db/migrations/20260721030000_create_lead_requests.down.sql new file mode 100644 index 0000000..d6eba2c --- /dev/null +++ b/crates/db/migrations/20260721030000_create_lead_requests.down.sql @@ -0,0 +1,7 @@ +DROP TABLE IF EXISTS lead_requests; +DROP FUNCTION IF EXISTS set_lead_request_reference_number(); +DROP SEQUENCE IF EXISTS lead_request_number_seq; +-- tracecoin_wallets/tracecoin_ledger intentionally left in place — an +-- already-deployed migration (20260318233000_tracecoin_ledger_immutable) +-- has triggers depending on tracecoin_ledger, and other data may reference +-- these tables by now. diff --git a/crates/db/migrations/20260721030000_create_lead_requests.up.sql b/crates/db/migrations/20260721030000_create_lead_requests.up.sql new file mode 100644 index 0000000..448a363 --- /dev/null +++ b/crates/db/migrations/20260721030000_create_lead_requests.up.sql @@ -0,0 +1,102 @@ +-- lead_requests backs the "professional sends a request to contact a +-- customer's requirement" flow (LeadRequestRepository, crates/db/src/models/ +-- lead_request.rs; send_lead_request in crates/contracts/src/profession_shared.rs; +-- list_requests/approve_request/reject_request in apps/customers/src/handlers.rs). +-- The table was only ever defined in disabled (.skip) migrations, and those +-- versions didn't match the columns the current Rust code actually reads/writes +-- (professional_user_id, customer_user_id, remarks, requested_at, resolved_at) — +-- so this flow has never worked. Schema below matches the live Rust code exactly. +CREATE TABLE IF NOT EXISTS lead_requests ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + lead_id UUID REFERENCES leads(id) ON DELETE CASCADE, + user_role_profile_id UUID, + professional_user_id UUID REFERENCES users(id) ON DELETE CASCADE, + customer_user_id UUID REFERENCES users(id) ON DELETE CASCADE, + status VARCHAR(50) NOT NULL DEFAULT 'PENDING', + tracecoins_reserved INTEGER NOT NULL DEFAULT 25, + remarks TEXT, + expires_at TIMESTAMPTZ NOT NULL DEFAULT (NOW() + INTERVAL '7 days'), + requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + resolved_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(lead_id, professional_user_id) +); + +CREATE INDEX IF NOT EXISTS idx_lead_requests_lead_id ON lead_requests(lead_id); +CREATE INDEX IF NOT EXISTS idx_lead_requests_user_role_profile_id ON lead_requests(user_role_profile_id); +CREATE INDEX IF NOT EXISTS idx_lead_requests_professional_user_id ON lead_requests(professional_user_id); +CREATE INDEX IF NOT EXISTS idx_lead_requests_customer_user_id ON lead_requests(customer_user_id); +CREATE INDEX IF NOT EXISTS idx_lead_requests_status ON lead_requests(status); + +-- Human-readable reference number (NXT-LED-YY-000001), same scheme as +-- verifications/payments/job_applications/etc. +CREATE SEQUENCE IF NOT EXISTS lead_request_number_seq; +ALTER TABLE lead_requests ADD COLUMN IF NOT EXISTS reference_number VARCHAR; + +CREATE OR REPLACE FUNCTION set_lead_request_reference_number() RETURNS TRIGGER AS $$ +BEGIN + IF NEW.reference_number IS NULL THEN + NEW.reference_number := 'NXT-LED-' || to_char(NOW(), 'YY') || '-' || lpad(nextval('lead_request_number_seq')::text, 6, '0'); + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS trg_lead_requests_reference_number ON lead_requests; +CREATE TRIGGER trg_lead_requests_reference_number + BEFORE INSERT ON lead_requests + FOR EACH ROW EXECUTE FUNCTION set_lead_request_reference_number(); + +DO $$ +DECLARE r RECORD; +BEGIN + FOR r IN SELECT id, created_at FROM lead_requests WHERE reference_number IS NULL ORDER BY created_at, id LOOP + UPDATE lead_requests SET reference_number = 'NXT-LED-' || to_char(r.created_at, 'YY') || '-' || lpad(nextval('lead_request_number_seq')::text, 6, '0') WHERE id = r.id; + END LOOP; +END $$; + +ALTER TABLE lead_requests ALTER COLUMN reference_number SET NOT NULL; + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'lead_requests_reference_number_key') THEN + ALTER TABLE lead_requests ADD CONSTRAINT lead_requests_reference_number_key UNIQUE (reference_number); + END IF; +END $$; + +-- tracecoin_wallets/tracecoin_ledger back the Tracecoin reservation that +-- happens when a lead request is sent (TracecoinWalletRepository). Like +-- lead_requests, they were only ever defined in disabled (.skip) migrations +-- — and even those used stale column names (`type`/`reason` instead of the +-- `transaction_type`/`reference_type` the current Rust code writes). An +-- earlier active migration (20260318233000_tracecoin_ledger_immutable, +-- already deployed) creates triggers ON tracecoin_ledger assuming it already +-- exists; CREATE TABLE IF NOT EXISTS here is a safe no-op if that migration +-- already succeeded against a table created some other way, and fills the +-- gap if it didn't. +CREATE TABLE IF NOT EXISTS tracecoin_wallets ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE, + balance INTEGER NOT NULL DEFAULT 0, + reserved INTEGER NOT NULL DEFAULT 0, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS tracecoin_ledger ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + wallet_id UUID NOT NULL REFERENCES tracecoin_wallets(id), + transaction_type VARCHAR(20) NOT NULL, + amount INTEGER NOT NULL, + reference_type VARCHAR(50), + reference_id UUID, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- In case tracecoin_ledger already existed under the older, stale column +-- names from the disabled migration's design. +ALTER TABLE tracecoin_ledger ADD COLUMN IF NOT EXISTS transaction_type VARCHAR(20); +ALTER TABLE tracecoin_ledger ADD COLUMN IF NOT EXISTS reference_type VARCHAR(50); + +CREATE INDEX IF NOT EXISTS idx_tracecoin_ledger_wallet_id ON tracecoin_ledger(wallet_id); +CREATE INDEX IF NOT EXISTS idx_tracecoin_ledger_reference_id ON tracecoin_ledger(reference_id);