Commit graph

121 commits

Author SHA1 Message Date
Ashwin Kumar Sivakumar
6defd52016 fix(ai-credits): whole subscription-lifecycle subsystem was non-functional
All checks were successful
build-and-release / build (cron) (push) Successful in 1m10s
build-and-release / build (catering-services) (push) Successful in 1m36s
build-and-release / build (companies) (push) Successful in 2m1s
build-and-release / build (customers) (push) Successful in 2m18s
build-and-release / build (employees) (push) Successful in 2m37s
build-and-release / build (developers) (push) Successful in 2m40s
build-and-release / build (gateway) (push) Successful in 1m19s
build-and-release / build (graphic-designers) (push) Successful in 1m59s
build-and-release / build (fitness-trainers) (push) Successful in 3m2s
build-and-release / build (jobs) (push) Successful in 1m53s
build-and-release / build (job-seekers) (push) Successful in 2m20s
build-and-release / build (makeup-artists) (push) Successful in 2m42s
build-and-release / build (social-media-managers) (push) Successful in 1m49s
build-and-release / build (payments) (push) Successful in 3m21s
build-and-release / build-db-migrate (push) Successful in 9s
build-and-release / build (photographers) (push) Successful in 2m38s
backend-integration-tests / ai-credits (push) Successful in 47s
build-and-release / build (tutors) (push) Successful in 2m51s
build-and-release / build (ugc-content-creators) (push) Successful in 2m55s
build-and-release / build (video-editors) (push) Successful in 2m43s
build-and-release / build (users) (push) Successful in 4m42s
Following up on the job-expiry 'companies' typo found in the same log
sweep: apps/users/src/ai_subscription.rs (upgrade_plan,
schedule_downgrade, apply_scheduled_downgrades, cancel_subscription,
start_trial, expire_trials, get_subscription_history) and the matching
cron tasks were written entirely against schema that was never
migrated:
  - user_ai_subscriptions.downgrade_scheduled_to/is_trial/trial_days/
    trial_ends_at don't exist
  - ai_subscription_history doesn't exist as a table at all, despite
    6 separate INSERT/UPDATE/SELECT call sites against it

Confirmed live: apply_scheduled_downgrades and expire_trials have been
failing on every hourly cron run. Any user hitting upgrade/downgrade/
cancel/trial-start would 500 the same way.

