- Add POST /api/ai-credits/coupons/validate: a dry-run of the same
validate_coupon() check create_order applies, so the checkout UI can
show a real discount/error before Pay instead of only finding out at
order-creation time. Never inserts anything.
- Add migration 20260814050000: restores integrity on invoices.payment_id
after 20260814030000 dropped the hard FK to support polymorphic
invoice_type (TRACECOIN_PURCHASE -> payments, AI_CREDIT_PURCHASE ->
ai_credit_orders). A BEFORE INSERT/UPDATE trigger now validates
payment_id against the right table per invoice_type instead of leaving
it fully unchecked. Verified against a live Postgres in a scratch
schema (valid/invalid payment_id, valid/bogus invoice_type).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The coming-soon page's 'Notify Me' form (nxtgauge-gitops/coming-soon/
index.html) only console.logged the email - nothing was actually
captured. Adds a public (no auth, same trust model as a newsletter
signup) endpoint on the users service:
- New waitlist_signups table (email unique, created_at) - no user_id/
FK since these are anonymous pre-launch signups, not necessarily
existing accounts.
- apps/users/src/handlers/waitlist.rs: POST / -> INSERT ... ON
CONFLICT DO NOTHING (repeat signups from the same email are a no-op,
not an error), loose email validation (has @ and a dotted domain -
this is a marketing signup, not an account, so overly strict
validation just loses real signups to minor typos).
- Routed /api/waitlist through the gateway to the users service,
alongside the other public routes (packages, kb, etc.)
Applied migration to nxtgauge_test and prod.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AI credit purchases (money -> credits via PayU) never generated an
invoice, even though the exact same infrastructure already works for
TraceCoin purchases in main.rs's generate_purchase_invoice. Spending
credits (try_reserve_credits/capture) correctly does NOT get an
invoice - only real-money purchases do, matching existing TraceCoin
behavior.
- invoices.payment_id had a hard FK to payments(id) only, which blocks
using it for ai_credit_orders(id) rows. Postgres has no polymorphic
FK; dropped the constraint (invoice_type already says which table
payment_id points into) via a new migration rather than editing the
original invoices migration.
- Added generate_ai_credit_invoice in ai_credits.rs, called from
verify_order after a successful PayU payment - mirrors main.rs's
pattern exactly (same non-blocking failure handling, same seller
details via the now pub(crate) seller_details(), invoice_type
'AI_CREDIT_PURCHASE').
- Caught a real bug while writing this: order.amount_inr is the
POST-discount final price, but NewInvoice.discount_amount is
subtracted again inside compute_totals (subtotal - discount) - using
amount_inr directly as unit_price_paise would have double-subtracted
the discount. Reconstructed the pre-discount price
(amount_inr + discount_applied) for the line item instead.
Applied the FK-drop migration to nxtgauge_test and prod; verified the
constraint is gone and user_id's FK is untouched.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
20260814000000: add coupon_code + discount_applied to ai_credit_orders
CREATE TABLE — create_order inserts both columns but the original
migration omitted them.
20260814020000 (new): create ai_coupons + ai_coupon_redemptions, the
tables referenced by create_order/verify_order in ai_credits.rs.
Extracted from the skipped 20260706300000 migration which couldn't be
enabled because it referenced ai_credit_orders before that table existed.
Runbook updated with the 3 pending prod migrations and redeploy step.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds applicable_roles TEXT[] to ai_credit_packages (empty = visible to
every role, matching today's behavior for the 4 existing packages -
nothing changes for them until an admin opts them into specific
roles). Mirrors pricing_packages' existing role_key convention, as an
array since one AI package can reasonably apply to several roles at
once.
- GET /api/ai-credits (list_packages) now accepts optional auth (via a
local MaybeAuthUser wrapper, since AuthUser doesn't implement axum's
optional-extraction trait) and filters out packages not applicable
to the viewer's roles. Anonymous viewers only see role-unrestricted
packages.
- POST /api/ai-credits/order (create_order) re-validates role
eligibility server-side too, not just in the listing - closes off
a logged-in user buying a package never shown to them.
- Admin CRUD (GET/POST /api/admin/ai-credits/packages,
PATCH .../{id}) now reads/writes applicable_roles.
Applied to nxtgauge_test and prod; verified column + index created,
existing 4 packages default to empty (all roles).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Discovered while wiring up role-scoped AI credit packages: the active
seed migration (20260615060600) stored price_inr as 99/349/999/2499 -
values that read like whole rupees, but every consumer (PayU's
paise_to_rupee_string, matching pricing_packages' existing paise
convention) treats price_inr as paise, meaning a real purchase would
have charged ~1% of the intended price.
The actual fix already existed as 20260705110000_ai_credit_packages
.up.sql.skip (correct paise values + the ai_credit_orders table
create_order inserts into) but got disabled in the same bulk skip
pass as everything else this session has been un-tangling - its
CREATE TABLE ai_credit_packages (no IF NOT EXISTS) hit 'relation
already exists' since 20260615060600 had already created it, aborting
before ai_credit_orders was ever created.
Net effect before this fix: buying AI credits was completely broken
(create_order would 500 - no ai_credit_orders table to insert into).
Confirmed zero rows anywhere in ai_credit_orders (the table simply
didn't exist) - nothing to reconcile, this is a pre-launch fix.
New migration (not resurrecting the old .skip file, to keep a clean
linear history matching this session's other fixes): UPDATE the 4
known-wrong rows by name to their correct paise values, CREATE TABLE
IF NOT EXISTS ai_credit_orders. Applied to both nxtgauge_test and
prod; verified.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
9 columns across ai_plans/user_ai_subscriptions/ai_feature_costs/
ai_usage_logs were TIMESTAMP (no timezone), left over from whatever
pre-20260703210000 mechanism originally created these 4 tables, but
crates/db/src/models/ai_credits.rs maps all of them to DateTime<Utc>,
which sqlx requires TIMESTAMPTZ for. This is a live bug, not just a
test-setup gap - discovered via crates/db/tests/ai_credits.rs failing
with ColumnDecode errors, but any production code path selecting
these columns (e.g. current_period_start/end on every wallet read)
would hit the same failure.
ALTER COLUMN ... USING col AT TIME ZONE 'UTC' is lossless here since
every write path uses NOW()/DEFAULT NOW() with no other timezone
handling anywhere in the codebase. Applied to both nxtgauge_test and
the live nxtgauge database directly; verified all 9 columns converted
and the 3 existing user_ai_subscriptions + 4 ai_plans rows' values are
intact and correctly interpreted as UTC afterward.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Re-enables and corrects 20260703210000_ai_credits_wallet, which had been
disabled (.up.sql.skip) in commit 0163349 because 'relation already
exists' aborted the original CREATE TABLE script on its very first
table (ai_plans) - silently leaving ai_credit_ledger and
ai_reservation_holds never created, despite the wallet system being
actively wired into apps/users/handlers/ai_credits.rs, apps/payments,
apps/cron with real subscription rows already in prod.
Rewrote the migration to be idempotent: ADD COLUMN IF NOT EXISTS to
bring ai_plans/user_ai_subscriptions/ai_feature_costs/ai_usage_logs up
to the full column set crates/db/src/models/ai_credits.rs expects
(safe zero/default backfills, no data loss - verified against the 3
existing subscription rows), and CREATE TABLE IF NOT EXISTS for the 2
genuinely-missing tables. down.sql updated to only reverse what this
corrected version actually adds, not drop tables that predate it.
Applied directly to the live database (verified schema + existing rows
intact afterward).
Also wires crates/db/tests/ai_credits.rs + ai_credits_reaper.rs to
Forgejo Actions CI (docs/LIVE_SERVER_RUNBOOK.md step 6):
- New TEST_DATABASE_URL repo secret, pointing at a dedicated
nxtgauge_test database (created fresh, schema mirrored from live prod
via pg_dump --schema-only - never runs against the real nxtgauge DB).
- .forgejo/workflows/test.yaml: runs on push to main/high-performance,
skips when crates/db/ isn't touched, cargo test -p db --test
ai_credits --test ai_credits_reaper -- --test-threads=1.
Also commits docs/openapi.wallet-holds.json (hand-written minimal
OpenAPI spec used for schemathesis fuzzing in step 4) and gitignores
the local .schemathesis/ cache directory from that run.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
20260627030000_wallet_full.up.sql was disabled in 0163349 as part of a
bulk cleanup of migrations referencing the dropped professionals
table, but this one doesn't touch that table at all - it only
references tracecoin_wallets, tracecoin_ledger and users, all of
which already exist in prod. It was swept up by mistake.
Without it, tracecoin_holds/tracecoin_buckets never got created, so
the new GET /wallet/me/holds route (crates/contracts/src/profession_shared.rs)
500s with 'relation "tracecoin_holds" does not exist'.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The live role_sidebar_configs row for JOB_SEEKER never included this
widget, so the verification status badge never rendered. Also applied
directly to the live DB row (this seed file wasn't wired into any
auto-run migration path, so editing it alone wouldn't have taken effect).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Seeds one TRACECOIN_BUNDLE package per role (100 Tracecoins / ₹250,
placeholder pricing per your instruction) so the purchase flow
(CreditsPage.tsx -> GET /api/packages, filtered client-side by
role_key) has something to show instead of "No packages available for
your role," and so create_order/verify_payment/invoice generation can
actually be exercised end-to-end. Idempotent (WHERE NOT EXISTS guard)
so it's safe against the custom db-migrate runner re-executing every
file on every deploy. Adjust real pricing later via the existing admin
CRUD (POST/PATCH /api/packages) — no new endpoint needed, it already
works.
Also fixes a unit bug introduced in the invoice wiring last commit:
payments.amount_inr is already paise (copied straight from
pricing_packages.price_inr, which is paise despite the name — PayU
order creation divides it by 100 via paise_to_rupee_string), but the
invoice line item multiplied it by 100 again, making every generated
invoice show a 100x inflated amount.
Finished the sweep of every table referenced by live Rust code but not
created by any active migration:
- audit_logs / audit_log_changes — backs wallet::audit() (crates/wallet),
called from every admin Tracecoin balance adjustment
(apps/payments/src/admin.rs::adjust_credits). actor_type defaulted since
the only caller doesn't supply it.
- tax_rules — backs the admin tax-rule CRUD in apps/payments/src/admin.rs.
init-db.sql has a differently-named version (title/percentage/
applicable_to); schema here matches the live code's actual columns
(name/tax_rate/applies_to).
- reviews — backs the professional review/rating system
(apps/users/src/handlers/reviews.rs).
Also fixed a real bug found while building the reviews schema:
admin_create_review's customer-lookup subquery selected
`lead_requests.user_id`, a column that has never existed (it's
`customer_user_id`) — would have failed on the very first review
creation attempt now that lead_requests actually exists.
Checked every other previously-flagged table (permissions, orders,
external_roles, internal_roles, kb_*, notification_*, order_items,
portfolio_images, smtp_configs, dashboard_widgets,
verification_documents) against actual Rust usage — none of them are
referenced by a real SQL query (grep hits were all Rust variable/type
names), so nothing to fix there. Full migration-chain sweep confirms
zero remaining "references a table before it's created" issues.
crates/invoice (InvoiceService, GST computation, HTML rendering) was
fully built but never wired to anything and never had its tables —
invoices/invoice_line_items/billing_profiles/invoice_number_seq never
existed in any active migration (same root cause as everything else
this session: only in scripts/init-db.sql, which the real db-migrate
job never runs). Created them, matching InvoiceService's actual
columns exactly rather than init-db.sql's older, simpler invoices
shape.
Fixed a real bug in InvoiceService::create while at it: four money
fields (total, and three line-item amounts) were bound as i64 against
columns/read-models that are i32 everywhere else — would have failed
every insert with a Postgres type mismatch the first time this code
ever actually ran against a real table.
Wired invoice generation into apps/payments' PayU verify_payment
handler (the actual success callback) — right after the wallet is
credited, a one-line-item GST invoice is generated from the purchased
package and PayU's billing fields (firstname/email/phone), using a
new INVOICE_SELLER_* env-configurable seller identity. Generation
failures are logged, not surfaced to the buyer, since the payment and
wallet credit have already succeeded by that point.
Also added the missing user-facing endpoints to fetch what got
generated: GET /api/payments/invoices (list) and
GET /api/payments/invoices/{id} (detail + line items) — previously
only admin-side invoice viewing existed.
NOTE: the frontend (nxtgauge-frontend-solid, a separate repo) has an
existing invoice-viewing page at src/routes/dashboard/wallet/invoices/
but it calls /wallet/me/invoices (no /api/ prefix) via a different,
apparently-dead API helper (api.get, not apiFetch) that every other
live page avoids — same dead-code pattern as the earlier apps/leads
discovery. The live purchase flow (CreditsPage.tsx) has no invoice UI
at all yet. Not fixed here since it's out of this repo's scope this
session — flagging for a frontend pass.
Same root cause, found via a systematic diff of every table in
scripts/init-db.sql against what active migrations actually create:
payments and pricing_packages (the Tracecoin/job-slot purchase flow,
apps/payments) were never created by any active migration.
20260317202300_coupons_discounts.up.sql (already deployed) references
payments(id) via FK a few hours later the same day — the earliest
still-live failure point for this table.
Schema uses the final PayU column names (payu_txnid/payu_mihpayid)
directly, matching what apps/payments/src/main.rs actually reads/writes
today, rather than init-db.sql's stale pre-rename Razorpay names —
20260626000000_payu_rename_columns (already deployed) only renames
columns that exist, so starting with the final names outright is
correct and makes that migration a safe no-op.
The most severe finding in this audit: `verifications` backs the entire
admin approval system this whole session's work depends on (every
role's profile submission, job posting, and requirement approval flow
goes through VerificationRepository). No active migration ever created
it, verification_logs, or approval_requests/approval_logs.
The correct schema for all of these already existed in
scripts/init-db.sql — a "complete schema" reference doc — but that file
is never actually executed by the real migration runner: Dockerfile.migrate
only COPYs crates/db/migrations into the image, and crates/db-migrate's
main.rs only ever reads that directory. init-db.sql has been dead
documentation this whole time, describing the schema this codebase
*should* have without any path to actually get there.
Cross-referencing init-db.sql against the Rust code that reads/writes
these tables found two bugs in init-db.sql itself:
- verification_logs.verification_request_id pointed at a separate,
unrelated `verification_requests` table instead of `verifications`
(already independently discovered and patched by an existing
migration, 20260718210823_fix_verification_logs_fk — this new
migration just creates it correctly from the start).
- approval_requests was missing UNIQUE(entity_type, entity_id), which
apps/users/src/handlers/verifications.rs's
`INSERT ... ON CONFLICT (entity_type, entity_id) DO UPDATE` requires.
Positioned before the earliest active migration that already assumed
these tables existed (20260718210823). The custom db-migrate runner
(crates/db-migrate) has no applied-migration tracking — it just re-runs
every *.up.sql file in filename order on every invocation — so there's
no "already applied, don't touch" risk from adding an earlier-dated
migration; idempotent IF NOT EXISTS guards make it safe regardless.
Continuing the migration-chain audit: professional portfolio/services
management (PortfolioPage.tsx -> /api/{profession}/portfolio/me,
/api/{profession}/services) was broken the same way as lead_requests —
portfolio_items and services were only ever defined in a disabled .skip
migration (keyed differently: user_id + profession_key, vs. the
user_role_profile_id every live query in
crates/db/src/models/professional.rs actually uses). Self-created both
tables with the schema the live code needs, in the same already-pushed
migration (20260317195000_profession_specific_profiles.up.sql) that was
unconditionally ALTERing them.
Also fixed TracecoinLedgerEntry (professional.rs) — its `type`/`reason`
fields didn't match the `transaction_type`/`reference_type` columns
TracecoinWalletRepository actually writes (see previous commit), so
GET /api/{profession}/wallet/ledger would have failed to deserialize
every row. professional.rs has its own duplicate, unreachable
try_reserve/debit/release_tracecoins using the old type/reason names —
left alone since nothing calls them (send_lead_request and friends all
go through TracecoinWalletRepository).
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>
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>
- 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
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
- 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
- New service at apps/ugc_content_creators (port 8095)
- DB model + repository in crates/db/src/models/ugc_content_creator.rs
- Migration: ugc_content_creator_profiles table with platforms, content_niches,
content_formats, follower_count, handles, and standard status/timestamps
- Contracts: is_professional_profile_approved() handles UGC_CONTENT_CREATOR case
- Gateway: routes /api/ugc-content-creators to new service
- Workspace Cargo.toml updated with new member
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- handlers/reviews.rs: admin CRUD for /api/admin/reviews (list, create, patch status, delete)
- handlers/coupons.rs: admin CRUD for /api/admin/coupons and /api/admin/discounts
- handlers/pricing.rs: admin CRUD for /api/admin/tracecoin-packages + /api/admin/reports/{users,revenue}
- handlers/dashboard.rs: replace all hardcoded fake data with real DB queries (registrations per day, revenue per week, live KPIs including pending approvals and total revenue)
- Migrations: extend reviews table (nullable FKs + admin fields), add coupons.title/role_keys, create discounts table
- gateway: route new admin paths to users service
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 2 new migrations: summary/tags columns on kb_articles, description/requester fields on support_tickets
- handlers/kb.rs: public routes (GET /api/kb/categories|articles|articles/:slug) + admin CRUD (/api/admin/kb/*)
- handlers/support.rs: user ticket routes + admin support-cases CRUD with internal notes
- Registered all new routes in users service main.rs
- Gateway resolve_upstream: /api/kb/*, /api/support/*, /api/admin/kb/*, /api/admin/support-cases/* → users service
- scripts/seed_kb.sql: 8 categories, 28 full-length published articles covering all user roles
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Full CRUD handler for designations with department JOIN, employee count,
level/can_manage_team/can_approve fields, and migration to extend the
minimal designations table with all management columns.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>