[
  {
    "type": "update",
    "description": "Backfill scheduleTimePunches.shiftId for historical punches left unassociated (shiftId NULL) before the 120-min shift-association grace fix in TimePunchController. Mirrors the live findScheduledShiftForUser logic: same employee, non-deleted shift, punchTime within 120 minutes of the shift interval (UTC vs UTC), nearest shift wins. Clears the mobile Team app 'missing' flag on shifts that were actually worked. Idempotent: only touches shiftId IS NULL punches; open shifts (employeeId NULL) never match.",
    "database": "{{store}}",
    "sql": "UPDATE scheduleTimePunches tp SET tp.shiftId = ( SELECT s.shiftId FROM scheduleShifts s WHERE s.employeeId = tp.employeeId AND s.deleted_at IS NULL AND s.shiftEnd IS NOT NULL AND (CASE WHEN tp.punchTime < s.shiftStart THEN TIMESTAMPDIFF(MINUTE, tp.punchTime, s.shiftStart) WHEN tp.punchTime > s.shiftEnd THEN TIMESTAMPDIFF(MINUTE, s.shiftEnd, tp.punchTime) ELSE 0 END) <= 120 ORDER BY (CASE WHEN tp.punchTime < s.shiftStart THEN TIMESTAMPDIFF(MINUTE, tp.punchTime, s.shiftStart) WHEN tp.punchTime > s.shiftEnd THEN TIMESTAMPDIFF(MINUTE, s.shiftEnd, tp.punchTime) ELSE 0 END) ASC, s.shiftStart ASC LIMIT 1 ) WHERE tp.shiftId IS NULL AND tp.deleted_at IS NULL AND EXISTS ( SELECT 1 FROM scheduleShifts s2 WHERE s2.employeeId = tp.employeeId AND s2.deleted_at IS NULL AND s2.shiftEnd IS NOT NULL AND (CASE WHEN tp.punchTime < s2.shiftStart THEN TIMESTAMPDIFF(MINUTE, tp.punchTime, s2.shiftStart) WHEN tp.punchTime > s2.shiftEnd THEN TIMESTAMPDIFF(MINUTE, s2.shiftEnd, tp.punchTime) ELSE 0 END) <= 120 )"
  }
]
