nxtgauge-backend-rust/crates/db/migrations/20260721030000_create_lead_requests.up.sql
Ashwin Kumar Sivakumar 70e63c6aff fix: lead_requests table never existed, and reference_numbers migration was unconditionally broken
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.
2026-07-21 02:00:23 +05:30

102 lines
5 KiB
PL/PgSQL

-- 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);