[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:
| Set | Definition | Count | Decision |
|---|---|---|---|
| G | live — user enabled, company status = 1 | 1,616 | Migrate |
| S | SUPER_HQ_EXTERNAL (company link via user_company pivot) | 72 | Migrate ~66 valid |
| E | company disabled (status = 0) | 1,157 | Skip — 94% have not posted a gig job in 2+ years |
| F | user disabled, company live | 348 | Skip |
| C | obsolete company (the 11) | 57 | Skip |
| A | user deleted (is_deleted = 1) | 1 | Skip |
| B | no company (orphan) | 1 | Skip |
| D | company deleted_at set | 0 | empty 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::Userbyremote_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
revokedmembership — 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: trueandis_phone_verified: truefor migrated employers. JodGig already trusted them; the login verification gate must not block them. Identities::User.mobilecarries a unique placeholder at migration time (the jodgigcontact_numberis a shared office number for 1,047 employers, andmobileisUNIQUEin 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_*_notificationcolumns from sub-issue 2.1. identities_users.address_geo_area_idfor a migrated employer = the employer's company'saddress_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 (matchusers.company_idif present, else earliest bycompanies.created_at).- No
remote_gig_user_idonorg_memberships.identities_users.remote_gig_user_idis the single source of truth (UNIQUE in production). Membership-side queries join throughidentities_users. org_memberships.is_defaultandis_ownerbecome realbooleancolumns (sub-issue 2.2). They arevarcharstoring'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.mdper 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_defaultandis_ownerfromvarchartoboolean
Theme 3 — Sync skeleton (wishful thinking)
- 3.1 — Skeleton:
EmployersSyncServicewith 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::Userupsert 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.