feat: enrich Workday mock data to REST API v6 shape (#6)

WORKDAY_WORKERS (9 workers, was 8):
- Add v6 fields to every worker: firstName, lastName, legalName,
  preferredName, primaryWorkPhone, effectiveDate, employeeID,
  primaryJob.manager ref, supervisoryOrganization, costCenter
- Add WORKDAY_WORKERS_BY_ID lookup index
- Add Taylor Brooks (WD-EMP-1009, Terminated) — new highest-severity drift

AD_USERS (9 users, was 7):
- Add Henry Park (EMP-1008, disabled/514) — new hire not yet provisioned
- Add Taylor Brooks (EMP-1009, enabled/512) — terminated but AD still active
- Seed Grace Lee title drift: AD 'Human Resources Director' vs Workday 'HR Director'
- Seed Frank Davis dept drift: AD 'Information Technology' vs Workday 'IT Operations'
- Normalize Emma/Grace AD dept to 'Human Resources' (remove unintentional mismatch)

WORKDAY_WORKERS (Emma Wilson):
- Set legalName='Emma Thompson' (name change) — triggers scan_name_variance

drift_detection.py:
- Add _build_workers_from_mock_data() — bridges WORKDAY_WORKERS + AD_USERS
  into the flat worker schema the scan functions consume
- MOCK_WORKERS_FROM_MOCK_DATA: built at import time; default for all scans
- Refactor all 4 scan functions with optional workers= param (default=None
  uses MOCK_WORKERS_FROM_MOCK_DATA; legacy MOCK_WORKERS constant preserved)

Scan results (USE_MOCK=true):
  scan_status_reconciliation  1 HIGH   (Taylor Brooks — terminated/enabled)
  scan_job_title_mismatches   2 MEDIUM (Bob, Grace)
  scan_department_drift       2 MEDIUM (Carol, Frank)
  scan_name_variance          1 LOW    (Emma — name change not synced to AD)

Refs: feat/enrich-workday-mock-data | Q2 live-data integration prep
This commit is contained in:
Nathan Castaldi 2026-04-16 18:55:39 -04:00 committed by GitHub
parent 63c1255420
commit af147a6bc5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 378 additions and 62 deletions

View File

@ -7,13 +7,94 @@ Workday (source of truth) and AD (target system) across multiple dimensions:
- Department drift
- Name variance (legal/preferred vs display name)
For production deployment, replace MOCK_WORKERS with live API calls to
workday_client.py and ad_adapter.py.
Each scan function accepts an optional `workers` parameter:
- When None (default): uses MOCK_WORKERS_FROM_MOCK_DATA built from mock_data.py,
which reflects the full enriched Workday + AD dataset.
- When provided: must be a dict mapping employee_id flat worker record
(same schema as MOCK_WORKERS below). This path is used by the live
integration once WorkdayClient + ADAdapter data is plumbed in.
The legacy MOCK_WORKERS constant is preserved for backwards compatibility
with existing tests that depend on its specific EMP001-EMP777 entries.
"""
import os
import sys
from typing import Any
# Mock dataset with reporting-line relationships for manager checks (WIS-017 prep)
# Make lib/ importable when run directly
_lib = os.path.dirname(os.path.abspath(__file__))
if _lib not in sys.path:
sys.path.insert(0, _lib)
def _build_workers_from_mock_data() -> dict[str, dict[str, Any]]:
"""Build a flat worker dict from mock_data.WORKDAY_WORKERS cross-referenced
against mock_data.AD_USERS. This is the default dataset for all scan
functions when no explicit workers argument is supplied.
The flat schema produced here matches the MOCK_WORKERS structure so that
scan functions only need one code path.
"""
try:
import mock_data as M
except ImportError:
return {}
# Build AD lookup by employeeID for fast cross-reference
ad_by_emp_id: dict[str, dict] = {
u["employeeID"]: u for u in M.AD_USERS if u.get("employeeID")
}
workers: dict[str, dict[str, Any]] = {}
for w in M.WORKDAY_WORKERS:
emp_id = w.get("employeeID", "")
if not emp_id:
continue
job = w.get("primaryJob") or {}
wd_title = (job.get("jobProfile") or {}).get("descriptor", "")
wd_dept = (job.get("businessUnit") or {}).get("descriptor", "")
cost_center = (w.get("costCenter") or {}).get("descriptor", "")
wd_status = (w.get("workerStatus") or {}).get("descriptor", "Active")
ad = ad_by_emp_id.get(emp_id, {})
ad_title = ad.get("title", "")
ad_dept = ad.get("department", "")
# AD enabled: userAccountControl "512" = enabled, "514" = disabled
uac = str(ad.get("userAccountControl", "512"))
try:
ad_enabled = (int(uac) & 2) == 0
except (ValueError, TypeError):
ad_enabled = True
workers[emp_id] = {
"name": w.get("preferredName") or w.get("descriptor", ""),
"legal_name": w.get("legalName", w.get("descriptor", "")),
"preferred_name": w.get("preferredName", ""),
"ad_display_name": ad.get("displayName", ""),
"status": wd_status,
"ad_enabled": ad_enabled,
"dept": wd_dept,
"workday_cost_center": cost_center,
"workday_title": wd_title,
"ad_title": ad_title,
"ad_department": ad_dept,
"email": w.get("primaryWorkEmail", ""),
"manager_id": (
(job.get("manager") or {}).get("id", "")
),
}
return workers
# Default dataset: built once at import time from mock_data.py
# Scan functions use this when no explicit workers argument is passed.
MOCK_WORKERS_FROM_MOCK_DATA: dict[str, dict[str, Any]] = _build_workers_from_mock_data()
# ── Legacy constant (kept for backwards compatibility with existing tests) ────
# MOCK_WORKERS uses a separate fictional dataset (EMP001-EMP777).
# New code should prefer MOCK_WORKERS_FROM_MOCK_DATA or pass live data directly.
MOCK_WORKERS: dict[str, dict[str, Any]] = {
"EMP001": {
"name": "Nathan",
@ -154,17 +235,25 @@ MOCK_WORKERS: dict[str, dict[str, Any]] = {
}
def scan_status_reconciliation_mismatches() -> dict[str, Any]:
def scan_status_reconciliation_mismatches(
workers: dict[str, dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Detect workers terminated in Workday but still enabled in AD.
Args:
workers: Optional flat worker dict (employee_id record). Defaults to
MOCK_WORKERS_FROM_MOCK_DATA (built from mock_data.py).
Pass live data here once WorkdayClient + ADAdapter are wired.
Returns:
dict with 'scan_summary' (total_records_checked, mismatches_found, status)
and 'mismatches' array of affected employees.
"""
dataset = workers if workers is not None else MOCK_WORKERS_FROM_MOCK_DATA
mismatches: list[dict[str, Any]] = []
total_scanned = 0
for employee_id, details in MOCK_WORKERS.items():
for employee_id, details in dataset.items():
total_scanned += 1
workday_status = details.get("status")
ad_enabled = bool(details.get("ad_enabled", False))
@ -191,16 +280,22 @@ def scan_status_reconciliation_mismatches() -> dict[str, Any]:
}
def scan_job_title_mismatches() -> dict[str, Any]:
def scan_job_title_mismatches(
workers: dict[str, dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Detect workers whose Workday title differs from their AD title.
Args:
workers: Optional flat worker dict. Defaults to MOCK_WORKERS_FROM_MOCK_DATA.
Returns:
dict with 'scan_summary' and 'mismatches' array.
"""
dataset = workers if workers is not None else MOCK_WORKERS_FROM_MOCK_DATA
mismatches: list[dict[str, Any]] = []
total_scanned = 0
for employee_id, details in MOCK_WORKERS.items():
for employee_id, details in dataset.items():
total_scanned += 1
workday_title = details.get("workday_title", "")
ad_title = details.get("ad_title", "")
@ -227,16 +322,22 @@ def scan_job_title_mismatches() -> dict[str, Any]:
}
def scan_department_drift() -> dict[str, Any]:
def scan_department_drift(
workers: dict[str, dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Detect workers whose Workday department context differs from AD department.
Args:
workers: Optional flat worker dict. Defaults to MOCK_WORKERS_FROM_MOCK_DATA.
Returns:
dict with 'scan_summary' and 'mismatches' array.
"""
dataset = workers if workers is not None else MOCK_WORKERS_FROM_MOCK_DATA
mismatches: list[dict[str, Any]] = []
total_scanned = 0
for employee_id, details in MOCK_WORKERS.items():
for employee_id, details in dataset.items():
total_scanned += 1
workday_department = details.get("dept", "")
workday_cost_center = details.get("workday_cost_center", "")
@ -270,16 +371,22 @@ def _normalize_name_tokens(value: str) -> list[str]:
return [token for token in value.lower().replace(".", " ").split() if token]
def scan_name_variance() -> dict[str, Any]:
def scan_name_variance(
workers: dict[str, dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Detect AD display names that do not align to legal or preferred Workday names.
Args:
workers: Optional flat worker dict. Defaults to MOCK_WORKERS_FROM_MOCK_DATA.
Returns:
dict with 'scan_summary' and 'mismatches' array.
"""
dataset = workers if workers is not None else MOCK_WORKERS_FROM_MOCK_DATA
mismatches: list[dict[str, Any]] = []
total_scanned = 0
for employee_id, details in MOCK_WORKERS.items():
for employee_id, details in dataset.items():
total_scanned += 1
legal_name = details.get("legal_name", "")
preferred_name = details.get("preferred_name", "")

View File

@ -3,21 +3,31 @@
Realistic enterprise mock data for all 6 shards.
Deliberate drift is seeded across systems to make audit tools return meaningful results.
Drift scenarios built in:
- Bob Martinez: jobTitle differs between AD ("Sr. Software Engineer") and Workday/Entra ("Software Engineer")
- Carol Chen: department differs Workday "Product Management" vs AD/Entra "Engineering"
Identity / Workday drift scenarios:
- Bob Martinez: jobTitle drift AD "Sr. Software Engineer" vs Workday/Entra "Software Engineer"
- Carol Chen: department drift AD "Engineering" vs Workday/Entra "Product Management"
- Grace Lee: title drift AD "Human Resources Director" vs Workday "HR Director"
- Frank Davis: department drift AD "Information Technology" vs Workday "IT Operations"
- Taylor Brooks: terminated in Workday but AD account still ENABLED (HIGH severity)
- Henry Park: Active in Workday (new hire) but AD account DISABLED / not yet provisioned
- David Kim: AD account disabled but Entra account still enabled (cloud/on-prem split)
- Emma Wilson: AD stale account no login in 120 days
Asset drift scenarios:
- LAPTOP-CAROL-03: serialNumber differs Lansweeper "SN-CAROL-03A" vs Intune "SN-CAROL-03B"
- David Kim: AD account disabled but Entra account still enabled
- Emma Wilson: AD stale account (no login in 120 days)
- SERVER-PROD-01: exists in Lansweeper + Helix CMDB but not in Intune (unmanaged server)
- SERVER-PROD-01: exists in Lansweeper + Helix CMDB but not in Intune (unmanaged server)
Workday worker objects follow the REST API v6 staffing/workers response shape:
https://community.workday.com/sites/default/files/file-hosting/restapi/
"""
import datetime
_NOW = datetime.datetime.utcnow()
_NOW = datetime.datetime.now(datetime.timezone.utc)
_FMT = "%Y-%m-%dT%H:%M:%SZ"
_D = lambda days: (_NOW - datetime.timedelta(days=days)).strftime(_FMT)
_DATE = lambda days: (_NOW - datetime.timedelta(days=days)).strftime("%Y-%m-%d")
_TODAY = _NOW.strftime("%Y-%m-%d")
# ─── Active Directory ─────────────────────────────────────────────────────────
@ -99,7 +109,7 @@ AD_USERS: list[dict] = [
"sAMAccountName": "emmaw",
"displayName": "Emma Wilson",
"mail": "emma.wilson@nexus.corp",
"department": "HR",
"department": "Human Resources",
"title": "HR Specialist",
"manager": "CN=Grace Lee,OU=Users,DC=corp,DC=nexus,DC=local",
"memberOf": "CN=HR,OU=Groups,DC=corp,DC=nexus,DC=local",
@ -117,7 +127,8 @@ AD_USERS: list[dict] = [
"sAMAccountName": "frankd",
"displayName": "Frank Davis",
"mail": "frank.davis@nexus.corp",
"department": "IT Operations",
# ⚡ DRIFT: AD department is "Information Technology" but Workday has "IT Operations"
"department": "Information Technology",
"title": "IT Director",
"manager": None,
"memberOf": "CN=IT Ops,OU=Groups,DC=corp,DC=nexus,DC=local",
@ -134,8 +145,9 @@ AD_USERS: list[dict] = [
"sAMAccountName": "gracel",
"displayName": "Grace Lee",
"mail": "grace.lee@nexus.corp",
"department": "HR",
"title": "HR Director",
"department": "Human Resources",
# ⚡ DRIFT: AD title is "Human Resources Director" but Workday has "HR Director"
"title": "Human Resources Director",
"manager": None,
"memberOf": "CN=HR,OU=Groups,DC=corp,DC=nexus,DC=local",
"employeeID": "EMP-1007",
@ -146,6 +158,42 @@ AD_USERS: list[dict] = [
"physicalDeliveryOfficeName": "HQ-Floor2",
"cn": "Grace Lee",
},
{
# ⚡ NEW HIRE NOT PROVISIONED: Active in Workday but AD account is disabled
"dn": "CN=Henry Park,OU=Users,DC=corp,DC=nexus,DC=local",
"sAMAccountName": "henryp",
"displayName": "Henry Park",
"mail": "henry.park@nexus.corp",
"department": "Engineering",
"title": "Junior Software Engineer",
"manager": "CN=Alice Johnson,OU=Users,DC=corp,DC=nexus,DC=local",
"memberOf": "CN=Engineering,OU=Groups,DC=corp,DC=nexus,DC=local",
"employeeID": "EMP-1008",
"userAccountControl": "514",
"lastLogonTimestamp": None,
"whenCreated": _D(5),
"telephoneNumber": "+1-555-0108",
"physicalDeliveryOfficeName": "HQ-Floor3",
"cn": "Henry Park",
},
{
# ⚡ TERMINATED BUT ENABLED: Terminated in Workday; AD account still active
"dn": "CN=Taylor Brooks,OU=Users,DC=corp,DC=nexus,DC=local",
"sAMAccountName": "taylorb",
"displayName": "Taylor Brooks",
"mail": "taylor.brooks@nexus.corp",
"department": "Sales",
"title": "Account Executive",
"manager": "CN=Frank Davis,OU=Users,DC=corp,DC=nexus,DC=local",
"memberOf": "CN=VPN Users,OU=Groups,DC=corp,DC=nexus,DC=local",
"employeeID": "EMP-1009",
"userAccountControl": "512",
"lastLogonTimestamp": _D(14),
"whenCreated": "2022-03-15T08:00:00Z",
"telephoneNumber": "+1-555-0109",
"physicalDeliveryOfficeName": "HQ-Floor2",
"cn": "Taylor Brooks",
},
]
AD_USERS_BY_SAM = {u["sAMAccountName"]: u for u in AD_USERS}
@ -267,6 +315,19 @@ ENTRA_USERS: list[dict] = [
"onPremisesSyncEnabled": False,
"assignedLicenses": [],
},
{
# ⚡ TERMINATED BUT STILL ENABLED in Entra (cloud side of the same terminated scenario)
"id": "aaa11111-0000-0000-0000-000000000009",
"displayName": "Taylor Brooks",
"userPrincipalName": "taylor.brooks@nexus.corp",
"mail": "taylor.brooks@nexus.corp",
"jobTitle": "Account Executive",
"department": "Sales",
"accountEnabled": True,
"createdDateTime": "2022-03-15T08:00:00Z",
"onPremisesSyncEnabled": True,
"assignedLicenses": [{"skuId": "6fd2c87f-b296-42f0-b197-1e91e994b900"}],
},
]
ENTRA_USERS_BY_MAIL = {u["mail"]: u for u in ENTRA_USERS if u.get("mail")}
@ -347,106 +408,252 @@ ENTRA_SERVICE_PRINCIPALS: list[dict] = [
WORKDAY_WORKERS: list[dict] = [
{
"id": "WD-EMP-1001", "descriptor": "Alice Johnson",
# Workday REST API v6 staffing/workers response shape
"id": "WD-EMP-1001",
"descriptor": "Alice Johnson",
"employeeID": "EMP-1001",
"firstName": "Alice",
"lastName": "Johnson",
"legalName": "Alice Mary Johnson",
"preferredName": "Alice",
"primaryWorkEmail": "alice.johnson@nexus.corp",
"primaryWorkPhone": "+1-555-0101",
"hireDate": "2019-04-01",
"effectiveDate": _TODAY,
"primaryJob": {
"jobProfile": {"descriptor": "Engineering Manager"},
"businessUnit": {"descriptor": "Engineering"},
"location": {"descriptor": "New York HQ"},
"jobProfile": {"descriptor": "Engineering Manager", "id": "PROFILE-101"},
"businessUnit": {"descriptor": "Engineering", "id": "ORG-101"},
"location": {"descriptor": "New York HQ", "id": "LOC-001"},
"manager": {
"descriptor": "Frank Davis",
"primaryWorkEmail": "frank.davis@nexus.corp",
"id": "WD-EMP-1006",
},
},
"supervisoryOrganization": {"descriptor": "Engineering", "id": "ORG-101"},
"costCenter": {"descriptor": "CC110-ENG", "id": "CC-101"},
"workerType": {"descriptor": "Employee"},
"workerStatus": {"descriptor": "Active"},
},
{
"id": "WD-EMP-1002", "descriptor": "Bob Martinez",
"id": "WD-EMP-1002",
"descriptor": "Bob Martinez",
"employeeID": "EMP-1002",
"firstName": "Bob",
"lastName": "Martinez",
"legalName": "Robert Martinez",
"preferredName": "Bob",
"primaryWorkEmail": "bob.martinez@nexus.corp",
"primaryWorkPhone": "+1-555-0102",
"hireDate": "2021-06-15",
"effectiveDate": _TODAY,
"primaryJob": {
# ⚡ DRIFT: Workday says "Software Engineer" (AD says "Sr. Software Engineer")
"jobProfile": {"descriptor": "Software Engineer"},
"businessUnit": {"descriptor": "Engineering"},
"location": {"descriptor": "New York HQ"},
# ⚡ DRIFT: Workday says "Software Engineer" — AD says "Sr. Software Engineer"
"jobProfile": {"descriptor": "Software Engineer", "id": "PROFILE-102"},
"businessUnit": {"descriptor": "Engineering", "id": "ORG-101"},
"location": {"descriptor": "New York HQ", "id": "LOC-001"},
"manager": {
"descriptor": "Alice Johnson",
"primaryWorkEmail": "alice.johnson@nexus.corp",
"id": "WD-EMP-1001",
},
},
"supervisoryOrganization": {"descriptor": "Engineering", "id": "ORG-101"},
"costCenter": {"descriptor": "CC110-ENG", "id": "CC-101"},
"workerType": {"descriptor": "Employee"},
"workerStatus": {"descriptor": "Active"},
},
{
"id": "WD-EMP-1003", "descriptor": "Carol Chen",
"id": "WD-EMP-1003",
"descriptor": "Carol Chen",
"employeeID": "EMP-1003",
"firstName": "Carol",
"lastName": "Chen",
"legalName": "Carol Chen",
"preferredName": "Carol",
"primaryWorkEmail": "carol.chen@nexus.corp",
"primaryWorkPhone": "+1-555-0103",
"hireDate": "2020-09-10",
"effectiveDate": _TODAY,
"primaryJob": {
"jobProfile": {"descriptor": "Product Manager"},
# ⚡ DRIFT: Workday department is "Product Management" (AD has "Engineering")
"businessUnit": {"descriptor": "Product Management"},
"location": {"descriptor": "New York HQ"},
"jobProfile": {"descriptor": "Product Manager", "id": "PROFILE-103"},
# ⚡ DRIFT: Workday dept "Product Management" — AD has "Engineering"
"businessUnit": {"descriptor": "Product Management", "id": "ORG-102"},
"location": {"descriptor": "New York HQ", "id": "LOC-001"},
"manager": {
"descriptor": "Alice Johnson",
"primaryWorkEmail": "alice.johnson@nexus.corp",
"id": "WD-EMP-1001",
},
},
"supervisoryOrganization": {"descriptor": "Product Management", "id": "ORG-102"},
"costCenter": {"descriptor": "CC120-PROD", "id": "CC-102"},
"workerType": {"descriptor": "Employee"},
"workerStatus": {"descriptor": "Active"},
},
{
"id": "WD-EMP-1004", "descriptor": "David Kim",
"id": "WD-EMP-1004",
"descriptor": "David Kim",
"employeeID": "EMP-1004",
"firstName": "David",
"lastName": "Kim",
"legalName": "David Kim",
"preferredName": "David",
"primaryWorkEmail": "david.kim@nexus.corp",
"primaryWorkPhone": "+1-555-0104",
"hireDate": "2018-02-20",
"effectiveDate": _TODAY,
"primaryJob": {
"jobProfile": {"descriptor": "Systems Administrator"},
"businessUnit": {"descriptor": "IT Operations"},
"location": {"descriptor": "New York HQ"},
"jobProfile": {"descriptor": "Systems Administrator", "id": "PROFILE-104"},
"businessUnit": {"descriptor": "IT Operations", "id": "ORG-103"},
"location": {"descriptor": "New York HQ", "id": "LOC-001"},
"manager": {
"descriptor": "Frank Davis",
"primaryWorkEmail": "frank.davis@nexus.corp",
"id": "WD-EMP-1006",
},
},
"supervisoryOrganization": {"descriptor": "IT Operations", "id": "ORG-103"},
"costCenter": {"descriptor": "CC130-ITOPS", "id": "CC-103"},
"workerType": {"descriptor": "Employee"},
"workerStatus": {"descriptor": "Leave of Absence"},
},
{
"id": "WD-EMP-1005", "descriptor": "Emma Wilson",
"id": "WD-EMP-1005",
"descriptor": "Emma Wilson",
"employeeID": "EMP-1005",
"firstName": "Emma",
"lastName": "Thompson",
# ⚡ NAME VARIANCE: recently married — legal name updated in Workday but AD still shows maiden name "Wilson"
"legalName": "Emma Thompson",
"preferredName": "Emma",
"primaryWorkEmail": "emma.wilson@nexus.corp",
"primaryWorkPhone": "+1-555-0105",
"hireDate": "2017-11-05",
"effectiveDate": _TODAY,
"primaryJob": {
"jobProfile": {"descriptor": "HR Specialist"},
"businessUnit": {"descriptor": "Human Resources"},
"location": {"descriptor": "New York HQ"},
"jobProfile": {"descriptor": "HR Specialist", "id": "PROFILE-105"},
"businessUnit": {"descriptor": "Human Resources", "id": "ORG-104"},
"location": {"descriptor": "New York HQ", "id": "LOC-001"},
"manager": {
"descriptor": "Grace Lee",
"primaryWorkEmail": "grace.lee@nexus.corp",
"id": "WD-EMP-1007",
},
},
"supervisoryOrganization": {"descriptor": "Human Resources", "id": "ORG-104"},
"costCenter": {"descriptor": "CC140-HR", "id": "CC-104"},
"workerType": {"descriptor": "Employee"},
"workerStatus": {"descriptor": "Active"},
},
{
"id": "WD-EMP-1006", "descriptor": "Frank Davis",
"id": "WD-EMP-1006",
"descriptor": "Frank Davis",
"employeeID": "EMP-1006",
"firstName": "Frank",
"lastName": "Davis",
"legalName": "Franklin Davis",
"preferredName": "Frank",
"primaryWorkEmail": "frank.davis@nexus.corp",
"primaryWorkPhone": "+1-555-0106",
"hireDate": "2016-03-01",
"effectiveDate": _TODAY,
"primaryJob": {
"jobProfile": {"descriptor": "IT Director"},
"businessUnit": {"descriptor": "IT Operations"},
"location": {"descriptor": "New York HQ"},
"jobProfile": {"descriptor": "IT Director", "id": "PROFILE-106"},
# ⚡ DRIFT: Workday dept "IT Operations" — AD has "Information Technology"
"businessUnit": {"descriptor": "IT Operations", "id": "ORG-103"},
"location": {"descriptor": "New York HQ", "id": "LOC-001"},
"manager": None,
},
"supervisoryOrganization": {"descriptor": "IT Operations", "id": "ORG-103"},
"costCenter": {"descriptor": "CC130-ITOPS", "id": "CC-103"},
"workerType": {"descriptor": "Employee"},
"workerStatus": {"descriptor": "Active"},
},
{
"id": "WD-EMP-1007", "descriptor": "Grace Lee",
"id": "WD-EMP-1007",
"descriptor": "Grace Lee",
"employeeID": "EMP-1007",
"firstName": "Grace",
"lastName": "Lee",
"legalName": "Grace Lee",
"preferredName": "Grace",
"primaryWorkEmail": "grace.lee@nexus.corp",
"primaryWorkPhone": "+1-555-0107",
"hireDate": "2016-07-15",
"effectiveDate": _TODAY,
"primaryJob": {
"jobProfile": {"descriptor": "HR Director"},
"businessUnit": {"descriptor": "Human Resources"},
"location": {"descriptor": "New York HQ"},
# ⚡ DRIFT: Workday title "HR Director" — AD has "Human Resources Director"
"jobProfile": {"descriptor": "HR Director", "id": "PROFILE-107"},
"businessUnit": {"descriptor": "Human Resources", "id": "ORG-104"},
"location": {"descriptor": "New York HQ", "id": "LOC-001"},
"manager": None,
},
"supervisoryOrganization": {"descriptor": "Human Resources", "id": "ORG-104"},
"costCenter": {"descriptor": "CC140-HR", "id": "CC-104"},
"workerType": {"descriptor": "Employee"},
"workerStatus": {"descriptor": "Active"},
},
{
"id": "WD-EMP-1008", "descriptor": "Henry Park",
# ⚡ NEW HIRE: Active in Workday; AD account created but still DISABLED (not provisioned)
"id": "WD-EMP-1008",
"descriptor": "Henry Park",
"employeeID": "EMP-1008",
"firstName": "Henry",
"lastName": "Park",
"legalName": "Henry Park",
"preferredName": "Henry",
"primaryWorkEmail": "henry.park@nexus.corp",
"hireDate": _DATE(5), # New hire this week
"primaryWorkPhone": "+1-555-0108",
"hireDate": _DATE(5),
"effectiveDate": _TODAY,
"primaryJob": {
"jobProfile": {"descriptor": "Junior Software Engineer"},
"businessUnit": {"descriptor": "Engineering"},
"location": {"descriptor": "New York HQ"},
"jobProfile": {"descriptor": "Junior Software Engineer", "id": "PROFILE-108"},
"businessUnit": {"descriptor": "Engineering", "id": "ORG-101"},
"location": {"descriptor": "New York HQ", "id": "LOC-001"},
"manager": {
"descriptor": "Alice Johnson",
"primaryWorkEmail": "alice.johnson@nexus.corp",
"id": "WD-EMP-1001",
},
},
"supervisoryOrganization": {"descriptor": "Engineering", "id": "ORG-101"},
"costCenter": {"descriptor": "CC110-ENG", "id": "CC-101"},
"workerType": {"descriptor": "Employee"},
"workerStatus": {"descriptor": "Active"},
},
{
# ⚡ TERMINATED BUT AD STILL ENABLED — highest severity drift scenario
"id": "WD-EMP-1009",
"descriptor": "Taylor Brooks",
"employeeID": "EMP-1009",
"firstName": "Taylor",
"lastName": "Brooks",
"legalName": "Taylor Brooks",
"preferredName": "Taylor",
"primaryWorkEmail": "taylor.brooks@nexus.corp",
"primaryWorkPhone": "+1-555-0109",
"hireDate": "2022-03-15",
"effectiveDate": _TODAY,
"primaryJob": {
"jobProfile": {"descriptor": "Account Executive", "id": "PROFILE-109"},
"businessUnit": {"descriptor": "Sales", "id": "ORG-105"},
"location": {"descriptor": "New York HQ", "id": "LOC-001"},
"manager": {
"descriptor": "Frank Davis",
"primaryWorkEmail": "frank.davis@nexus.corp",
"id": "WD-EMP-1006",
},
},
"supervisoryOrganization": {"descriptor": "Sales", "id": "ORG-105"},
"costCenter": {"descriptor": "CC150-SALES", "id": "CC-105"},
"workerType": {"descriptor": "Employee"},
"workerStatus": {"descriptor": "Terminated"},
},
]
WORKDAY_WORKERS_BY_EMAIL = {w["primaryWorkEmail"]: w for w in WORKDAY_WORKERS}
WORKDAY_WORKERS_BY_ID = {w["id"]: w for w in WORKDAY_WORKERS}
WORKDAY_POSITIONS: list[dict] = [
{"id": "POS-2001", "descriptor": "Senior Product Manager", "status": "Open", "businessUnit": "Product Management", "location": "New York HQ"},
@ -456,11 +663,12 @@ WORKDAY_POSITIONS: list[dict] = [
]
WORKDAY_ORGANIZATIONS: list[dict] = [
{"id": "ORG-100", "descriptor": "Nexus Corp", "type": "Company", "headcount": 8},
{"id": "ORG-100", "descriptor": "Nexus Corp", "type": "Company", "headcount": 9},
{"id": "ORG-101", "descriptor": "Engineering", "type": "Supervisory", "headcount": 3, "parent": "ORG-100"},
{"id": "ORG-102", "descriptor": "Product Management", "type": "Supervisory", "headcount": 1, "parent": "ORG-100"},
{"id": "ORG-103", "descriptor": "IT Operations", "type": "Supervisory", "headcount": 2, "parent": "ORG-100"},
{"id": "ORG-104", "descriptor": "Human Resources", "type": "Supervisory", "headcount": 2, "parent": "ORG-100"},
{"id": "ORG-105", "descriptor": "Sales", "type": "Supervisory", "headcount": 1, "parent": "ORG-100"},
]
WORKDAY_COMPENSATION: dict[str, dict] = {
@ -472,6 +680,7 @@ WORKDAY_COMPENSATION: dict[str, dict] = {
"WD-EMP-1006": {"grade": "G8", "salaryRange": {"min": 155000, "max": 200000, "currency": "USD"}, "payFrequency": "Annual"},
"WD-EMP-1007": {"grade": "G7", "salaryRange": {"min": 130000, "max": 170000, "currency": "USD"}, "payFrequency": "Annual"},
"WD-EMP-1008": {"grade": "G2", "salaryRange": {"min": 75000, "max": 95000, "currency": "USD"}, "payFrequency": "Annual"},
"WD-EMP-1009": {"grade": "G4", "salaryRange": {"min": 80000, "max": 105000, "currency": "USD"}, "payFrequency": "Annual"},
}