Skip to main content

Phase 3: apply the calculated plan

The writer receives the planner's output. It does not rediscover eligibility or invent different mapping rules while writing.

Outcome

TL;DR

Apply each person's complete plan atomically, continue after isolated failures, and make a second identical run do nothing.

At the end of this phase, the employer sync can be started in preview mode or write mode. Both modes use the same planning rules and produce the same kind of report. The sync can:

  • run in dry-run or write mode using the same planner;
  • create and update eligible identities;
  • reconcile JodGig memberships and outlet assignments;
  • revoke access that disappeared;
  • preserve native JodApp memberships;
  • report every skip, review case and failure; and
  • be safely repeated.

Use one supported way to start the sync

The manual command and the scheduled job must call the same employer-sync code. The caller chooses whether to preview the changes or write them. This prevents the manual run and daily run from developing different rules.

ModePlannerWriterReport
Dry runYesNoYes
Manual applyYesYesYes
Scheduled applyYesYesYes

Do not create a second single-user employer migration for login. An exceptional same-day onboarding uses the same full reconciliation manually.

Only one write-mode employer reconciliation may run at a time. Use the project's established job uniqueness or locking convention so two manual or scheduled runs cannot compete.

Apply one person at a time

TL;DR

One person's identity and access succeed together or roll back together. Another malformed person does not undo the whole run.

For each plan:

  1. skip skip records without writing;
  2. record needs_review without writing;
  3. open a database transaction for migrate, update or revoke;
  4. apply identity changes when the plan requires them;
  5. reconcile memberships;
  6. reconcile outlet assignments;
  7. commit the person's transaction; and
  8. on error, roll back that person, record the failure and continue.

Use raising writes such as save! or update!. Silent validation failures make the final run status dishonest.

Write identities with guarded ownership

SituationWriter action
Planner found identity by remote JodGig IDUpdate that identity
Planner approved an unlinked email matchStamp the stable JodGig ID, then update
Planner found no match for eligible userCreate one identity
Planner emitted conflictWrite nothing
Source user became ineligibleKeep identity history; reconcile JodGig memberships to revoked

The writer must confirm the planner's identity assumptions are still true inside the transaction. If another process linked or changed the identity after planning, stop that person's write and report a conflict.

Reconcile source-owned identity fields

TL;DR

Later JodGig changes replace copied JodApp values; migrated users cannot edit those fields in JodApp.

Write every agreed copied field on creation and update, not only on insertion. This includes normalized email and the three notification inputs used by memberships.

Before changing an email, confirm that no other identity owns the normalized value. A collision is a review case, not an automatic merge.

Do not globally deactivate the identity when legacy employer access disappears. Revoke JodGig-provisioned memberships so candidate and native JodApp access can continue.

Apply password changes safely

Source and destination stateWriter action
Source is BCryptCopy it after the required PHP-to-Ruby prefix adjustment
Both still hold the same legacy hashKeep the legacy value
Source legacy hash is unchanged and destination was upgraded to BCrypt after loginPreserve destination BCrypt
Source later changes to BCryptCopy the new source value

Login must inspect the stored format before choosing the checker:

  • BCrypt formats use normal Rails authentication;
  • legacy formats use the existing JodGig password checker;
  • after a successful legacy check, save the submitted password through Rails so the destination becomes BCrypt.

Never log either source or destination password hashes.

Reconcile memberships by provenance

TL;DR

Make only provisioning_source = jodgig memberships agree with the plan.

For every desired JodGig membership:

  1. find or initialize it by identity and company;
  2. reject a collision if that pair already exists with provisioning_source = jodapp;
  3. assign role, status, title, default and the three matching notification fields;
  4. set is_owner = false;
  5. set provisioning_source = jodgig; and
  6. save with a raising write.

A native membership for the same identity and company represents an ambiguous ownership case because the unique pair cannot store a second row. Report it for a human decision; do not silently adopt it as JodGig-owned.

After writing the desired set, revoke existing JodGig memberships absent from it. Never revoke rows whose source is jodapp.

Maintain one default

SituationDefault action
Ordinary employer has one active JodGig membershipMark it default unless an active native default is deliberately retained by the application rule
External jodgig.users.company_id is one valid desired grantUse that membership
Source company is missing or invalidUse the valid grant with the highest jodgig.user_company.id
Previous default is revokedMove default to the calculated valid membership
No active memberships remainClear default flags

The final database state must contain at most one default for an identity. Enforce this transactionally; do not depend on write order.

Reconcile outlet assignments

User typeWriter behaviour
HQ or external super-HQEnsure no JodGig-provisioned scoped assignments remain
LOCATIONKeep exactly the one planned outlet assignment
AREAKeep exactly the planned set of area outlets

Create desired assignments using the unique membership-and-outlet key. Remove JodGig-owned assignments absent from the plan.

If Org::OutletAssignment does not currently record provenance, ownership can be inherited from its membership because native assignments cannot belong to a JodGig-owned membership without becoming part of that membership's access definition. If the product needs mixed-source assignments inside one membership, stop and define that requirement before implementation.

Record an honest run result

CountMeaning
ExaminedEmployer-type source users read
MigratedNew identities successfully created
UpdatedExisting linked identities successfully reconciled
RevokedPeople whose JodGig access was removed
SkippedIneligible people with no previous access
Needs reviewAmbiguous cases with no writes
FailedUnexpected exceptions rolled back
Membership and assignment changesCreated, reactivated, updated, revoked or removed rows

A run with failures or unresolved review cases is not fully successful. Do not use a “last successful timestamp” to filter the next run; the next run reads the complete source again.

Store reason codes and identifiers needed for investigation, but no personal data or password material.

Repeat safety

TL;DR

The first run converges JodApp to the plan. The second run observes that agreement and performs no material writes.

Avoid unconditional timestamp updates. Compare intended attributes with current values before saving so the report can distinguish a genuine update from an already-correct row.

Repeat safety must cover removal as well as creation:

  • re-running does not duplicate identities;
  • re-running does not duplicate memberships or assignments;
  • an already-revoked membership remains revoked without another change;
  • a native membership is never absorbed into the legacy set; and
  • a locally upgraded BCrypt password is not replaced by an unchanged legacy hash.

Writer tests

ScenarioExpected database result
Planner emits migrateIdentity, membership and assignments commit together
Membership validation failsPerson transaction rolls back; next person continues
Identity conflict appears after planningPerson writes nothing and is reported
Notification values change in JodGigAll JodGig memberships update matching channels
One external pivot disappearsOnly that JodGig membership is revoked
Native membership existsIt remains unchanged
Native membership collides on same identity and companyReview case; no silent provenance change
Area outlet disappearsOld assignment is removed
Source user disabledAll JodGig memberships revoked; identity and native access remain
Same plan applied twiceSecond application reports no material changes
Two write jobs start togetherOnly one proceeds

Phase completion gate

TL;DR

Do not schedule the job yet. First prove dry run, manual apply and identical second-run behaviour in QA.

This phase is complete when the public service can apply the planner's output to QA, isolate a deliberately malformed person, and produce no unexpected changes on an immediate second run.