Schema reconstructed from every call site's actual INSERT/UPDATE/SELECT,
not guessed: from_plan_id nullable (LEFT JOIN'd, never NOT NULL in any
insert), to_plan_id NOT NULL (always bound, INNER JOIN'd),
proration_credits/proration_days_remaining default (only upgrade_plan
binds them), created_by nullable (expire_trials never binds it).

Also fixed a real, unrelated bug found in the same cron-log sweep:
apps/cron/src/tasks/jobs.rs's expire_stale_jobs referenced a
nonexistent 'companies' table (real name: company_profiles) -- also
failing every hourly run, meaning LIVE jobs past expiry were never
being flipped to EXPIRED.

Verified against a live Postgres by calling the actual production
functions (not simulated SQL) end-to-end: upgrade with proration +
history, schedule-then-real-cron-applied downgrade, cancellation,
trial-start-then-real-cron-expiry, and the joined history read-back.
All 5 passed.
2026-08-18 04:53:30 +05:30
Ashwin Kumar Sivakumar
341322e0ee feat(cron): 7-day accepted-lead expiry, 3-month purchased-Tracecoin expiry
All checks were successful
build-and-release / build (companies) (push) Successful in 2m14s
build-and-release / build (customers) (push) Successful in 2m17s
build-and-release / build (developers) (push) Successful in 2m25s
build-and-release / build (cron) (push) Successful in 2m36s
build-and-release / build (catering-services) (push) Successful in 2m43s
build-and-release / build (gateway) (push) Successful in 33s
build-and-release / build (jobs) (push) Successful in 39s
build-and-release / build (employees) (push) Successful in 1m41s
build-and-release / build (graphic-designers) (push) Successful in 1m51s
build-and-release / build (fitness-trainers) (push) Successful in 2m17s
build-and-release / build (makeup-artists) (push) Successful in 1m54s
build-and-release / build (photographers) (push) Successful in 1m53s
build-and-release / build (job-seekers) (push) Successful in 3m29s
build-and-release / build (payments) (push) Successful in 3m0s
build-and-release / build-db-migrate (push) Successful in 10s
build-and-release / build (tutors) (push) Successful in 2m32s
backend-integration-tests / ai-credits (push) Successful in 44s
build-and-release / build (social-media-managers) (push) Successful in 3m4s
build-and-release / build (ugc-content-creators) (push) Successful in 2m59s
build-and-release / build (video-editors) (push) Successful in 2m37s
build-and-release / build (users) (push) Successful in 4m40s
Two checklist rules that had zero implementation until now (confirmed
by the earlier audit this session -- no code, no schema for either):

1. Accepted leads expire 7 days after acceptance
   (apps/cron/src/tasks/lead_requests.rs::expire_accepted_leads,
   hourly). No wallet/ledger involvement -- by ACCEPTED, the
   professional's Tracecoins were already permanently debited from
   `reserved` at acceptance time, so expiry is purely a status/
   visibility change marking the engagement window closed, not a
   financial reversal. Guarded on status = 'ACCEPTED' so it can't
   double-process or race an in-flight customer/professional action on
   the same lead_request.

2. Purchased Tracecoins expire 3 months after purchase
   (apps/cron/src/tasks/tracecoin_expiry.rs::expire_purchased_
   tracecoin_buckets, daily). This is the bigger piece:
   - TracecoinWalletRepository::create_purchase_bucket records each
     real purchase as a bucket in the already-existing but previously
     unused tracecoin_buckets table, with a 90-day expiry. Wired into
     apps/payments/src/reconcile.rs::finalize_successful_payment, the
     only real Tracecoin purchase path in the codebase (idempotent on
     payment id via the table's existing unique index -- a retried
     finalize can't create two buckets).
   - TracecoinWalletRepository::consume_purchase_buckets_fifo draws
     buckets down oldest-expiring-first whenever a permanent spend
     actually happens -- wired into try_debit_reserved_tracecoins
     (lead acceptance) and try_debit_balance (the urgent-lead
     upgrade). NOT wired into reserve/release, since no money has
     left the wallet yet at that point. NOT wired into admin_adjust --
     "purchased credits", not all credits; admin grants don't create
     buckets and so can't be clawed back by this either.
   - The daily sweep claws back only a bucket's *unspent* remainder
     once it's past expiry, locked per-wallet (FOR UPDATE) so it can't
     race a concurrent spend, bounded to never drive balance negative,
     and writes a real tracecoin_ledger DEBIT/TRACECOIN_EXPIRY entry.
     No email is sent -- the one plausible template
     (send_lead_expired_email, "lead-expired") says "tracecoins
     returned", which would misstate what happened; sending a wrong-
     context email is worse than sending none. In-app notification
     only, for now.

Verified against a live Postgres (not just compiled), including two
things that would be very easy to get subtly wrong:
  - FIFO ordering: seeded a sooner-expiring small bucket and a
    later-expiring large one, spent an amount spanning both, confirmed
    the soonest-expiring bucket drains completely before the second
    one is touched at all.
  - The expiry sweep run against three buckets in different states
    (fully spent, expired-with-leftover, not-yet-expired) confirms it
    claws back only the expired+unspent one, leaves the other two
    untouched, and is idempotent on immediate re-run.
  - The 7-day lead expiry only fires on ACCEPTED leads whose
    resolved_at is actually past the cutoff, leaving a recently-
    accepted lead and an unrelated still-PENDING request alone.

cargo test -p db (real integration suite, live DB) still green: 8/8.
2026-08-18 02:11:08 +05:30
Ashwin Kumar Sivakumar
c5b3a9583d fix(users): registration was broken -- users.first_name/last_name never existed
All checks were successful
build-and-release / build (cron) (push) Successful in 1m2s
build-and-release / build (catering-services) (push) Successful in 1m36s
build-and-release / build (companies) (push) Successful in 2m11s
build-and-release / build (employees) (push) Successful in 2m25s
build-and-release / build (developers) (push) Successful in 2m47s
build-and-release / build (customers) (push) Successful in 3m5s
build-and-release / build (gateway) (push) Successful in 1m46s
build-and-release / build (fitness-trainers) (push) Successful in 2m44s
build-and-release / build (graphic-designers) (push) Successful in 2m0s
build-and-release / build (jobs) (push) Successful in 1m52s
build-and-release / build (makeup-artists) (push) Successful in 1m40s
build-and-release / build (job-seekers) (push) Successful in 3m1s
build-and-release / build (social-media-managers) (push) Successful in 1m54s
build-and-release / build (payments) (push) Successful in 2m57s
build-and-release / build (photographers) (push) Successful in 2m57s
build-and-release / build (tutors) (push) Successful in 2m43s
backend-integration-tests / ai-credits (push) Successful in 1m9s
build-and-release / build (ugc-content-creators) (push) Successful in 3m12s
build-and-release / build-db-migrate (push) Successful in 2m23s
build-and-release / build (video-editors) (push) Successful in 2m46s
build-and-release / build (users) (push) Successful in 4m56s
Root cause, traced via git history: commit a3076ed ("update DB schema -
split users.first_name, users.last_name, roles split", 2026-04-15)
rewrote ~35 files across the codebase (UserRepository, and read sites
throughout apps/users, apps/companies, apps/job_seekers,
apps/customers, crates/contracts) to use users.first_name/last_name
instead of users.full_name -- but never added the migration to
actually create those columns. UserRepository::create is the only
INSERT into users and the live registration path; it's been trying to
insert into nonexistent columns ever since.

Confirmed this independently by spinning up Postgres locally, applying
every migration in crates/db/migrations (the exact set Dockerfile.migrate
bakes into the deployed db-migrate image, so this isn't a
theoretical-config-drift argument -- it's what's actually shipped) with
DROP_EXISTING_TABLES unset against a clean DB, and running the real
UserRepository::create call: `column "first_name" of relation "users"
does not exist`.

Finishes what that commit should have done: adds first_name/last_name
to users, backfilled from the pre-existing full_name (naive split on
first space). Does NOT drop full_name -- apps/payments (invoice/billing
legal names) and job_seeker_profiles still correctly read it, so
UserRepository::create now also derives and writes full_name from
first_name/last_name, keeping both representations correct for every
user going forward instead of only whichever one a given caller reads.

Verified against a real local Postgres, not just compiled:
- All 40 migrations in this directory apply cleanly in sequence, and
  a second and third run are fully idempotent (this surfaced three
  more of my own earlier migrations this session -- job_applications
  unique constraint, company_profiles non-negative checks, lead_requests
  FKs -- that used bare `ADD CONSTRAINT` with no IF NOT EXISTS guard;
  since this repo's db-migrate has no migration-tracking table and
  re-executes every .up.sql on every deploy, those would have errored
  on the second deploy and silently blocked every migration after them
  alphabetically, including all of today's other fixes. Fixed to match
  the DO $$ ... pg_constraint IF NOT EXISTS pattern already used
  elsewhere in this migrations directory.)
- A standalone harness actually calling UserRepository::create against
  that live DB confirms registration succeeds and full_name ends up
  correctly populated.
- The backfill was verified against simulated legacy rows (full_name
  only, both single- and multi-word names).
- cargo test -p db (real integration tests, not just compile-checked --
  requires TEST_DATABASE_URL, which is why these hadn't run all session)
  passes in full: 8 tests including concurrent_reservations_cannot_
  overdraw_wallet and expired_hold_is_swept_and_credits_return_to_available.
2026-08-18 01:58:23 +05:30
Ashwin Kumar Sivakumar
efd2ec6222 fix: wire up dead admin router, IDOR on submit_requirement, dead code cleanup
All checks were successful
build-and-release / build (cron) (push) Successful in 55s
build-and-release / build (catering-services) (push) Successful in 2m7s
build-and-release / build (companies) (push) Successful in 2m14s
build-and-release / build (fitness-trainers) (push) Successful in 1m41s
build-and-release / build (customers) (push) Successful in 2m58s
build-and-release / build (developers) (push) Successful in 2m28s
build-and-release / build (employees) (push) Successful in 2m34s
build-and-release / build (gateway) (push) Successful in 1m52s
build-and-release / build (graphic-designers) (push) Successful in 2m8s
build-and-release / build (jobs) (push) Successful in 1m49s
build-and-release / build (job-seekers) (push) Successful in 2m31s
build-and-release / build (makeup-artists) (push) Successful in 1m52s
build-and-release / build (social-media-managers) (push) Successful in 1m46s
build-and-release / build (photographers) (push) Successful in 2m32s
build-and-release / build (payments) (push) Successful in 3m24s
build-and-release / build (tutors) (push) Successful in 2m38s
backend-integration-tests / ai-credits (push) Successful in 46s
build-and-release / build (ugc-content-creators) (push) Successful in 3m6s
build-and-release / build (video-editors) (push) Successful in 2m37s
build-and-release / build-db-migrate (push) Successful in 2m19s
build-and-release / build (users) (push) Successful in 5m8s
apps/payments/src/admin.rs (tax rules, ledger, orders, credits, invoice
admin endpoints) was written but never actually shipped: no `mod
admin;` in main.rs, no `wallet` crate dependency in Cargo.toml, and its
routes used axum 0.6-style `:id` path syntax (this workspace is on
0.8, which requires `{id}` and panics on `:id` at router-build time).
Meanwhile apps/gateway explicitly forwards /api/admin/invoices*,
/api/admin/tax, /api/admin/orders, /api/admin/ledger, and
/api/admin/credits/* to this service, so every one of those admin
endpoints has been 404ing in production. Fixed all three issues and
merged the router in. Also fixed three inline SQL queries
(list_ledger x2, get_credit_ledger) still selecting the nonexistent
`type`/`reason` columns this session's earlier wallet-crate fix
already corrected everywhere else -- these went unnoticed until now
because dynamic SQL isn't checked at compile time, and the file had
literally never been compiled before.

apps/customers/src/handlers.rs submit_requirement had no ownership
check -- every sibling handler on the same resource (get_requirement,
update_requirement, mark_requirement_urgent) checks
req.created_by_user_id against auth.user_id; this one didn't, so any
authenticated customer could submit another customer's DRAFT
requirement into the approval queue by id, with the verification case
and email misattributed to the caller instead of the actual owner.

Removed ProfessionalRepository::try_reserve_tracecoins/
try_debit_reserved_tracecoins/try_release_reserved_tracecoins
(crates/db/src/models/professional.rs) -- a dead, unreferenced
duplicate of TracecoinWalletRepository's already-correct
reserve/debit/release, whose ledger INSERTs used the same nonexistent
`type`/`reason` columns and would have errored at runtime if anything
had ever called it. A second implementation of the same money-moving
logic is itself the risk, so removed rather than fixed in place.

Added the missing FK constraints on lead_requests.lead_id and
.user_role_profile_id (only professional_user_id/customer_user_id had
one) -- app code always populates them correctly today, but nothing
DB-side stopped a future bug from writing a dangling reference.

Centralized the 18% GST rate (invoice::STANDARD_GST_RATE_PERCENT)
instead of the literal 18.0 duplicated at both invoice-generation call
sites.

Not fixed, flagged instead: the admin reconcile_credits report
compares each wallet's all-time balance against a ledger sum restricted
to a 30-day-by-default window -- objectively wrong, but the deeper
problem is that tracecoin_ledger.amount's sign convention differs
between writers (TracecoinWalletRepository stores unsigned magnitudes
keyed by transaction_type; crates/wallet stores signed balance deltas
directly), so a correct "balance == sum(ledger)" reconciliation needs
that inconsistency resolved first -- not a quick fix, a design
decision on the write side.
2026-08-18 01:44:10 +05:30
Ashwin Kumar Sivakumar
024fa05e96 feat(marketplace): urgent/featured leads for customers, correct request cap
Some checks failed
build-and-release / build (customers) (push) Successful in 1m57s
build-and-release / build (employees) (push) Successful in 2m5s
build-and-release / build (catering-services) (push) Successful in 2m32s
build-and-release / build (companies) (push) Successful in 2m38s
build-and-release / build (cron) (push) Successful in 2m54s
build-and-release / build (gateway) (push) Successful in 1m26s
build-and-release / build (fitness-trainers) (push) Successful in 1m46s
build-and-release / build (developers) (push) Successful in 3m58s
build-and-release / build (jobs) (push) Successful in 1m12s
build-and-release / build (graphic-designers) (push) Successful in 2m30s
build-and-release / build (makeup-artists) (push) Successful in 1m43s
build-and-release / build (job-seekers) (push) Successful in 3m1s
build-and-release / build (payments) (push) Successful in 2m51s
build-and-release / build (social-media-managers) (push) Successful in 2m33s
build-and-release / build-db-migrate (push) Successful in 7s
build-and-release / build (photographers) (push) Successful in 3m1s
backend-integration-tests / ai-credits (push) Successful in 50s
build-and-release / build (tutors) (push) Successful in 2m51s
build-and-release / build (ugc-content-creators) (push) Successful in 2m42s
build-and-release / build (video-editors) (push) Successful in 2m33s
build-and-release / build (users) (push) Has been cancelled
New paid feature per product clarification: a customer can pay 50
Tracecoins to mark their own open requirement ("lead", professional-
facing) urgent. Urgent leads:
  - sort first in the professional-facing open-leads feed (new --
    there was previously no way for a professional to discover a
    requirement other than being handed its id directly, so this adds
    GET /marketplace alongside the existing GET /marketplace/{id})
  - get a raised per-lead request cap: 20 instead of the normal 10
    (was hardcoded to 20 for everyone -- corrected to the actual
    intended normal cap while adding the urgent tier)
  - trigger an immediate best-effort notification to approved
    professionals in the same profession + location

No separate expiry: urgent status rides the existing 7-day
expires_at, it doesn't extend anything -- it only changes ordering,
cap, and notification while the lead is already live.

TracecoinWalletRepository::try_debit_balance is a new one-shot debit
primitive (SELECT...FOR UPDATE, idempotent on reference_type +
reference_id) for spends that don't need reserve/confirm's two-step
hold -- mark_requirement_urgent debits before flagging the lead, so a
failed/insufficient-funds debit leaves no state to compensate, and a
retry (or two concurrent requests for the same lead) can't double-
charge.
2026-08-18 01:34:24 +05:30
Ashwin Kumar Sivakumar
0432ddcb51 fix(wallet): fund-leak on lead rejection, broken expiry refund, dead admin_adjust
All checks were successful
build-and-release / build (catering-services) (push) Successful in 1m53s
build-and-release / build (developers) (push) Successful in 1m56s
build-and-release / build (employees) (push) Successful in 2m14s
build-and-release / build (customers) (push) Successful in 2m32s
build-and-release / build (cron) (push) Successful in 2m42s
build-and-release / build (companies) (push) Successful in 3m36s
build-and-release / build (fitness-trainers) (push) Successful in 1m58s
build-and-release / build (graphic-designers) (push) Successful in 1m53s
build-and-release / build (gateway) (push) Successful in 2m14s
build-and-release / build (jobs) (push) Successful in 1m55s
build-and-release / build (makeup-artists) (push) Successful in 1m54s
build-and-release / build (photographers) (push) Successful in 1m50s
build-and-release / build (social-media-managers) (push) Successful in 2m33s
build-and-release / build (tutors) (push) Successful in 2m43s
build-and-release / build-db-migrate (push) Successful in 15s
build-and-release / build (ugc-content-creators) (push) Successful in 2m34s
backend-integration-tests / ai-credits (push) Successful in 1m6s
build-and-release / build (video-editors) (push) Successful in 2m32s
build-and-release / build (job-seekers) (push) Successful in 7m25s
build-and-release / build (users) (push) Successful in 5m5s
build-and-release / build (payments) (push) Successful in 7m42s
Three independent Tracecoin correctness bugs found auditing the
services-marketplace lead flow:

1. reject_request passed lead.user_role_profile_id -- the wrong id --
   to try_release_reserved_tracecoins, which looks the wallet up by
   user_id. It never found a matching wallet, silently returned
   Ok(false), and the handler returned 409 to the customer -- but
   lead_requests.status had already committed to REJECTED on the
   previous statement. Every rejected lead permanently stranded the
   professional's reserved Tracecoins, with no cron sweeping REJECTED
   leads to recover them. Now uses professional_user_id, matching
   approve_request's (correct) debit call.

2. apps/cron/src/tasks/leads.rs's stale-PENDING-lead refund wrote
   `UPDATE tracecoin_wallets SET current_balance = current_balance +
   $1` -- that column doesn't exist (it's `balance`), so this errored
   on every run that hit a reserved-coins expiry, aborting the whole
   function via `?` before the transaction committed (rolling back the
   EXPIRED status flip with it). Leads with reserved coins never
   actually expired or refunded, silently, every 15 minutes. Fixed the
   column name and made it also decrement `reserved` (previously it
   only ever credited `balance`, never releasing the hold itself).

3. crates/wallet (credit/reserve/release/confirm/admin_adjust) and its
   hold/escrow submodule were built against a tracecoin_ledger schema
   (`type`, `reason`, `balance_after`, `actor_user_id`, `metadata`
   columns, a `lock_tracecoin_wallet()` SQL function) that was never
   actually migrated -- the migration that would have added it was
   disabled (`.up.sql.skip`), and even that skipped file assumed
   column names that don't match this repo's real, separately-evolved
   ledger schema (`transaction_type`/`reference_type`). Every call into
   crates/wallet errored before doing anything. The one live caller is
   the admin manual wallet-adjustment endpoint (apps/payments/src/
   admin.rs), so every admin credit/debit adjustment 500'd. Migration
   adds the function plus the genuinely-new audit columns
   (balance_after/actor_user_id/metadata) and an idempotency index,
   while the code now reads/writes through the existing transaction_
   type/reference_type columns instead of introducing a second,
   competing pair.

Also fixes the underlying check-then-act race on lead_requests.status:
approve_request/reject_request read status, branched in Rust, then
called an unconditional UPDATE with no re-check -- concurrent
approve/reject on the same lead could both pass the in-memory check.
LeadRequestRepository::update_status_from adds a `WHERE status =
$from` guard, used both for the initial PENDING transition and to
compensate (revert to PENDING) if the wallet debit/release fails after
the status flip already committed.

hold::place (the escrow "reserve" half) has zero callers anywhere in
the codebase -- flagging separately, not fixed here, since wiring it
up or removing it is a product decision, not a bug fix.
2026-08-18 00:51:19 +05:30
Ashwin Kumar Sivakumar
617a75971f fix(companies): contact-unlock double-spend and job-quota race
view_contact previously decremented free_contact_views/
purchased_contact_views on every call with no record of which
applications had already had their contact unlocked -- refreshing the
same application's contact panel silently re-spent the allowance every
time, not just under concurrency, and the check-then-act shape also let
concurrent requests both pass the exhausted-quota check before either
UPDATE committed. Now the whole claim (dedup check + conditional
decrement + job_applications.contact_unlocked_at write) runs in one
transaction with company_profiles locked FOR UPDATE, gated on a
WHERE ... > 0 guard on the decrement itself.

create_job had the same shape for the 1-free-job/month rule and
purchased_job_slots: two concurrent requests could both observe
"quota available" before either committed, letting a company publish
2+ free jobs in a month or drive purchased_job_slots negative. Same
fix: lock company_profiles FOR UPDATE for the whole check+mutate+
insert sequence.

Also adds CHECK (>= 0) constraints on all four company_profiles
counters as defense-in-depth, matching the pattern tracecoin_wallets
already uses for balance/reserved.
2026-08-18 00:51:02 +05:30
Ashwin Kumar Sivakumar
f6a23cb99a fix(db): add missing job_applications unique(job_id, applicant_user_id)
All checks were successful
build-and-release / build (cron) (push) Successful in 1m2s
build-and-release / build (customers) (push) Successful in 2m3s
build-and-release / build (developers) (push) Successful in 2m14s
build-and-release / build (employees) (push) Successful in 2m20s
build-and-release / build (catering-services) (push) Successful in 2m44s
build-and-release / build (gateway) (push) Successful in 1m12s
build-and-release / build (fitness-trainers) (push) Successful in 2m29s
build-and-release / build (jobs) (push) Successful in 1m5s
build-and-release / build (companies) (push) Successful in 4m0s
build-and-release / build (graphic-designers) (push) Successful in 2m32s
build-and-release / build (makeup-artists) (push) Successful in 1m46s
build-and-release / build (payments) (push) Successful in 2m43s
build-and-release / build (photographers) (push) Successful in 2m40s
build-and-release / build (tutors) (push) Successful in 2m47s
build-and-release / build (ugc-content-creators) (push) Successful in 2m46s
backend-integration-tests / ai-credits (push) Successful in 46s
build-and-release / build (video-editors) (push) Successful in 2m45s
build-and-release / build (job-seekers) (push) Successful in 8m2s
build-and-release / build-db-migrate (push) Successful in 3m9s
build-and-release / build (social-media-managers) (push) Successful in 7m54s
build-and-release / build (users) (push) Successful in 8m25s
apply_to_job does check-then-insert with no transaction/lock to block
duplicate applications - race-condition-prone. The INSERT error handler
already anticipated a unique-violation ('unique' in error string -> 409
ALREADY_APPLIED) but the constraint itself was never created, so that
branch was dead code. Adding it now gives the existing handling a real
backstop.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 20:42:53 +05:30
Ashwin Kumar Sivakumar
4ee593f407 fix(db): pricing_packages.price_inr was seeded as rupees, treated as paise
All checks were successful
build-and-release / build (companies) (push) Successful in 2m4s
build-and-release / build (customers) (push) Successful in 2m7s
build-and-release / build (employees) (push) Successful in 2m14s
build-and-release / build (catering-services) (push) Successful in 2m34s
build-and-release / build (cron) (push) Successful in 2m40s
build-and-release / build (developers) (push) Successful in 3m16s
build-and-release / build (gateway) (push) Successful in 1m12s
build-and-release / build (graphic-designers) (push) Successful in 1m44s
build-and-release / build (jobs) (push) Successful in 1m52s
build-and-release / build (fitness-trainers) (push) Successful in 2m38s
build-and-release / build (job-seekers) (push) Successful in 2m25s
build-and-release / build (photographers) (push) Successful in 1m55s
build-and-release / build (makeup-artists) (push) Successful in 2m43s
build-and-release / build (social-media-managers) (push) Successful in 2m56s
build-and-release / build (tutors) (push) Successful in 2m49s
build-and-release / build (payments) (push) Successful in 4m44s
backend-integration-tests / ai-credits (push) Successful in 1m3s
build-and-release / build (video-editors) (push) Successful in 2m55s
build-and-release / build (ugc-content-creators) (push) Successful in 4m26s
build-and-release / build-db-migrate (push) Successful in 3m6s
build-and-release / build (users) (push) Successful in 8m29s
58 of 59 rows were inserted with plain rupee-looking values (499, 999,
1999, ...) but every consumer (apps/payments/src/main.rs) treats
price_inr as paise, so PayU would have charged 1/100th of the intended
price. Confirmed zero rows in payments/orders — pre-launch data fix, not
a refund situation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 19:23:56 +05:30
Ashwin Kumar Sivakumar
e8f4262804 fix(db): drop untracked invoices_payment_id_check trigger before 050000 recreates it
Prod has this trigger from an out-of-band creation that was never recorded
in _sqlx_migrations, so migration 20260814050000 failed with 'trigger
already exists' and stopped the whole migrate run partway through.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 18:37:08 +05:30
Tracewebstudio Dev
92072f04bd fix: testing session bugs (BUG-54 through BUG-56) and API improvements
- BUG-55: Fix coupon min_order_amount display dividing rupees by 100
  (users/handlers/coupons.rs - remove /100 from error message format)
- BUG-56: Fix admin applications endpoint 500 - add reference_number to
  SELECT in list_applications query (companies/handlers/admin.rs)
- BUG-54: KB category requires explicit slug field (documented; by-design)
- Various API handler improvements discovered during end-to-end testing:
  auth, profile, support, reviews, kb, pricing, modules, activity_logs,
  admin, ai_phase4, config handlers in users service
- Job seekers and customers service handler fixes
- Gateway and jobs service improvements
- DB model field additions for professional role types

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-17 14:57:50 +02:00
Ashwin Kumar Sivakumar
f9718f85d0 ci: build and push nxtgauge-db-migrate image on every push
All checks were successful
build-and-release / build (developers) (push) Successful in 3m6s
build-and-release / build (jobs) (push) Successful in 1m54s
build-and-release / build-db-migrate (push) Successful in 2m42s
build-and-release / build (companies) (push) Successful in 1m50s
build-and-release / build (photographers) (push) Successful in 2m59s
build-and-release / build (makeup-artists) (push) Successful in 1m34s
build-and-release / build (gateway) (push) Successful in 1m34s
build-and-release / build (ugc-content-creators) (push) Successful in 3m13s
build-and-release / build (job-seekers) (push) Successful in 3m0s
build-and-release / build (tutors) (push) Successful in 2m48s
build-and-release / build (users) (push) Successful in 4m55s
build-and-release / build (customers) (push) Successful in 1m50s
build-and-release / build (graphic-designers) (push) Successful in 1m53s
build-and-release / build (social-media-managers) (push) Successful in 1m51s
build-and-release / build (video-editors) (push) Successful in 2m52s
build-and-release / build (catering-services) (push) Successful in 2m28s
build-and-release / build (fitness-trainers) (push) Successful in 2m49s
build-and-release / build (payments) (push) Successful in 2m44s
build-and-release / build (cron) (push) Successful in 1m6s
build-and-release / build (employees) (push) Successful in 2m28s
backend-integration-tests / ai-credits (push) Successful in 52s
The migrate image (Dockerfile.migrate) was never part of the CI build
matrix - it's a separate crate, not an apps/* service, and the running
K8s Job pulls a static <branch>-latest tag rather than a pinned digest.
Without this, a stale image silently no-ops new migrations: the Job
reports success but applies nothing, which is exactly what happened
with the ai_guard_violations migration on 2026-08-15.
2026-08-15 20:20:26 +05:30
Ashwin Kumar Sivakumar
605f3bdaaa Move ai_guard_violations migration to migrations/ dir picked up by db-migrate
Some checks failed
build-and-release / build (graphic-designers) (push) Successful in 1m52s
build-and-release / build (cron) (push) Successful in 2m34s
build-and-release / build (makeup-artists) (push) Successful in 1m32s
build-and-release / build (video-editors) (push) Waiting to run
backend-integration-tests / ai-credits (push) Waiting to run
build-and-release / build (jobs) (push) Successful in 1m54s
build-and-release / build (customers) (push) Successful in 1m42s
build-and-release / build (companies) (push) Successful in 2m39s
build-and-release / build (gateway) (push) Successful in 1m7s
build-and-release / build (job-seekers) (push) Successful in 3m0s
build-and-release / build (tutors) (push) Has been cancelled
build-and-release / build (photographers) (push) Has been cancelled
build-and-release / build (developers) (push) Successful in 1m44s
build-and-release / build (payments) (push) Has been cancelled
build-and-release / build (fitness-trainers) (push) Successful in 1m51s
build-and-release / build (ugc-content-creators) (push) Has been cancelled
build-and-release / build (social-media-managers) (push) Has been cancelled
build-and-release / build (employees) (push) Successful in 2m6s
build-and-release / build (catering-services) (push) Successful in 2m17s
build-and-release / build (users) (push) Has been cancelled
migrations_new/ is not read by db-migrate (it only honors MIGRATIONS_DIR,
which defaults to /migrations, i.e. crates/db/migrations). The file was
misplaced and would never have run.
2026-08-15 20:15:05 +05:30
Tracewebstudio Dev
d9b3bb4d94 feat(ai-guard): add semantic moderation layer + guard violations DB logging
All checks were successful
build-and-release / build (tutors) (push) Successful in 2m36s
build-and-release / build (users) (push) Successful in 4m52s
build-and-release / build (jobs) (push) Successful in 41s
backend-integration-tests / ai-credits (push) Successful in 45s
build-and-release / build (fitness-trainers) (push) Successful in 1m52s
build-and-release / build (catering-services) (push) Successful in 1m59s
build-and-release / build (social-media-managers) (push) Successful in 3m3s
build-and-release / build (employees) (push) Successful in 1m50s
build-and-release / build (graphic-designers) (push) Successful in 1m54s
build-and-release / build (ugc-content-creators) (push) Successful in 2m46s
build-and-release / build (companies) (push) Successful in 2m6s
build-and-release / build (payments) (push) Successful in 1m55s
build-and-release / build (makeup-artists) (push) Successful in 2m38s
build-and-release / build (cron) (push) Successful in 2m22s
build-and-release / build (job-seekers) (push) Successful in 3m9s
build-and-release / build (customers) (push) Successful in 2m38s
build-and-release / build (developers) (push) Successful in 2m23s
build-and-release / build (photographers) (push) Successful in 2m51s
build-and-release / build (video-editors) (push) Successful in 2m45s
build-and-release / build (gateway) (push) Successful in 44s
- Add llm_moderation_check() — async call to LiteLLM /moderations endpoint
  (routes to OpenAI Moderation API, free) for hate/sexual/violence detection.
  Fails open: if LiteLLM is unreachable, message is allowed through with a
  warning log so core chat is never broken by a network hiccup.
- Add spawn_guard_log() — fire-and-forget tokio task that persists every
  guard rejection (keyword AND moderation) to ai_guard_violations table.
- Wire both guards into ai_chat_message, ai_chat_ask, and ai_chat_stream.
- Add migration: ai_guard_violations(id, user_id, message_excerpt,
  guard_type, reason, categories JSONB, created_at).
- Add GET /api/admin/ai/guard-events with filter by guard_type, pagination.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-15 14:20:44 +02:00
Ashwin Kumar Sivakumar
cc2dab112b fix(db): 4 fresh-bootstrap migration chain bugs found + verified today
All checks were successful
build-and-release / build (cron) (push) Successful in 58s
build-and-release / build (companies) (push) Successful in 1m59s
build-and-release / build (customers) (push) Successful in 2m9s
build-and-release / build (employees) (push) Successful in 2m17s
build-and-release / build (developers) (push) Successful in 2m29s
build-and-release / build (catering-services) (push) Successful in 2m45s
build-and-release / build (gateway) (push) Successful in 49s
build-and-release / build (jobs) (push) Successful in 42s
build-and-release / build (fitness-trainers) (push) Successful in 2m38s
build-and-release / build (graphic-designers) (push) Successful in 1m57s
build-and-release / build (payments) (push) Successful in 1m54s
build-and-release / build (job-seekers) (push) Successful in 2m57s
build-and-release / build (makeup-artists) (push) Successful in 2m47s
build-and-release / build (photographers) (push) Successful in 2m48s
build-and-release / build (social-media-managers) (push) Successful in 2m59s
backend-integration-tests / ai-credits (push) Successful in 50s
build-and-release / build (tutors) (push) Successful in 2m45s
build-and-release / build (ugc-content-creators) (push) Successful in 2m50s
build-and-release / build (video-editors) (push) Successful in 2m40s
build-and-release / build (users) (push) Successful in 4m47s
Reconciling the local dev DB (63 migrations behind) surfaced 4 genuine
bugs in the migration chain itself -- not just this DB's legacy
scripts/init-db.sql seeding -- confirmed by replaying the full chain
against a brand new, completely empty database from scratch:

- 20260318235959: customer_profiles.status is UPDATEd by
  20260319090000's backfill 4 months before it's ever ADDed
  (20260721020000). Adds it early (IF NOT EXISTS).
- 20260401235959: 20260317190000 creates the old users-linked
  `employees` shape; 20260402030000's CREATE TABLE IF NOT EXISTS no-ops
  against it and fails creating an index on a column that was never
  added. Conditionally drops the old shape first (only if it's still
  pre-transition -- gated on the missing `email` column), restoring
  20260402030000's own documented original intent.
- 20260419235959: the schema `20260420000003_external_role_modules`
  needs (persona_types, modules, role_module_access, ...) was only ever
  defined in a disabled .up.sql.skip (its version slot was taken by
  ...seed.sql); the seed data that depends on it was never skipped.
  Creates that schema early -- verbatim content, purely additive.
- 20260421235959: same disabled-migration pattern for the
  role_permissions -> role_admin_permissions / dashboard_configs ->
  role_sidebar_configs / runtime_configs -> role_runtime_configs /
  user_roles -> user_role_assignments renames that 20260422000000's
  widget seed needs. Conditionally renames each pair (only if old name
  exists and new name doesn't), verified against live Rust code that
  already queries the new names exclusively.

payments/invoices were NOT chain bugs -- confirmed clean on the fresh
bootstrap test -- so no fix needed for those; they only conflicted on
this one DB because of its scripts/init-db.sql legacy seed, which is
already a documented, known gap.

Verified twice: applied cleanly as no-ops against the now-fully-migrated
local dev DB, and applied successfully end-to-end (0 manual steps)
against a brand new empty database created from scratch.

Runbook updated with the full pending-migration list and today's
findings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 18:30:42 +05:30
Ashwin Kumar Sivakumar
c394d9fc07 feat(ai-credits): real coupon validation endpoint; restore invoice FK integrity
All checks were successful
build-and-release / build (employees) (push) Successful in 1m46s
build-and-release / build (customers) (push) Successful in 2m0s
build-and-release / build (cron) (push) Successful in 2m13s
build-and-release / build (companies) (push) Successful in 2m20s
build-and-release / build (catering-services) (push) Successful in 2m36s
build-and-release / build (developers) (push) Successful in 2m52s
build-and-release / build (gateway) (push) Successful in 53s
build-and-release / build (jobs) (push) Successful in 44s
build-and-release / build (fitness-trainers) (push) Successful in 1m39s
build-and-release / build (job-seekers) (push) Successful in 2m14s
build-and-release / build (payments) (push) Successful in 1m52s
build-and-release / build (graphic-designers) (push) Successful in 2m37s
build-and-release / build (makeup-artists) (push) Successful in 3m3s
build-and-release / build (photographers) (push) Successful in 2m48s
build-and-release / build (social-media-managers) (push) Successful in 2m40s
backend-integration-tests / ai-credits (push) Successful in 45s
build-and-release / build (tutors) (push) Successful in 2m44s
build-and-release / build (ugc-content-creators) (push) Successful in 2m44s
build-and-release / build (video-editors) (push) Successful in 2m45s
build-and-release / build (users) (push) Successful in 4m49s
- 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>
2026-08-14 15:26:47 +05:30
Ashwin Kumar Sivakumar
77c8330b11 feat(waitlist): POST /api/waitlist for the nxtgauge.com coming-soon form
All checks were successful
build-and-release / build (catering-services) (push) Successful in 1m28s
build-and-release / build (cron) (push) Successful in 1m51s
build-and-release / build (companies) (push) Successful in 2m7s
build-and-release / build (customers) (push) Successful in 2m54s
build-and-release / build (developers) (push) Successful in 1m39s
build-and-release / build (fitness-trainers) (push) Successful in 1m39s
build-and-release / build (gateway) (push) Successful in 1m31s
build-and-release / build (jobs) (push) Successful in 42s
build-and-release / build (employees) (push) Successful in 2m22s
build-and-release / build (graphic-designers) (push) Successful in 2m8s
build-and-release / build (makeup-artists) (push) Successful in 1m58s
build-and-release / build (payments) (push) Successful in 1m48s
build-and-release / build (job-seekers) (push) Successful in 3m4s
build-and-release / build (social-media-managers) (push) Successful in 2m39s
build-and-release / build (photographers) (push) Successful in 2m42s
backend-integration-tests / ai-credits (push) Successful in 50s
build-and-release / build (tutors) (push) Successful in 3m4s
build-and-release / build (ugc-content-creators) (push) Successful in 2m44s
build-and-release / build (video-editors) (push) Successful in 2m43s
build-and-release / build (users) (push) Successful in 4m38s
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>
2026-08-14 03:46:28 +05:30
Ashwin Kumar Sivakumar
c2fb7d61f7 feat(invoices): generate GST invoices for AI credit purchases
All checks were successful
build-and-release / build (developers) (push) Successful in 1m41s
build-and-release / build (catering-services) (push) Successful in 1m58s
build-and-release / build (companies) (push) Successful in 2m0s
build-and-release / build (cron) (push) Successful in 2m8s
build-and-release / build (customers) (push) Successful in 2m42s
build-and-release / build (gateway) (push) Successful in 1m2s
build-and-release / build (fitness-trainers) (push) Successful in 1m34s
build-and-release / build (jobs) (push) Successful in 44s
build-and-release / build (employees) (push) Successful in 1m50s
build-and-release / build (graphic-designers) (push) Successful in 2m41s
build-and-release / build (makeup-artists) (push) Successful in 1m49s
build-and-release / build (job-seekers) (push) Successful in 3m2s
build-and-release / build (photographers) (push) Successful in 2m40s
build-and-release / build (payments) (push) Successful in 2m50s
build-and-release / build (social-media-managers) (push) Successful in 2m55s
backend-integration-tests / ai-credits (push) Successful in 52s
build-and-release / build (ugc-content-creators) (push) Successful in 2m37s
build-and-release / build (tutors) (push) Successful in 2m56s
build-and-release / build (video-editors) (push) Successful in 2m41s
build-and-release / build (users) (push) Successful in 4m44s
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>
2026-08-14 03:17:17 +05:30
Tracewebstudio Dev
65262e842c fix(db): add missing ai_credit_orders columns + ai_coupons tables
All checks were successful
build-and-release / build (cron) (push) Successful in 1m0s
build-and-release / build (catering-services) (push) Successful in 1m53s
build-and-release / build (companies) (push) Successful in 2m2s
build-and-release / build (employees) (push) Successful in 2m17s
build-and-release / build (developers) (push) Successful in 2m32s
build-and-release / build (customers) (push) Successful in 2m39s
build-and-release / build (gateway) (push) Successful in 1m4s
build-and-release / build (fitness-trainers) (push) Successful in 2m38s
build-and-release / build (graphic-designers) (push) Successful in 1m49s
build-and-release / build (makeup-artists) (push) Successful in 1m37s
build-and-release / build (jobs) (push) Successful in 1m44s
build-and-release / build (job-seekers) (push) Successful in 2m57s
build-and-release / build (social-media-managers) (push) Successful in 1m43s
build-and-release / build (payments) (push) Successful in 2m47s
backend-integration-tests / ai-credits (push) Successful in 52s
build-and-release / build (photographers) (push) Successful in 2m57s
build-and-release / build (ugc-content-creators) (push) Successful in 2m31s
build-and-release / build (tutors) (push) Successful in 2m53s
build-and-release / build (video-editors) (push) Successful in 2m33s
build-and-release / build (users) (push) Successful in 4m52s
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>
2026-08-13 23:10:40 +02:00
Ashwin Kumar Sivakumar
41b17baa14 feat(ai-credits): role-scoped purchasable credit packages
All checks were successful
build-and-release / build (catering-services) (push) Successful in 1m25s
build-and-release / build (cron) (push) Successful in 1m50s
build-and-release / build (customers) (push) Successful in 1m55s
build-and-release / build (companies) (push) Successful in 2m0s
build-and-release / build (developers) (push) Successful in 2m22s
build-and-release / build (employees) (push) Successful in 2m34s
build-and-release / build (fitness-trainers) (push) Successful in 1m41s
build-and-release / build (gateway) (push) Successful in 1m32s
build-and-release / build (graphic-designers) (push) Successful in 2m3s
build-and-release / build (job-seekers) (push) Successful in 2m4s
build-and-release / build (jobs) (push) Successful in 2m7s
build-and-release / build (payments) (push) Successful in 1m45s
build-and-release / build (makeup-artists) (push) Successful in 2m42s
build-and-release / build (tutors) (push) Successful in 1m42s
build-and-release / build (photographers) (push) Successful in 2m45s
backend-integration-tests / ai-credits (push) Successful in 50s
build-and-release / build (social-media-managers) (push) Successful in 2m39s
build-and-release / build (ugc-content-creators) (push) Successful in 2m40s
build-and-release / build (video-editors) (push) Successful in 2m42s
build-and-release / build (users) (push) Successful in 4m38s
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>
2026-08-14 01:46:03 +05:30
Ashwin Kumar Sivakumar
8e4d64d33b fix(db): correct ai_credit_packages pricing (paise not rupees), add missing ai_credit_orders table
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>
2026-08-14 01:38:59 +05:30
Ashwin Kumar Sivakumar
eefc0457be fix(test): use a fresh random user per ai_credits_reaper.rs run
All checks were successful
build-and-release / build (catering-services) (push) Successful in 1m31s
build-and-release / build (cron) (push) Successful in 1m54s
build-and-release / build (companies) (push) Successful in 2m7s
build-and-release / build (customers) (push) Successful in 2m53s
build-and-release / build (fitness-trainers) (push) Successful in 1m29s
build-and-release / build (developers) (push) Successful in 1m43s
build-and-release / build (gateway) (push) Successful in 1m35s
build-and-release / build (employees) (push) Successful in 2m15s
build-and-release / build (jobs) (push) Successful in 1m10s
build-and-release / build (graphic-designers) (push) Successful in 2m9s
build-and-release / build (makeup-artists) (push) Successful in 1m35s
build-and-release / build (photographers) (push) Successful in 1m44s
build-and-release / build (job-seekers) (push) Successful in 3m8s
build-and-release / build (payments) (push) Successful in 2m45s
build-and-release / build (social-media-managers) (push) Successful in 2m35s
backend-integration-tests / ai-credits (push) Successful in 50s
build-and-release / build (tutors) (push) Successful in 3m10s
build-and-release / build (ugc-content-creators) (push) Successful in 2m49s
build-and-release / build (video-editors) (push) Successful in 2m48s
build-and-release / build (users) (push) Successful in 4m38s
The fixed UUID (33333333-...) meant every re-run against the same
persistent test database reused the same wallet - any earlier partial
run that panicked before its own capture/release step left
reserved_credits permanently stuck above zero, so this run's fresh
reserve (expected to bring it to exactly 4) actually reads 8, 12, etc.
depending on how many times the suite had previously failed midway.
Matches ai_credits.rs's make_user() pattern (Uuid::new_v4()) instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 22:09:33 +05:30
Ashwin Kumar Sivakumar
85690b3002 fix(test): make ai_credits_reaper.rs resilient to accumulated stale holds
Some checks failed
build-and-release / build (catering-services) (push) Successful in 1m30s
build-and-release / build (employees) (push) Successful in 1m42s
build-and-release / build (cron) (push) Successful in 2m11s
build-and-release / build (companies) (push) Successful in 2m16s
build-and-release / build (gateway) (push) Successful in 39s
build-and-release / build (developers) (push) Successful in 2m49s
build-and-release / build (customers) (push) Successful in 3m0s
build-and-release / build (graphic-designers) (push) Successful in 1m36s
build-and-release / build (jobs) (push) Successful in 1m46s
build-and-release / build (fitness-trainers) (push) Successful in 2m54s
build-and-release / build (job-seekers) (push) Successful in 2m17s
build-and-release / build (photographers) (push) Successful in 1m43s
build-and-release / build (payments) (push) Successful in 2m45s
build-and-release / build (makeup-artists) (push) Successful in 3m6s
build-and-release / build (social-media-managers) (push) Successful in 2m34s
backend-integration-tests / ai-credits (push) Failing after 1m7s
build-and-release / build (tutors) (push) Successful in 2m46s
build-and-release / build (ugc-content-creators) (push) Successful in 3m1s
build-and-release / build (users) (push) Has been cancelled
build-and-release / build (video-editors) (push) Has been cancelled
The sweep query is intentionally global (matches the real cron
reaper), but against a persistent, never-reset test database re-run
many times over a debugging session, other tests' un-captured/
un-released reservations eventually go stale and get picked up
alongside this test's own hold, breaking the exact-match assertion
(observed: 6 expired holds instead of 1). Clear pre-existing stale
'held' rows first, matching the clean slate a continuously-running
production reaper would actually maintain.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 22:01:24 +05:30
Ashwin Kumar Sivakumar
b48a1dd204 fix(test): self-provision the fixture user in ai_credits_reaper.rs
Some checks failed
build-and-release / build (cron) (push) Successful in 1m0s
build-and-release / build (catering-services) (push) Successful in 1m29s
build-and-release / build (companies) (push) Successful in 1m52s
build-and-release / build (customers) (push) Successful in 2m13s
build-and-release / build (employees) (push) Successful in 2m20s
build-and-release / build (gateway) (push) Successful in 1m1s
build-and-release / build (developers) (push) Successful in 3m9s
build-and-release / build (fitness-trainers) (push) Successful in 2m50s
build-and-release / build (job-seekers) (push) Successful in 1m54s
build-and-release / build (jobs) (push) Successful in 1m55s
build-and-release / build (graphic-designers) (push) Successful in 2m38s
build-and-release / build (makeup-artists) (push) Successful in 2m59s
build-and-release / build (photographers) (push) Successful in 1m52s
build-and-release / build (payments) (push) Successful in 2m45s
backend-integration-tests / ai-credits (push) Failing after 51s
build-and-release / build (social-media-managers) (push) Successful in 2m40s
build-and-release / build (tutors) (push) Successful in 2m51s
build-and-release / build (ugc-content-creators) (push) Successful in 2m53s
build-and-release / build (video-editors) (push) Has been cancelled
build-and-release / build (users) (push) Has been cancelled
expired_hold_is_swept_and_credits_return_to_available used a hardcoded
UUID assuming a matching users row already existed in whatever
database it ran against, but user_ai_subscriptions.user_id has a FK to
users(id) - against a fresh database (like CI's nxtgauge_test) this
failed with a foreign key violation before the actual sweep/release
logic under test ever ran. Provision it inline instead, matching
ai_credits.rs's make_user() pattern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 21:52:54 +05:30
Ashwin Kumar Sivakumar
1af10420ce fix(db): convert ai_credits timestamp columns to TIMESTAMPTZ
Some checks failed
build-and-release / build (cron) (push) Successful in 57s
build-and-release / build (customers) (push) Successful in 1m54s
build-and-release / build (employees) (push) Successful in 2m11s
build-and-release / build (companies) (push) Successful in 2m27s
build-and-release / build (catering-services) (push) Successful in 2m28s
build-and-release / build (gateway) (push) Successful in 48s
build-and-release / build (developers) (push) Successful in 3m3s
build-and-release / build (jobs) (push) Successful in 43s
build-and-release / build (fitness-trainers) (push) Successful in 2m36s
build-and-release / build (makeup-artists) (push) Successful in 1m45s
build-and-release / build (graphic-designers) (push) Successful in 2m38s
build-and-release / build (job-seekers) (push) Successful in 2m28s
build-and-release / build (photographers) (push) Successful in 2m34s
build-and-release / build (social-media-managers) (push) Successful in 2m34s
build-and-release / build (payments) (push) Successful in 3m20s
backend-integration-tests / ai-credits (push) Failing after 44s
build-and-release / build (tutors) (push) Successful in 2m41s
build-and-release / build (video-editors) (push) Has been cancelled
build-and-release / build (users) (push) Has been cancelled
build-and-release / build (ugc-content-creators) (push) Successful in 2m41s
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>
2026-08-13 21:44:30 +05:30
Ashwin Kumar Sivakumar
da9be7f6a3 feat(db,ci): fix missing ai_credits tables, wire integration tests to CI
Some checks failed
build-and-release / build (cron) (push) Successful in 1m10s
build-and-release / build (developers) (push) Successful in 1m40s
build-and-release / build (catering-services) (push) Successful in 1m58s
build-and-release / build (employees) (push) Successful in 2m19s
build-and-release / build (companies) (push) Successful in 2m36s
build-and-release / build (customers) (push) Successful in 2m45s
build-and-release / build (gateway) (push) Successful in 1m11s
build-and-release / build (jobs) (push) Successful in 40s
build-and-release / build (fitness-trainers) (push) Successful in 2m28s
build-and-release / build (graphic-designers) (push) Successful in 2m14s
build-and-release / build (payments) (push) Successful in 1m48s
build-and-release / build (job-seekers) (push) Successful in 2m57s
build-and-release / build (makeup-artists) (push) Successful in 2m49s
build-and-release / build (photographers) (push) Successful in 2m43s
backend-integration-tests / ai-credits (push) Failing after 31s
build-and-release / build (social-media-managers) (push) Successful in 2m58s
build-and-release / build (tutors) (push) Successful in 2m47s
build-and-release / build (ugc-content-creators) (push) Successful in 2m39s
build-and-release / build (users) (push) Has been cancelled
build-and-release / build (video-editors) (push) Has been cancelled
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>
2026-08-13 17:31:22 +05:30
Ashwin Kumar Sivakumar
f007ca0999 fix(db): re-enable wallet_full migration (tracecoin_holds/buckets)
All checks were successful
build-and-release / build (gateway) (push) Successful in 49s
build-and-release / build (employees) (push) Successful in 1m46s
build-and-release / build (ugc-content-creators) (push) Successful in 2m36s
build-and-release / build (companies) (push) Successful in 1m55s
build-and-release / build (catering-services) (push) Successful in 2m48s
build-and-release / build (fitness-trainers) (push) Successful in 1m45s
build-and-release / build (makeup-artists) (push) Successful in 1m43s
build-and-release / build (payments) (push) Successful in 2m52s
build-and-release / build (tutors) (push) Successful in 2m54s
build-and-release / build (job-seekers) (push) Successful in 7m28s
build-and-release / build (developers) (push) Successful in 2m28s
build-and-release / build (photographers) (push) Successful in 2m37s
build-and-release / build (video-editors) (push) Successful in 2m32s
build-and-release / build (users) (push) Successful in 4m55s
build-and-release / build (social-media-managers) (push) Successful in 7m32s
build-and-release / build (customers) (push) Successful in 2m11s
build-and-release / build (cron) (push) Successful in 2m14s
build-and-release / build (jobs) (push) Successful in 39s
build-and-release / build (graphic-designers) (push) Successful in 2m0s
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>
2026-08-12 23:18:51 +05:30
Tracewebstudio Dev
8423b7a0f5 fix: security, wallet holds, printpdf upgrade
- Upgrade printpdf 0.7 → 0.12.5 in job_seekers (fixes lopdf HIGH CVE RUSTSEC-2023-0068)
  Rewrote build_resume_pdf() for the new Op-based API; same PDF output
- Add wallet/me/holds and wallet/me/holds/{id}/release routes to profession_shared
  Backed by wallet::hold::list_for_user and wallet::hold::release
  Applies to all 9 profession services via the shared router
- Add deny.toml for cargo-deny (advisory + ban policy enforcement)
  RSA timing CVE and bincode unmaintained acknowledged with documented reasons

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-12 13:38:17 +02:00
Ashwin Kumar Sivakumar
80acfdd64f Add presigned document URLs to stop leaking permanent Backblaze links
All checks were successful
build-and-release / build (employees) (push) Successful in 1m34s
build-and-release / build (catering-services) (push) Successful in 1m43s
build-and-release / build (cron) (push) Successful in 1m59s
build-and-release / build (companies) (push) Successful in 2m4s
build-and-release / build (customers) (push) Successful in 2m26s
build-and-release / build (developers) (push) Successful in 2m38s
build-and-release / build (fitness-trainers) (push) Successful in 1m31s
build-and-release / build (gateway) (push) Successful in 1m29s
build-and-release / build (jobs) (push) Successful in 1m9s
build-and-release / build (graphic-designers) (push) Successful in 2m19s
build-and-release / build (job-seekers) (push) Successful in 2m16s
build-and-release / build (photographers) (push) Successful in 1m43s
build-and-release / build (makeup-artists) (push) Successful in 3m3s
build-and-release / build (social-media-managers) (push) Successful in 2m36s
build-and-release / build (ugc-content-creators) (push) Successful in 2m46s
build-and-release / build (payments) (push) Successful in 4m15s
build-and-release / build (video-editors) (push) Successful in 2m53s
build-and-release / build (tutors) (push) Successful in 4m24s
build-and-release / build (users) (push) Successful in 7m40s
Verification documents were being stored/rendered as permanent, unsigned
B2 URLs across every role's admin review and self-service dashboard.
Add StorageClient::presign() plus two mediating endpoints (admin and
self-service) so viewers always get a short-lived signed URL instead of
the raw storage link.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 18:07:15 +05:30
Ashwin Kumar Sivakumar
cd695514fe Surface real B2 upload errors and enforce 10MB body limit on document uploads
All checks were successful
build-and-release / build (developers) (push) Successful in 1m27s
build-and-release / build (employees) (push) Successful in 1m52s
build-and-release / build (customers) (push) Successful in 1m56s
build-and-release / build (cron) (push) Successful in 2m16s
build-and-release / build (companies) (push) Successful in 2m21s
build-and-release / build (catering-services) (push) Successful in 2m29s
build-and-release / build (gateway) (push) Successful in 39s
build-and-release / build (fitness-trainers) (push) Successful in 2m0s
build-and-release / build (graphic-designers) (push) Successful in 1m52s
build-and-release / build (jobs) (push) Successful in 1m34s
build-and-release / build (job-seekers) (push) Successful in 2m4s
build-and-release / build (photographers) (push) Successful in 1m35s
build-and-release / build (makeup-artists) (push) Successful in 2m41s
build-and-release / build (payments) (push) Successful in 3m15s
build-and-release / build (tutors) (push) Successful in 2m16s
build-and-release / build (social-media-managers) (push) Successful in 2m33s
build-and-release / build (ugc-content-creators) (push) Successful in 2m47s
build-and-release / build (video-editors) (push) Successful in 2m32s
build-and-release / build (users) (push) Successful in 4m11s
Document upload endpoints (job_seekers, customers, companies, and the
profession_shared crate used by 10 profession apps) returned a generic
"File upload failed" 500 on any storage error, hiding the actual cause
from both the API response and (for job_seekers/companies) the server
logs, which only printed anyhow's outer context via Display instead of
the full error chain via Debug.

Also add an explicit DefaultBodyLimit(11MB) to every affected app's
router — none had one, so axum's implicit 2MB default could silently
reject uploads under the UI's advertised 10MB cap.
2026-07-26 19:25:53 +05:30
Ashwin Kumar Sivakumar
fdd5c7a418 Fix security audit findings: admin authz, captcha, secret leak, CORS
All checks were successful
build-and-release / build (developers) (push) Successful in 5m31s
build-and-release / build (catering-services) (push) Successful in 5m56s
build-and-release / build (employees) (push) Successful in 6m23s
build-and-release / build (companies) (push) Successful in 6m31s
build-and-release / build (cron) (push) Successful in 6m38s
build-and-release / build (customers) (push) Successful in 7m19s
build-and-release / build (gateway) (push) Successful in 1m28s
build-and-release / build (fitness-trainers) (push) Successful in 2m2s
build-and-release / build (jobs) (push) Successful in 1m12s
build-and-release / build (graphic-designers) (push) Successful in 2m20s
build-and-release / build (job-seekers) (push) Successful in 2m24s
build-and-release / build (photographers) (push) Successful in 2m20s
build-and-release / build (makeup-artists) (push) Successful in 2m55s
build-and-release / build (tutors) (push) Successful in 2m15s
build-and-release / build (payments) (push) Successful in 3m48s
build-and-release / build (ugc-content-creators) (push) Successful in 2m35s
build-and-release / build (social-media-managers) (push) Successful in 4m46s
build-and-release / build (video-editors) (push) Successful in 2m41s
build-and-release / build (users) (push) Successful in 8m7s
- Require admin role on role/module/permission management endpoints
  that previously accepted any authenticated user (privilege escalation)
- Add server-side captcha generation/verification (Redis-backed,
  single-use, 5 min TTL) enforced on register/login for users and
  employees services
- Untrack .env.test111 (contained a live SMTP key) and harden
  .gitignore against future .env commits
- Stop logging OTP codes in plaintext
- Restrict jobs service CORS to an explicit origin allowlist
- Mask PayU merchant secret/salt in payment-gateway-config responses,
  preserving the stored value on save when the field is left unchanged
- Bump vulnerable transitive dependencies (quinn-proto, rustls-webpki,
  anyhow) via cargo update; switch aws-sdk-s3 off the legacy rustls
  feature

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 17:31:48 +05:30
Ashwin Kumar Sivakumar
73beb03368 fix: seed verification_status into JOB_SEEKER dashboard widgets
All checks were successful
build-and-release / build (catering-services) (push) Successful in 1m45s
build-and-release / build (developers) (push) Successful in 1m43s
build-and-release / build (companies) (push) Successful in 1m53s
build-and-release / build (cron) (push) Successful in 2m9s
build-and-release / build (customers) (push) Successful in 2m27s
build-and-release / build (employees) (push) Successful in 1m46s
build-and-release / build (fitness-trainers) (push) Successful in 1m48s
build-and-release / build (graphic-designers) (push) Successful in 1m47s
build-and-release / build (jobs) (push) Successful in 1m32s
build-and-release / build (gateway) (push) Successful in 2m21s
build-and-release / build (job-seekers) (push) Successful in 2m53s
build-and-release / build (photographers) (push) Successful in 1m32s
build-and-release / build (makeup-artists) (push) Successful in 1m58s
build-and-release / build (social-media-managers) (push) Successful in 2m17s
build-and-release / build (payments) (push) Successful in 2m51s
build-and-release / build (tutors) (push) Successful in 2m38s
build-and-release / build (ugc-content-creators) (push) Successful in 2m41s
build-and-release / build (video-editors) (push) Successful in 2m39s
build-and-release / build (users) (push) Successful in 4m13s
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>
2026-07-23 04:26:05 +05:30
Ashwin Kumar Sivakumar
3e701f2fe6 fix: close two real money/spend races (Tracecoin double-credit, unbounded AI overspend)
Some checks failed
build-and-release / build (ugc-content-creators) (push) Waiting to run
build-and-release / build (users) (push) Waiting to run
build-and-release / build (video-editors) (push) Waiting to run
build-and-release / build (catering-services) (push) Successful in 1m47s
build-and-release / build (companies) (push) Successful in 2m0s
build-and-release / build (cron) (push) Successful in 2m12s
build-and-release / build (customers) (push) Successful in 2m42s
build-and-release / build (gateway) (push) Successful in 1m0s
build-and-release / build (developers) (push) Successful in 1m27s
build-and-release / build (fitness-trainers) (push) Successful in 1m54s
build-and-release / build (employees) (push) Successful in 1m59s
build-and-release / build (job-seekers) (push) Successful in 1m57s
build-and-release / build (makeup-artists) (push) Successful in 1m39s
build-and-release / build (graphic-designers) (push) Has been cancelled
build-and-release / build (payments) (push) Has been cancelled
build-and-release / build (jobs) (push) Has been cancelled
build-and-release / build (social-media-managers) (push) Has been cancelled
build-and-release / build (tutors) (push) Has been cancelled
build-and-release / build (photographers) (push) Has been cancelled
Asked to review Tracecoin and AI implementation safety. Found and fixed
two exploitable TOCTOU races, plus a data-integrity bug:

1. apps/payments/src/main.rs::verify_payment — the PayU success callback
   is called directly by the client (not a server-to-server webhook), so
   a user fully controls how many times they replay a valid success
   payload. The payment "is it still PENDING" check and the "mark
   SUCCESS + credit wallet" write were separate, non-transactional
   queries — concurrent replays could both pass the check before either
   commits, double- (or N-times-) crediting the wallet for one real
   payment. Now wrapped in a single transaction with
   `SELECT ... FOR UPDATE` on the payments row, so a second concurrent
   call blocks until the first commits, then correctly sees the row is
   no longer PENDING (Postgres re-evaluates the WHERE clause via
   EvalPlanQual after the lock is granted).

2. crates/db/src/models/ai/repository.rs — UserAiSubscriptionRepository
   had the exact same shape of bug: apps/users/src/ai/credits.rs::
   charge_feature read the subscription, checked daily-limit and credit
   balance, THEN issued two separate unconditional `UPDATE ... SET x =
   x + $1` statements with no WHERE guard on the balance. N concurrent
   requests from one user all pass the check before any deduction
   lands, running up unlimited LLM API spend (this endpoint is called
   before/around real LiteLLM calls, so the cost is real). Added
   UserAiSubscriptionRepository::try_charge — a single conditional
   UPDATE that checks the daily limit and credit balance and deducts
   atomically, returning None (mapped to the existing error types) if
   either check fails.

3. apps/cron/src/tasks/auto_apply.rs — daily_actions_used was being
   incremented twice per auto-applied job (once in the credit-deduct
   UPDATE, once more in a second, redundant UPDATE right after) —
   silently halving job seekers' effective daily auto-apply limit.
   Removed the redundant second UPDATE.

Also added non-negative CHECK constraints directly to the live
database (tracecoin_wallets.balance/reserved,
user_ai_subscriptions.daily_actions_used/monthly_credits_used/
purchased_credits_used) as defense in depth — belt-and-suspenders in
case a future code path reintroduces a similar bug.
2026-07-21 05:51:23 +05:30
Ashwin Kumar Sivakumar
9309334d52 seed: add a starter Tracecoin package per role; fix paise unit bug in invoice line item
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.
2026-07-21 03:47:54 +05:30
Ashwin Kumar Sivakumar
68f659903c fix: audit_logs, tax_rules, reviews tables never existed; fix wrong column in review creation
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.
2026-07-21 03:05:14 +05:30
Ashwin Kumar Sivakumar
63fd3f5135 feat: generate a GST invoice automatically after a successful Tracecoin/PayU purchase
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.
2026-07-21 03:02:47 +05:30
Ashwin Kumar Sivakumar
119c39e184 fix: payments/pricing_packages tables never existed either
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.
2026-07-21 02:28:57 +05:30
Ashwin Kumar Sivakumar
36c555dc55 fix: verifications/approval_requests tables never existed — the single biggest gap
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.
2026-07-21 02:26:15 +05:30
Ashwin Kumar Sivakumar
442dac8c04 fix: portfolio_items/services tables never existed; wallet ledger read used stale column names
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).
2026-07-21 02:21:52 +05:30
Ashwin Kumar Sivakumar
94a8a1096a fix: three more already-deployed migrations unconditionally ALTERed tables that were never created
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.
2026-07-21 02:18:23 +05:30
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
Ashwin Kumar Sivakumar
b5dea58ed4 fix: customer document submission and profile verification
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.
2026-07-21 01:39:25 +05:30
Ashwin Kumar Sivakumar
28b09aa019 fix: professionals (photographer, tutor, developer, etc.) couldn't submit profiles/documents
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.
2026-07-21 01:19:49 +05:30
Ashwin Kumar Sivakumar
2e95c4750b fix: company job posting, job seeker profile submission, and job applications
- 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.
2026-07-21 01:00:47 +05:30
Ashwin Kumar Sivakumar
e4c9fc31ee feat: human-readable reference numbers + fix verification document visibility
All checks were successful
build-and-release / build (developers) (push) Successful in 1m42s
build-and-release / build (companies) (push) Successful in 1m48s
build-and-release / build (catering-services) (push) Successful in 1m55s
build-and-release / build (cron) (push) Successful in 2m4s
build-and-release / build (customers) (push) Successful in 2m26s
build-and-release / build (employees) (push) Successful in 1m24s
build-and-release / build (fitness-trainers) (push) Successful in 1m37s
build-and-release / build (gateway) (push) Successful in 1m38s
build-and-release / build (graphic-designers) (push) Successful in 1m54s
build-and-release / build (jobs) (push) Successful in 1m48s
build-and-release / build (leads) (push) Successful in 1m40s
build-and-release / build (job-seekers) (push) Successful in 2m52s
build-and-release / build (photographers) (push) Successful in 1m50s
build-and-release / build (payments) (push) Successful in 2m16s
build-and-release / build (makeup-artists) (push) Successful in 2m48s
build-and-release / build (tutors) (push) Successful in 2m16s
build-and-release / build (social-media-managers) (push) Successful in 2m42s
build-and-release / build (ugc-content-creators) (push) Successful in 2m36s
build-and-release / build (video-editors) (push) Successful in 2m19s
build-and-release / build (users) (push) Successful in 5m10s
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>
2026-07-19 16:34:00 +05:30
Ashwin Kumar Sivakumar
dbb02e54cc fix(db): stop wiping employees table on every migration run, restore phone column
All checks were successful
build-and-release / build (catering-services) (push) Successful in 1m43s
build-and-release / build (companies) (push) Successful in 1m56s
build-and-release / build (cron) (push) Successful in 2m10s
build-and-release / build (customers) (push) Successful in 2m29s
build-and-release / build (gateway) (push) Successful in 51s
build-and-release / build (fitness-trainers) (push) Successful in 1m22s
build-and-release / build (developers) (push) Successful in 1m50s
build-and-release / build (employees) (push) Successful in 2m42s
build-and-release / build (job-seekers) (push) Successful in 2m1s
build-and-release / build (jobs) (push) Successful in 2m10s
build-and-release / build (graphic-designers) (push) Successful in 2m51s
build-and-release / build (leads) (push) Successful in 2m22s
build-and-release / build (photographers) (push) Successful in 1m42s
build-and-release / build (makeup-artists) (push) Successful in 2m49s
build-and-release / build (payments) (push) Successful in 2m11s
build-and-release / build (social-media-managers) (push) Successful in 2m33s
build-and-release / build (tutors) (push) Successful in 2m36s
build-and-release / build (ugc-content-creators) (push) Successful in 2m12s
build-and-release / build (video-editors) (push) Successful in 2m38s
build-and-release / build (users) (push) Successful in 4m29s
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>
2026-07-19 05:40:48 +05:30
Ashwin Kumar Sivakumar
bb616c6db1 fix(db): make three more migrations idempotent; log swallowed login DB error
All checks were successful
build-and-release / build (customers) (push) Successful in 1m23s
build-and-release / build (employees) (push) Successful in 1m48s
build-and-release / build (catering-services) (push) Successful in 1m57s
build-and-release / build (companies) (push) Successful in 2m6s
build-and-release / build (cron) (push) Successful in 2m23s
build-and-release / build (gateway) (push) Successful in 51s
build-and-release / build (developers) (push) Successful in 2m44s
build-and-release / build (fitness-trainers) (push) Successful in 1m39s
build-and-release / build (jobs) (push) Successful in 45s
build-and-release / build (graphic-designers) (push) Successful in 2m16s
build-and-release / build (job-seekers) (push) Successful in 2m16s
build-and-release / build (leads) (push) Successful in 1m46s
build-and-release / build (payments) (push) Successful in 2m19s
build-and-release / build (makeup-artists) (push) Successful in 3m0s
build-and-release / build (photographers) (push) Successful in 2m42s
build-and-release / build (social-media-managers) (push) Successful in 2m19s
build-and-release / build (ugc-content-creators) (push) Successful in 2m33s
build-and-release / build (tutors) (push) Successful in 2m42s
build-and-release / build (video-editors) (push) Successful in 2m44s
build-and-release / build (users) (push) Successful in 6m38s
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>
2026-07-19 04:40:31 +05:30
Ashwin Kumar Sivakumar
e80ce2901c fix(db): make ai_plans_and_limits migration idempotent
All checks were successful
build-and-release / build (cron) (push) Successful in 48s
build-and-release / build (catering-services) (push) Successful in 1m31s
build-and-release / build (companies) (push) Successful in 1m34s
build-and-release / build (developers) (push) Successful in 1m59s
build-and-release / build (customers) (push) Successful in 2m10s
build-and-release / build (employees) (push) Successful in 2m15s
build-and-release / build (gateway) (push) Successful in 59s
build-and-release / build (jobs) (push) Successful in 46s
build-and-release / build (graphic-designers) (push) Successful in 1m33s
build-and-release / build (fitness-trainers) (push) Successful in 2m44s
build-and-release / build (makeup-artists) (push) Successful in 1m53s
build-and-release / build (job-seekers) (push) Successful in 2m30s
build-and-release / build (leads) (push) Successful in 2m32s
build-and-release / build (photographers) (push) Successful in 2m33s
build-and-release / build (payments) (push) Successful in 2m53s
build-and-release / build (social-media-managers) (push) Successful in 2m38s
build-and-release / build (ugc-content-creators) (push) Successful in 2m19s
build-and-release / build (tutors) (push) Successful in 2m53s
build-and-release / build (video-editors) (push) Successful in 2m14s
build-and-release / build (users) (push) Successful in 4m35s
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>
2026-07-19 03:14:02 +05:30
Ashwin Kumar Sivakumar
466e7abf03 fix(db): point verification_logs FK at verifications, not legacy verification_requests
Some checks failed
build-and-release / build (gateway) (push) Successful in 55s
build-and-release / build (graphic-designers) (push) Successful in 2m13s
build-and-release / build (social-media-managers) (push) Has been cancelled
build-and-release / build (video-editors) (push) Has been cancelled
build-and-release / build (employees) (push) Successful in 2m0s
build-and-release / build (developers) (push) Successful in 2m43s
build-and-release / build (leads) (push) Successful in 1m49s
build-and-release / build (photographers) (push) Successful in 2m17s
build-and-release / build (tutors) (push) Has been cancelled
build-and-release / build (cron) (push) Successful in 47s
build-and-release / build (companies) (push) Successful in 2m6s
build-and-release / build (jobs) (push) Successful in 44s
build-and-release / build (customers) (push) Successful in 2m12s
build-and-release / build (fitness-trainers) (push) Successful in 2m20s
build-and-release / build (payments) (push) Successful in 2m45s
build-and-release / build (makeup-artists) (push) Successful in 3m10s
build-and-release / build (users) (push) Has been cancelled
build-and-release / build (ugc-content-creators) (push) Has been cancelled
build-and-release / build (catering-services) (push) Successful in 1m36s
build-and-release / build (job-seekers) (push) Successful in 2m22s
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>
2026-07-19 03:01:25 +05:30
Ashwin Kumar Sivakumar
8a29b81c84 feat(admin-auth): add POST /api/admin/auth/refresh for sliding session window
All checks were successful
build-and-release / build (gateway) (push) Successful in 1m33s
build-and-release / build (graphic-designers) (push) Successful in 2m13s
build-and-release / build (ugc-content-creators) (push) Successful in 2m12s
build-and-release / build (users) (push) Successful in 4m48s
build-and-release / build (catering-services) (push) Successful in 1m22s
build-and-release / build (job-seekers) (push) Successful in 3m20s
build-and-release / build (companies) (push) Successful in 2m36s
build-and-release / build (social-media-managers) (push) Successful in 2m18s
build-and-release / build (tutors) (push) Successful in 2m41s
build-and-release / build (customers) (push) Successful in 1m38s
build-and-release / build (employees) (push) Successful in 3m16s
build-and-release / build (makeup-artists) (push) Successful in 1m46s
build-and-release / build (photographers) (push) Successful in 2m39s
build-and-release / build (jobs) (push) Successful in 1m5s
build-and-release / build (cron) (push) Successful in 1m40s
build-and-release / build (leads) (push) Successful in 2m18s
build-and-release / build (payments) (push) Successful in 3m10s
build-and-release / build (fitness-trainers) (push) Successful in 1m25s
build-and-release / build (video-editors) (push) Successful in 2m11s
build-and-release / build (developers) (push) Successful in 1m43s
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>
2026-07-19 00:14:08 +05:30