Skip to main content

[Employer Sync] Epic — Migrate JodGig employer users into JodApp

TL;DR: Redo of PR #1634 as 18 scoped sub-issues across four phases. Migrate ~1,682 JodGig employer users into JodApp Identities::User + Org::Membership + Org::OutletAssignment, keyed on remote_gig_user_id, with per-record errors, placeholder mobiles, and a JIT login path.

Visual overview: see Employer Sync — Epic Overview for the seven-set partition flow, the phase/dependency graph, and the per-employer + login decision flows.

Context

JodGig is the legacy gig platform. JodApp is the new platform. Both run in production at the same time. We are moving features over one at a time — Org::Company and Org::Outlet are already migrated. This epic moves the people who manage those companies: jodgig users with user_type in HQ, AREA, LOCATION, SUPER_HQ_EXTERNAL. After this epic, an existing JodGig employer logs into JodApp with the same email and password, and lands on an Org::Membership that lets them post Ads::Campaign and Careers::Job.

Why now

The business goal is direct. Ads and full-time Careers jobs are paid products. Selling them to clients we already have is the fastest revenue path. A migrated employer who can log in without registering again is a sale we keep. Mobile-number accuracy matters because the transactional notifications go through WhatsApp (with SMS as a backup) — for example, we see employers ignoring talent applications, and a WhatsApp flow that lets them accept or reject candidates from their phone (without opening the dashboard) lifts that completion rate.

What happened before

A first attempt landed as PR #1634 in jod-app/jodapp-api. After a code-and-data review, it is being redone here. PR #1634's central choices — matching Identities::User by email, one rescue around 2,000 records, no JIT for employers, no login-flow changes — cascade through every file in the sync, so a rewrite is cleaner than iteration. PR #1634 stays open as a reference; it is closed when the sub-issues here land. The full review is at docs/30-49-domains/33-org/gig-migrations/jodgig-employer-sync.md.

The data picture

Audited on jodgig production in May 2026. Universe = 3,252 employer users. Mutually exclusive sets:

SetDefinitionCountDecision
Glive — user enabled, company status = 11,616Migrate
SSUPER_HQ_EXTERNAL (company link via user_company pivot)72Migrate ~66 valid
Ecompany disabled (status = 0)1,157Skip — 94% have not posted a gig job in 2+ years
Fuser disabled, company live348Skip
Cobsolete company (the 11)57Skip
Auser deleted (is_deleted = 1)1Skip
Bno company (orphan)1Skip
Dcompany deleted_at set0empty set

Migration target ≈ 1,682 employers. Inside that target, ~862 are active (logged in within 2 years) and ~820 are dormant-but-live — those are the re-engagement targets for sales.

Confirmed decisions

  • Match Identities::User by remote_gig_user_id (not email). One change kills the email-collision class of bugs in the doc (A1, A2, E1).
  • Per-record error handling inside the batch loop. One bad record fails one record, not 2,000.
  • One owner per company. The HQ user is the owner. For super-HQ-only companies, the user matching companies.created_by, else the earliest super-HQ membership.
  • Skip-on-create for disabled / closed-company / orphan employers. But for an employer already in jodapp who later becomes disabled in jodgig, propagate to a revoked membership — never silently filter (or JodApp keeps a working login after offboarding).
  • SUPER_HQ_INTERNAL, INTERNAL, SUPER_ADMIN, USER_ADMIN, blank user_type — all jod-internal or junk. Excluded.
  • Re-hash non-bcrypt passwords on login in Identities::Sessions::CreateManager. Low priority — the legacy-hash cohort (1,363 users, 42%) is dormant — but the fix is small and correct.
  • Force is_email_verified: true and is_phone_verified: true for migrated employers. JodGig already trusted them; the login verification gate must not block them.
  • Identities::User.mobile carries a unique placeholder at migration time (the jodgig contact_number is a shared office number for 1,047 employers, and mobile is UNIQUE in JodApp). Real personal mobiles are collected via a non-blocking prompt on first login.
  • JodGig notification settings (email / WhatsApp / SMS) are NOT migrated. JodApp will own notification preferences as its own concern; JodGig's notification data is unreliable. This removes the F1 swap bug from scope (the data is not carried at all) and removes the three allow_*_notification columns from sub-issue 2.1.
  • identities_users.address_geo_area_id for a migrated employer = the employer's company's address_geo_area_id. The column is NOT NULL in production; a jodgig employer has no personal address. For super-HQ users across multiple companies, use the precedence rule (match users.company_id if present, else earliest by companies.created_at).
  • No remote_gig_user_id on org_memberships. identities_users.remote_gig_user_id is the single source of truth (UNIQUE in production). Membership-side queries join through identities_users.
  • org_memberships.is_default and is_owner become real boolean columns (sub-issue 2.2). They are varchar storing 't'/'f' today; 4.3 writes booleans after 2.2 lands.

Six themes, 18 sub-issues

Theme 1 — Data cleaning and scope

  • 1.2 — (Optional) JodGig data cleanup and real-mobile collection plan

The spec-doc update originally drafted as 1.1 was done directly in jodgig-employer-sync.md per the spec-first principle. The doc is the contract, not a sub-issue.

Theme 2 — Database migrations (jodapp-api)

  • 2.1 — Upsert-key uniqueness: 3 unique-index migrations with pre-flights
  • 2.2 — Convert org_memberships.is_default and is_owner from varchar to boolean

Theme 3 — Sync skeleton (wishful thinking)

  • 3.1 — Skeleton: EmployersSyncService with stubbed mappers

Theme 4 — Implement the wished functions

  • 4.1 — Implement select_employers_in_scope
  • 4.2 — Map JodGig employer → Identities::User
  • 4.3 — Map → Org::Membership (with one-owner rule)
  • 4.4 — Map → Org::OutletAssignment
  • 4.5 — Re-hash legacy (non-bcrypt) passwords on login

Theme 5 — Around the sync

  • 5.1 — Extract a shared Identities::User upsert service
  • 5.2 — JIT detect-and-enqueue on employer login
  • 5.3 — Run employer sync hourly and enforce cron ordering
  • 5.4 — Verify session response does not assume Talent::Profile

Theme 6 — Frontend (jodapp-web)

  • 6.1 — Rewrite employer login and signup copy
  • 6.2 — Handle the membership-less employer (stop redirecting to create-account)
  • 6.3 — Honest login states on the login page (provisioning, needs-attention)
  • 6.4 — Non-blocking prompt for migrated employers to set their real mobile
  • 6.5 — Employer login page UI/UX redesign (Bootstrap only)

Sequencing

  • Phase A (parallel, start now): 1.2, 2.1, 2.2, 5.4, 6.1, 6.2, 6.4
  • Phase B: 3.1 (depends on 2.1)
  • Phase C: 4.1, 4.2, 4.3, 4.4, 4.5, 5.3, 6.3, 6.5 (parallel where possible; 4.3 also depends on 2.2)
  • Phase D: 5.1 → 5.2

When the sub-issues in this epic are landed, PR #1634 is closed.