class_name StaffService extends RefCounted signal staff_registered(staff: StaffRuntime) signal staff_removed(staff_id: StringName, reason: StringName) signal shift_registered(shift: ShiftInstance) signal shift_state_changed(staff_id: StringName, shift_id: StringName, state: int) signal task_registered(task: StaffTask) signal task_assigned(task_id: StringName, staff_id: StringName) signal task_reopened(task_id: StringName, reason: StringName) signal task_completed(task_id: StringName, staff_id: StringName) const StaffRuntimeType = preload("res://scripts/staff/staff_runtime.gd") const ShiftInstanceType = preload("res://scripts/staff/shift_instance.gd") const StaffTaskType = preload("res://scripts/staff/staff_task.gd") const TaskDispatcherType = preload("res://scripts/staff/task_dispatcher.gd") const CommandResultType = preload("res://scripts/core/command_result.gd") const LEGACY_SNAPSHOT_VERSION := 1 const PAYROLL_SNAPSHOT_VERSION := 2 const EMPLOYMENT_SNAPSHOT_VERSION := 3 const SNAPSHOT_VERSION := 4 const HIGH_PRIORITY_THRESHOLD := 90 const DEFAULT_LATE_REASON := &"PERSONAL" const MAX_PAYROLL_DAY := 1_000_000 const MAX_PAYROLL_SETTLEMENTS := 10_000 const MAX_PAYROLL_ROWS := 2_000 const MAX_PAYROLL_AMOUNT := 9_000_000_000_000 const MINUTES_PER_DAY := 1_440 var current_minute := 0 var last_error := "" var last_dispatch_report: Dictionary = {} var last_recovery_repair_report: Dictionary = {} var _staff: Dictionary = {} var _shifts: Dictionary = {} var _tasks: Dictionary = {} var _schedule_overrides: Dictionary = {} var _work_assignments: Dictionary = {} var _departed_payroll: Dictionary = {} var _payroll_settlements: Dictionary = {} var _payroll_days: Dictionary = {} var _next_shift_sequence := 1 var _next_task_sequence := 1 var _last_advanced_minute := -1 var _dispatcher := TaskDispatcherType.new() var _item_reserver := Callable() var _item_releaser := Callable() var _travel_estimator := Callable() var _external_task_driver := Callable() var _attendance_driver := Callable() var _arrival_driver := Callable() # Optional scheduler policy: zero delegates work progression to Runtime. var _work_progress_driver: Callable func configure_work_progress_driver(driver: Callable) -> void: _work_progress_driver = driver func configure_arrival_driver(driver: Callable) -> void: _arrival_driver = driver func configure_callbacks( item_reserver: Callable = Callable(), item_releaser: Callable = Callable(), travel_estimator: Callable = Callable() ) -> StaffService: _item_reserver = item_reserver _item_releaser = item_releaser _travel_estimator = travel_estimator _dispatcher.configure_callbacks(item_reserver, item_releaser, travel_estimator) return self ## External operations own their work-completion transaction while retaining ## ordinary Staff movement, shifts, and bidirectional Task ownership. func configure_external_task_driver(driver: Callable = Callable()) -> void: _external_task_driver = driver ## Runtime attendance uses the employee's real position. Component consumers ## and legacy one-off shifts retain their existing scheduling contract. func configure_attendance_driver(driver: Callable = Callable()) -> void: _attendance_driver = driver func requires_attendance(staff_id: StringName) -> bool: return _attendance_driver.is_valid() and _work_assignments.has(staff_id) \ and not String(_work_assignments[staff_id][&"facility_id"]).is_empty() func is_externally_driven_task(task_id: StringName) -> bool: return _external_task_driver.is_valid() and bool(_external_task_driver.call(task_id)) func assign_task_to_staff(task_id: StringName, staff_id: StringName, absolute_minute: int) -> CommandResult: var task := get_task(task_id) var staff := get_staff(staff_id) if task == null or staff == null or not task.is_dispatchable(): return _conflict("Task or employee is unavailable for assignment.") if not task.required_items.is_empty() or not task.reservation_ids.is_empty(): return _conflict("Explicit assignment requires externally reserved operation inputs.") if not staff.is_available_for_dispatch() or not staff.meets_task_requirements(task): return _conflict("Employee is busy or does not meet the Task requirements.") if not task.reserve(staff_id, [], absolute_minute): return _conflict("Task could not reserve its employee.") if not staff.reserve_task(task_id, absolute_minute): task.reopen(&"ASSIGNMENT_ROLLED_BACK", absolute_minute, true) return _conflict("Employee could not reserve its Task.") task_assigned.emit(task_id, staff_id) return _success({&"task_id": task_id, &"staff_id": staff_id}) func retarget_external_task(task_id: StringName, target_cell: Vector3i, absolute_minute: int) -> CommandResult: var task := get_task(task_id) if task == null or not is_externally_driven_task(task_id) or task.is_terminal(): return _conflict("Only an active externally driven Task can be retargeted.") var staff := get_staff(task.assigned_staff_id) if staff == null or staff.current_task_id != task_id: return _conflict("Task and employee ownership differ.") task.target_cell = target_cell task.state = StaffTaskType.State.MOVING task.progress_minutes = 0 task.state_changed_at = absolute_minute staff.transition_to(StaffRuntimeType.State.MOVING_TO_TASK, &"TASK_RETARGETED", absolute_minute) return _success({&"task_id": task_id}) func register_staff(value: Variant) -> CommandResult: if value == null or not value is StaffRuntime: return _invalid("Staff runtime is missing or has an invalid type.") var staff: StaffRuntime = value if not staff.is_valid(): return _invalid("Staff runtime is invalid.", {&"staff_id": staff.staff_id}) if _staff.has(staff.staff_id): return _duplicate("Staff", staff.staff_id) if _departed_payroll.has(staff.staff_id): return _conflict("Staff ID has an unsettled departed payroll obligation.") if not staff.current_task_id.is_empty() or not staff.shift_id.is_empty(): return _invalid("New Staff must register before Task or Shift assignment.") _staff[staff.staff_id] = staff staff_registered.emit(staff) last_error = "" return _success({&"staff_id": staff.staff_id}) func create_staff( staff_id: StringName, role_id: StringName, options: Dictionary = {} ) -> CommandResult: var staff := StaffRuntimeType.new().configure(staff_id, role_id, options) return register_staff(staff) func has_staff(staff_id: StringName) -> bool: return _staff.has(staff_id) func get_staff(staff_id: StringName) -> StaffRuntime: return _staff.get(staff_id) as StaffRuntime func get_staff_members() -> Array[StaffRuntime]: var result: Array[StaffRuntime] = [] for staff_id: StringName in _sorted_keys(_staff): result.append(_staff[staff_id]) return result ## Stable input for the Operations daily-close adapter. Departed wage debt is ## intentionally not mixed into this daily expense: it remains in ## departed_payroll until a separate settlement transaction clears it. func get_daily_payroll_rows() -> Array[Dictionary]: var rows: Array[Dictionary] = [] for staff_id: StringName in _sorted_keys(_staff): var staff: StaffRuntime = _staff[staff_id] rows.append({ &"staff_id": staff.staff_id, &"employment_kind": staff.get_employment_kind_name(), &"daily_wage": staff.get_effective_daily_wage(), }) return rows func get_daily_payroll_total() -> int: var total := 0 for row: Dictionary in get_daily_payroll_rows(): total += int(row[&"daily_wage"]) return total ## Returns the currently carried wage liabilities without accruing a new day. ## Every returned row has the same shape and the whole result is ordered by ## Staff ID so Operations never needs access to the private payroll ledgers. func get_payroll_obligations() -> Array[Dictionary]: var by_staff_id: Dictionary = {} for staff_id: StringName in _sorted_keys(_staff): var staff: StaffRuntime = _staff[staff_id] by_staff_id[staff_id] = { &"staff_id": staff_id, &"payroll_status": &"ACTIVE", &"employment_kind": staff.get_employment_kind_name(), &"daily_wage": staff.get_effective_daily_wage(), &"unpaid_wages": staff.unpaid_wages, } for staff_id: StringName in _sorted_keys(_departed_payroll): if int(_departed_payroll[staff_id]) <= 0: continue by_staff_id[staff_id] = { &"staff_id": staff_id, &"payroll_status": &"DEPARTED", &"employment_kind": &"", &"daily_wage": 0, &"unpaid_wages": int(_departed_payroll[staff_id]), } var rows: Array[Dictionary] = [] for staff_id: StringName in _sorted_keys(by_staff_id): rows.append((by_staff_id[staff_id] as Dictionary).duplicate(true)) return rows func get_departed_staff_ids() -> Array[StringName]: var result: Array[StringName] = [] for staff_id: StringName in _sorted_keys(_departed_payroll): if int(_departed_payroll[staff_id]) > 0: result.append(staff_id) return result func get_payroll_settlement(settlement_id: StringName) -> Dictionary: var record: Variant = _payroll_settlements.get(settlement_id, {}) return record.duplicate(true) if record is Dictionary else {} ## Pure preview for one daily close. Active Staff accrue exactly one daily wage; ## departed Staff contribute only debt already preserved at departure. func preview_daily_payroll(day: Variant) -> Dictionary: if typeof(day) != TYPE_INT or int(day) <= 0 or int(day) > MAX_PAYROLL_DAY: return _payroll_preview_error(&"INVALID_DAY", "Payroll day must be a positive integer.") var payroll_day := int(day) if _payroll_days.has(payroll_day): return _payroll_preview_error( &"DAY_ALREADY_SETTLED", "Payroll day already has a settlement.", {&"day": payroll_day, &"settlement_id": _payroll_days[payroll_day]} ) var built := _build_payroll_preview(payroll_day) if not bool(built.get(&"ok", false)): return built return built.duplicate(true) ## Atomically accrues the day's wages and applies the caller's allocation. A ## settlement ID is an idempotency key: exact input replays, conflicting input ## or another settlement for the same day is rejected without mutation. func apply_daily_payroll_settlement( settlement_id: Variant, day: Variant, payments_by_staff: Variant ) -> CommandResult: if not _stable_settlement_id_is_valid(settlement_id): return _invalid("Payroll settlement ID is invalid.") if typeof(day) != TYPE_INT or int(day) <= 0 or int(day) > MAX_PAYROLL_DAY: return _invalid("Payroll day must be a positive integer.") if not payments_by_staff is Dictionary: return _invalid("Payroll payments must be a Dictionary.") var stable_id := StringName(settlement_id) var payroll_day := int(day) var parsed_payments := _parse_payroll_payments(payments_by_staff) if not bool(parsed_payments.get(&"ok", false)): return _invalid(String(parsed_payments.get(&"message", "Payroll payments are invalid."))) var input_payments: Array[Dictionary] = parsed_payments[&"payments"] var fingerprint := _payroll_fingerprint(payroll_day, input_payments) if _payroll_settlements.has(stable_id): var existing: Dictionary = _payroll_settlements[stable_id] if int(existing[&"day"]) != payroll_day or String(existing[&"fingerprint"]) != fingerprint: return _conflict("Payroll settlement ID conflicts with an earlier input.") var replay_details := existing.duplicate(true) replay_details[&"replayed"] = true return _success(replay_details) if _payroll_days.has(payroll_day): return _conflict("Payroll day already has a different settlement.") if _payroll_settlements.size() >= MAX_PAYROLL_SETTLEMENTS: return _conflict("Payroll settlement history budget is exhausted.") var preview := _build_payroll_preview(payroll_day) if not bool(preview.get(&"ok", false)): return _invalid(String(preview.get(&"message", "Payroll preview is invalid."))) var due_by_staff: Dictionary = {} for row: Dictionary in preview[&"rows"]: due_by_staff[row[&"staff_id"]] = int(row[&"total_due"]) for payment: Dictionary in input_payments: var staff_id: StringName = payment[&"staff_id"] var amount := int(payment[&"amount"]) if not due_by_staff.has(staff_id): return _invalid("Payroll payment references an unknown Staff ID.", {&"staff_id": staff_id}) if amount > int(due_by_staff[staff_id]): return _invalid("Payroll payment exceeds the Staff obligation.", {&"staff_id": staff_id}) var payment_index: Dictionary = {} for payment: Dictionary in input_payments: payment_index[payment[&"staff_id"]] = int(payment[&"amount"]) var settlement_rows: Array[Dictionary] = [] var paid_total := 0 var unpaid_after_total := 0 for preview_row: Dictionary in preview[&"rows"]: var staff_id: StringName = preview_row[&"staff_id"] var payment := int(payment_index.get(staff_id, 0)) var unpaid_after := int(preview_row[&"total_due"]) - payment var row := preview_row.duplicate(true) row[&"payment"] = payment row[&"unpaid_after"] = unpaid_after settlement_rows.append(row) paid_total += payment unpaid_after_total += unpaid_after # All validation and derived calculations finish before the first live write. for row: Dictionary in settlement_rows: var staff_id: StringName = row[&"staff_id"] var unpaid_after := int(row[&"unpaid_after"]) if row[&"payroll_status"] == &"ACTIVE": (_staff[staff_id] as StaffRuntime).unpaid_wages = unpaid_after elif unpaid_after > 0: _departed_payroll[staff_id] = unpaid_after else: _departed_payroll.erase(staff_id) var record := { &"settlement_id": stable_id, &"day": payroll_day, &"fingerprint": fingerprint, &"input_payments": input_payments.duplicate(true), &"rows": settlement_rows, &"new_wages_total": int(preview[&"new_wages_total"]), &"prior_arrears_total": int(preview[&"prior_arrears_total"]), &"total_due": int(preview[&"total_due"]), &"paid_total": paid_total, &"unpaid_after_total": unpaid_after_total, } _payroll_settlements[stable_id] = record _payroll_days[payroll_day] = stable_id var details := record.duplicate(true) details[&"replayed"] = false return _success(details) func register_shift(value: Variant) -> CommandResult: if value == null or not value is ShiftInstance: return _invalid("Shift instance is missing or has an invalid type.") var shift: ShiftInstance = value if not shift.is_valid(): return _invalid("Shift instance is invalid.", {&"shift_id": shift.shift_instance_id}) if _shifts.has(shift.shift_instance_id): return _duplicate("Shift", shift.shift_instance_id) var staff := get_staff(shift.staff_id) if staff == null: return _not_found("Staff", shift.staff_id) if not staff.can_take_shift(): return _conflict("Staff cannot take a Shift in the current state.") _apply_override_to_shift(shift) for existing_shift_id: StringName in _sorted_keys(_shifts): var existing: ShiftInstance = _shifts[existing_shift_id] if existing.staff_id != shift.staff_id: continue if _shifts_overlap(existing, shift): return _conflict("Staff already has an overlapping Shift.") _shifts[shift.shift_instance_id] = shift shift_registered.emit(shift) # Future Shift instances are schedule records, not the Staff's active pointer. # Reconciliation selects the correct one when its commute window begins. This # lets non-overlapping future Shifts be registered in any order. _update_shift_states(current_minute) last_error = "" return _success({&"shift_id": shift.shift_instance_id}) func create_standard_shift( staff_id: StringName, template_id: StringName, start_day: int, assigned_facility_id: StringName = &"", shift_id: StringName = &"" ) -> CommandResult: var effective_id := shift_id if effective_id.is_empty(): effective_id = StringName( "shift_%08d_d%05d" % [_next_shift_sequence, maxi(start_day, 0)] ) var shift := ShiftInstanceType.create_standard( effective_id, staff_id, template_id, start_day, assigned_facility_id ) if shift == null: return _invalid("Only DAY and NIGHT are standard Shift templates.") var result := register_shift(shift) if result.is_success() and shift_id.is_empty(): _next_shift_sequence += 1 return result func create_flex_shift( staff_id: StringName, start_day: int, start_minute_of_day: int, end_minute_of_day: int, assigned_facility_id: StringName = &"", shift_id: StringName = &"" ) -> CommandResult: var effective_id := shift_id if effective_id.is_empty(): effective_id = StringName( "shift_%08d_d%05d" % [_next_shift_sequence, maxi(start_day, 0)] ) var shift := ShiftInstanceType.new().configure( effective_id, staff_id, &"FLEX", start_day, start_minute_of_day, end_minute_of_day, assigned_facility_id ) var result := register_shift(shift) if result.is_success() and shift_id.is_empty(): _next_shift_sequence += 1 return result func get_shift(shift_id: StringName) -> ShiftInstance: return _shifts.get(shift_id) as ShiftInstance func get_shifts() -> Array[ShiftInstance]: var result: Array[ShiftInstance] = [] for shift_id: StringName in _sorted_keys(_shifts): result.append(_shifts[shift_id]) return result ## A work assignment is a repeating plan; concrete Shift records retain the ## facility and elapsed interval used by historical wage allocation. func get_work_assignments() -> Array[Dictionary]: var rows: Array[Dictionary] = [] for staff_id: StringName in _sorted_keys(_work_assignments): rows.append((_work_assignments[staff_id] as Dictionary).duplicate(true)) return rows func get_work_assignment(staff_id: StringName) -> Dictionary: return (_work_assignments.get(staff_id, {}) as Dictionary).duplicate(true) func set_work_assignment(staff_id: StringName, facility_id: StringName, template_id: StringName, absolute_minute := -1) -> CommandResult: var minute := current_minute if absolute_minute < 0 else absolute_minute if minute < current_minute: return _invalid("Assignment time cannot move backwards.", {&"reason": &"TIME_REGRESSION"}) var staff := get_staff(staff_id) if staff == null: return _invalid("Staff was not found.", {&"reason": &"STAFF_NOT_FOUND"}) if template_id not in [&"DAY", &"NIGHT"]: return _invalid("Choose DAY or NIGHT.", {&"reason": &"INVALID_TEMPLATE"}) if not staff.current_task_id.is_empty(): return _invalid("Finish the current task before changing assignment.", {&"reason": &"STAFF_BUSY"}) if not staff.can_take_shift() or staff.state not in [ StaffRuntimeType.State.OFF_DUTY, StaffRuntimeType.State.COMMUTING, StaffRuntimeType.State.AVAILABLE, ]: return _invalid("Staff cannot change assignment in the current state.", {&"reason": &"STAFF_UNAVAILABLE"}) var previous := get_work_assignment(staff_id) if not previous.is_empty() and StringName(previous[&"facility_id"]) == facility_id \ and StringName(previous[&"template_id"]) == template_id: return _success(_work_assignment_result(staff_id, &"ASSIGNMENT_UNCHANGED")) # Build every replacement before the first live write. Never repurpose the # facility of elapsed work, including a NIGHT segment crossing midnight. var replacements: Dictionary = {} for shift: ShiftInstance in _shifts_for_staff(staff_id): if shift.cancelled or shift.end_at <= minute: continue var replacement := ShiftInstanceType.from_snapshot(shift.create_snapshot()) # Attendance can contain gaps, so clamping by scheduled duration alone # cannot remove an already recorded paused-edit boundary minute. The # current AVAILABLE employee recorded that minute; its replacement plan # records it only if the employee is physically at the new workplace. if requires_attendance(staff_id) and minute == current_minute \ and staff.state == StaffRuntimeType.State.AVAILABLE \ and staff.shift_id == shift.shift_instance_id \ and shift.contains_minute(minute) and shift.worked_minutes > 0: replacement.worked_minutes -= 1 if replacement.worked_minutes == 0 and replacement.overtime_minutes == 0: replacement.actual_started_at = -1 if replacement.start_at >= minute: replacement.cancelled = true else: replacement.end_at = minute replacement.end_minute_of_day = posmod(minute, MINUTES_PER_DAY) replacement.worked_minutes = mini(replacement.worked_minutes, minute - replacement.start_at) replacement.close(minute) if not replacement.is_valid(): return _invalid("Existing shift could not be split.", {&"reason": &"SHIFT_CONFLICT"}) replacements[replacement.shift_instance_id] = replacement var interval := _work_interval(template_id, minute) var start_at := maxi(interval.x, minute) var new_shift: ShiftInstance = null if not facility_id.is_empty(): new_shift = _make_work_shift(staff_id, facility_id, template_id, start_at, interval.y) if _shifts.has(new_shift.shift_instance_id): return _invalid("Generated shift ID conflicts.", {&"reason": &"SHIFT_CONFLICT"}) for shift_id: StringName in _sorted_keys(replacements): _shifts[shift_id] = replacements[shift_id] # An explicit repeating plan supersedes future one-day overrides. Keeping # them would silently move an employee back to an old facility later. for override_id: StringName in _sorted_keys(_schedule_overrides): var entry: Dictionary = _schedule_overrides[override_id] if StringName(entry[&"staff_id"]) == staff_id \ and int(entry[&"start_day"]) >= minute / MINUTES_PER_DAY: _schedule_overrides.erase(override_id) _work_assignments[staff_id] = { &"staff_id": String(staff_id), &"facility_id": String(facility_id), &"template_id": String(template_id), &"changed_at": minute, &"effective_from": start_at if new_shift != null else minute, &"next_start_at": interval.x + MINUTES_PER_DAY, } staff.shift_id = &"" staff.assigned_facility_id = &"" staff.is_overtime = false if String(staff.absence_reason).begins_with("COMMUTE_"): staff.absence_reason = &"" staff.transition_to(StaffRuntimeType.State.OFF_DUTY, &"WORK_ASSIGNMENT_CHANGED", minute) if new_shift != null: _shifts[new_shift.shift_instance_id] = new_shift _next_shift_sequence += 1 # APPLY_COMMANDS precedes the Staff AI phase in a running tick. The AI # records that new minute later; paused edits record the boundary here. _update_shift_states(minute, staff_id, minute == current_minute) if new_shift != null: shift_registered.emit(new_shift) return _success(_work_assignment_result(staff_id, &"ASSIGNMENT_UPDATED")) func _work_assignment_result(staff_id: StringName, reason: StringName) -> Dictionary: var row := get_work_assignment(staff_id) row[&"reason"] = reason row[&"shift_id"] = get_staff(staff_id).shift_id row[&"unassigned"] = String(row[&"facility_id"]).is_empty() return row static func _work_interval(template_id: StringName, absolute_minute: int) -> Vector2i: var day := absolute_minute / MINUTES_PER_DAY var minute_of_day := posmod(absolute_minute, MINUTES_PER_DAY) if template_id == &"DAY": if minute_of_day >= ShiftInstanceType.DAY_END: day += 1 return Vector2i(day * MINUTES_PER_DAY + ShiftInstanceType.DAY_START, day * MINUTES_PER_DAY + ShiftInstanceType.DAY_END) if minute_of_day < ShiftInstanceType.NIGHT_END: day -= 1 return Vector2i(day * MINUTES_PER_DAY + ShiftInstanceType.NIGHT_START, (day + 1) * MINUTES_PER_DAY + ShiftInstanceType.NIGHT_END) func _make_work_shift(staff_id: StringName, facility_id: StringName, template_id: StringName, start_at: int, end_at: int) -> ShiftInstance: return ShiftInstanceType.new().configure( StringName("shift_%08d_d%05d" % [_next_shift_sequence, start_at / MINUTES_PER_DAY]), staff_id, template_id, start_at / MINUTES_PER_DAY, posmod(start_at, MINUTES_PER_DAY), posmod(end_at, MINUTES_PER_DAY), facility_id ) func _ensure_work_assignment_shifts(absolute_minute: int) -> void: for staff_id: StringName in _sorted_keys(_work_assignments): var row: Dictionary = _work_assignments[staff_id] if String(row[&"facility_id"]).is_empty() \ or absolute_minute < int(row[&"next_start_at"]) - ShiftInstanceType.COMMUTE_LEAD_MINUTES: continue var interval := _work_interval(StringName(row[&"template_id"]), absolute_minute) # During a running shift the next occurrence still lies in the future. if interval.x < int(row[&"next_start_at"]): continue var shift := _make_work_shift(staff_id, StringName(row[&"facility_id"]), StringName(row[&"template_id"]), maxi(interval.x, 0), interval.y) _apply_override_to_shift(shift) var overlaps := false for existing: ShiftInstance in _shifts_for_staff(staff_id): if _shifts_overlap(existing, shift): overlaps = true break row[&"next_start_at"] = interval.x + MINUTES_PER_DAY # Explicit one-day shifts win over the recurring template on that day. if overlaps or _shifts.has(shift.shift_instance_id) or not shift.is_valid(): continue _shifts[shift.shift_instance_id] = shift _next_shift_sequence += 1 shift_registered.emit(shift) static func _work_assignment_shape_is_valid(row: Dictionary) -> bool: if row.size() != 6: return false for key: StringName in [&"staff_id", &"facility_id", &"template_id"]: if typeof(row.get(key, null)) not in [TYPE_STRING, TYPE_STRING_NAME]: return false for key: StringName in [&"changed_at", &"effective_from", &"next_start_at"]: if typeof(row.get(key, null)) != TYPE_INT or int(row[key]) < 0: return false if String(row[&"staff_id"]).is_empty() or StringName(row[&"template_id"]) not in [&"DAY", &"NIGHT"]: return false var standard_start := ShiftInstanceType.DAY_START if StringName(row[&"template_id"]) == &"DAY" else ShiftInstanceType.NIGHT_START return int(row[&"effective_from"]) >= int(row[&"changed_at"]) \ and int(row[&"effective_from"]) < int(row[&"next_start_at"]) \ and posmod(int(row[&"next_start_at"]), MINUTES_PER_DAY) == standard_start func set_schedule_override( staff_id: StringName, start_day: int, patch: Dictionary ) -> CommandResult: if not _staff.has(staff_id): return _not_found("Staff", staff_id) if start_day < 0 or patch.is_empty(): return _invalid("ScheduleOverride requires a day and a non-empty patch.") var key := _override_key(staff_id, start_day) _schedule_overrides[key] = { &"staff_id": staff_id, &"start_day": start_day, &"patch": patch.duplicate(true), } last_error = "" return _success({&"staff_id": staff_id, &"start_day": start_day}) func get_schedule_override(staff_id: StringName, start_day: int) -> Dictionary: var value: Variant = _schedule_overrides.get(_override_key(staff_id, start_day), {}) return value.duplicate(true) if value is Dictionary else {} func register_task(value: Variant) -> CommandResult: if value == null or not value is StaffTask: return _invalid("Task is missing or has an invalid type.") var task: StaffTask = value if not task.is_valid() or not task.is_dispatchable(): return _invalid("A new Task must be a valid OPEN Task.") if _tasks.has(task.task_id): return _duplicate("Task", task.task_id) _tasks[task.task_id] = task task_registered.emit(task) last_error = "" return _success({&"task_id": task.task_id}) func create_task( task_type: StringName, priority: int, created_at: int, options: Dictionary = {}, task_id: StringName = &"" ) -> CommandResult: var effective_id := task_id if effective_id.is_empty(): effective_id = StringName("task_%08d" % _next_task_sequence) var task := StaffTaskType.new().configure( effective_id, task_type, priority, created_at, options ) var result := register_task(task) if result.is_success() and task_id.is_empty(): _next_task_sequence += 1 return result func get_task(task_id: StringName) -> StaffTask: return _tasks.get(task_id) as StaffTask func get_tasks() -> Array[StaffTask]: var result: Array[StaffTask] = [] for task_id: StringName in _sorted_keys(_tasks): result.append(_tasks[task_id]) return result func dispatch_now(absolute_minute: int = -1) -> Dictionary: var minute := current_minute if absolute_minute < 0 else absolute_minute last_dispatch_report = _dispatcher.dispatch(get_tasks(), _staff, minute) for assignment: Dictionary in last_dispatch_report.get(&"assigned", []): task_assigned.emit(assignment[&"task_id"], assignment[&"staff_id"]) for interruption: Dictionary in last_dispatch_report.get(&"interruptions", []): task_reopened.emit(interruption[&"interrupted_task_id"], &"INTERRUPTED") last_error = "" return last_dispatch_report.duplicate(true) func advance_one_minute( absolute_minute: int = -1, force_dispatch := false ) -> Dictionary: if absolute_minute < 0: absolute_minute = current_minute + 1 if absolute_minute < current_minute: return {&"ok": false, &"reason": &"TIME_REGRESSION"} if absolute_minute <= _last_advanced_minute: return {&"ok": false, &"reason": &"MINUTE_ALREADY_APPLIED"} current_minute = absolute_minute var repairs := audit_orphan_tasks(current_minute) _ensure_work_assignment_shifts(current_minute) _update_shift_states(current_minute) var task_actions := _advance_assigned_tasks(current_minute) var high_priority := _has_high_priority_open_task() var dispatched := false if force_dispatch or _dispatcher.should_dispatch(current_minute, high_priority): dispatch_now(current_minute) dispatched = true _last_advanced_minute = current_minute return { &"ok": true, &"minute": current_minute, &"repairs": repairs, &"task_actions": task_actions, &"dispatched": dispatched, &"dispatch_report": last_dispatch_report.duplicate(true) if dispatched else {}, } func begin_task(task_id: StringName, absolute_minute: int = -1) -> CommandResult: var task := get_task(task_id) if task == null: return _not_found("Task", task_id) var staff := get_staff(task.assigned_staff_id) if staff == null or staff.current_task_id != task_id: return _conflict("Task and Staff reservation do not match.") var minute := current_minute if absolute_minute < 0 else absolute_minute if task.state not in [StaffTaskType.State.RESERVED, StaffTaskType.State.MOVING]: return _conflict("Task cannot begin from its current state.") if staff.state not in [StaffRuntimeType.State.MOVING_TO_TASK, StaffRuntimeType.State.AVAILABLE]: return _conflict("Staff cannot begin the reserved Task from its current state.") if _arrival_driver.is_valid() and not bool(_arrival_driver.call(task, staff, minute)): return _conflict("Task arrival was redirected or is no longer valid.") # Both state preconditions are checked before either side is mutated. var task_state_before: StaffTask.State = task.state var task_changed_at_before := task.state_changed_at if not task.begin_working(minute): return _conflict("Task cannot begin from its current state.") if not staff.begin_task(minute): # Defensive rollback; this branch should be unreachable after the preflight. task.state = task_state_before task.state_changed_at = task_changed_at_before return _conflict("Task cannot begin from its current state.") last_error = "" return _success({&"task_id": task_id, &"staff_id": staff.staff_id}) func progress_task( task_id: StringName, minutes: int = 1, absolute_minute: int = -1, externally_driven := false ) -> CommandResult: if is_externally_driven_task(task_id) and not externally_driven: return _conflict("Task completion belongs to its external operation.") var task := get_task(task_id) if task == null: return _not_found("Task", task_id) var staff := get_staff(task.assigned_staff_id) if staff == null or staff.current_task_id != task_id: return _conflict("Task and Staff reservation do not match.") var minute := current_minute if absolute_minute < 0 else absolute_minute if task.state != StaffTaskType.State.WORKING: return _conflict("Only a WORKING Task can progress.") if minutes <= 0: return _invalid("Task progress minutes must be positive.") if minute <= task.state_changed_at: return _conflict("Task progress was already applied at this minute.") var progress_before := task.progress_minutes var changed_at_before := task.state_changed_at var completed := task.advance_work(minutes, minute) if completed: var completed_by := staff.staff_id var ids := task.reservation_ids.duplicate() if not _release_reservations(ids, task, minute): task.progress_minutes = progress_before task.state_changed_at = changed_at_before return _conflict("Completed Task inventory Reservations could not be released.") if not task.complete(minute): task.progress_minutes = progress_before task.state_changed_at = changed_at_before return _conflict("Task reached its duration but failed to complete.") staff.release_task(minute, &"TASK_COMPLETED") task_completed.emit(task_id, completed_by) return _success({ &"task_id": task_id, &"completed": completed, &"progress_minutes": task.progress_minutes, }) ## Advances only work that existed before this minute. Dispatch runs after this ## pass, so a newly reserved Task cannot begin and consume work in the same tick. ## Runtime path travel can move MOVING Tasks independently; once their actor is ## at target, this service begins them on the next deterministic minute. func _advance_assigned_tasks(absolute_minute: int) -> Array[Dictionary]: var actions: Array[Dictionary] = [] for task_id: StringName in _sorted_keys(_tasks): var task: StaffTask = _tasks[task_id] if task.assigned_staff_id.is_empty() or task.state_changed_at >= absolute_minute: continue var staff := get_staff(task.assigned_staff_id) if staff == null or staff.current_task_id != task.task_id: continue match task.state: StaffTaskType.State.RESERVED: if staff.current_cell == task.target_cell: var begin_result := begin_task(task.task_id, absolute_minute) if begin_result.is_success(): actions.append({ &"task_id": task.task_id, &"staff_id": staff.staff_id, &"action": &"BEGAN_WORK", }) elif task.begin_moving(absolute_minute): actions.append({ &"task_id": task.task_id, &"staff_id": staff.staff_id, &"action": &"BEGAN_MOVING", }) StaffTaskType.State.MOVING: if staff.current_cell == task.target_cell: var begin_result := begin_task(task.task_id, absolute_minute) if begin_result.is_success(): actions.append({ &"task_id": task.task_id, &"staff_id": staff.staff_id, &"action": &"BEGAN_WORK", }) StaffTaskType.State.WORKING: if is_externally_driven_task(task_id): continue var units := 1 if _work_progress_driver.is_valid(): units = int(_work_progress_driver.call(task.task_id)) if units <= 0: continue var progress_result := progress_task(task.task_id, units, absolute_minute) if progress_result.is_success(): actions.append({ &"task_id": task.task_id, &"staff_id": staff.staff_id, &"action": ( &"COMPLETED" if bool(progress_result.details.get(&"completed", false)) else &"PROGRESSED" ), &"progress_minutes": int(progress_result.details.get(&"progress_minutes", 0)), }) return actions func cancel_task( task_id: StringName, reason: StringName = &"CANCELLED", absolute_minute: int = -1, externally_driven := false ) -> CommandResult: if is_externally_driven_task(task_id) and not externally_driven: return _conflict("External operation must release its cargo before cancellation.") var task := get_task(task_id) if task == null: return _not_found("Task", task_id) if task.is_terminal(): return _conflict("Terminal Task cannot be cancelled again.") var minute := current_minute if absolute_minute < 0 else absolute_minute var ids := task.reservation_ids.duplicate() if not _release_reservations(ids, task, minute): return _conflict("Task inventory Reservation could not be released.") var staff := get_staff(task.assigned_staff_id) if staff != null and staff.current_task_id == task_id: staff.release_task(minute, reason) task.cancel(reason, minute) last_error = "" return _success({&"task_id": task_id}) func remove_staff( staff_id: StringName, reason: StringName = &"REMOVED", preserve_unpaid_wages := true, absolute_minute: int = -1 ) -> CommandResult: var staff := get_staff(staff_id) if staff == null: return _not_found("Staff", staff_id) if staff.employment_kind == StaffRuntimeType.EmploymentKind.UNIQUE: return _conflict("Unique Staff cannot be dismissed, sold or permanently removed.") if is_externally_driven_task(staff.current_task_id): return _conflict("Finish the employee external operation before removal.") var minute := current_minute if absolute_minute < 0 else absolute_minute if not staff.current_task_id.is_empty(): var task := get_task(staff.current_task_id) if task != null: var ids := task.reservation_ids.duplicate() if not _release_reservations(ids, task, minute): return _conflict("Staff removal could not release Task Reservations.") task.reopen(&"STAFF_REMOVED", minute, task.preserve_progress_on_interrupt) task_reopened.emit(task.task_id, &"STAFF_REMOVED") if preserve_unpaid_wages and staff.unpaid_wages > 0: _departed_payroll[staff_id] = int(_departed_payroll.get(staff_id, 0)) + staff.unpaid_wages var removed_shift_ids: Array[StringName] = [] for shift_id: StringName in _sorted_keys(_shifts): var shift: ShiftInstance = _shifts[shift_id] if shift.staff_id != staff_id: continue removed_shift_ids.append(shift_id) _shifts.erase(shift_id) var removed_override_days: Array[int] = [] for override_id: StringName in _sorted_keys(_schedule_overrides): var entry: Dictionary = _schedule_overrides[override_id] if StringName(entry.get(&"staff_id", &"")) != staff_id: continue removed_override_days.append(int(entry.get(&"start_day", -1))) _schedule_overrides.erase(override_id) _work_assignments.erase(staff_id) _staff.erase(staff_id) staff_removed.emit(staff_id, reason) last_error = "" return _success({ &"staff_id": staff_id, &"unpaid_wages_preserved": int(_departed_payroll.get(staff_id, 0)), &"removed_shift_ids": removed_shift_ids, &"removed_override_days": removed_override_days, }) func retire_staff(staff_id: StringName, absolute_minute: int = -1) -> CommandResult: var staff := get_staff(staff_id) if staff == null: return _not_found("Staff", staff_id) if staff.employment_kind == StaffRuntimeType.EmploymentKind.UNIQUE: return _conflict("Unique Staff cannot be dismissed, sold or permanently removed.") if is_externally_driven_task(staff.current_task_id): return _conflict("Finish the employee external operation before retirement.") staff.is_retiring = true return remove_staff(staff_id, &"RETIRED", true, absolute_minute) func get_departed_unpaid_wages(staff_id: StringName) -> int: return int(_departed_payroll.get(staff_id, 0)) func audit_orphan_tasks(absolute_minute: int = -1) -> Dictionary: var minute := current_minute if absolute_minute < 0 else absolute_minute var reopened: Array[StringName] = [] var released_staff: Array[StringName] = [] var release_failed: Array[StringName] = [] for task_id: StringName in _sorted_keys(_tasks): var task: StaffTask = _tasks[task_id] if task.assigned_staff_id.is_empty(): continue var staff := get_staff(task.assigned_staff_id) if staff != null and staff.current_task_id == task.task_id: continue var ids := task.reservation_ids.duplicate() if not _release_reservations(ids, task, minute): release_failed.append(task.task_id) continue task.reopen(&"ORPHAN_STAFF", minute, task.preserve_progress_on_interrupt) reopened.append(task.task_id) task_reopened.emit(task.task_id, &"ORPHAN_STAFF") for staff_id: StringName in _sorted_keys(_staff): var staff: StaffRuntime = _staff[staff_id] if staff.current_task_id.is_empty(): continue var task := get_task(staff.current_task_id) if task == null or task.assigned_staff_id != staff.staff_id: if staff.release_task(minute, &"ORPHAN_TASK"): released_staff.append(staff.staff_id) return { &"reopened_task_ids": reopened, &"released_staff_ids": released_staff, &"release_failed_task_ids": release_failed, } ## Recovery-only repair used on an isolated candidate service. All orphan Task ## Reservation IDs are handed to the external releaser in one call, allowing ## the Inventory boundary to roll the entire release set back atomically. Staff ## and Task objects are mutated only after that one transaction succeeds. func _audit_orphan_tasks_atomically(absolute_minute: int) -> Dictionary: var orphan_tasks: Array[StaffTask] = [] var orphan_staff: Array[StaffRuntime] = [] var all_reservation_ids: Array[StringName] = [] for task_id: StringName in _sorted_keys(_tasks): var task: StaffTask = _tasks[task_id] if task.assigned_staff_id.is_empty(): continue var assigned_staff := get_staff(task.assigned_staff_id) if assigned_staff != null and assigned_staff.current_task_id == task.task_id: continue orphan_tasks.append(task) for reservation_id: StringName in task.reservation_ids: if not all_reservation_ids.has(reservation_id): all_reservation_ids.append(reservation_id) for staff_id: StringName in _sorted_keys(_staff): var staff: StaffRuntime = _staff[staff_id] if staff.current_task_id.is_empty(): continue var task := get_task(staff.current_task_id) if task == null or task.assigned_staff_id != staff.staff_id: orphan_staff.append(staff) all_reservation_ids.sort_custom( func(left: StringName, right: StringName) -> bool: return String(left) < String(right) ) if not all_reservation_ids.is_empty(): if orphan_tasks.is_empty() or not _release_reservations( all_reservation_ids, orphan_tasks[0], absolute_minute ): var failed_ids: Array[StringName] = [] for task: StaffTask in orphan_tasks: if not task.reservation_ids.is_empty(): failed_ids.append(task.task_id) return { &"reopened_task_ids": [], &"released_staff_ids": [], &"release_failed_task_ids": failed_ids, &"released_reservation_ids": [], } var reopened: Array[StringName] = [] for task: StaffTask in orphan_tasks: if task.reopen(&"ORPHAN_STAFF", absolute_minute, task.preserve_progress_on_interrupt): reopened.append(task.task_id) var released_staff: Array[StringName] = [] for staff: StaffRuntime in orphan_staff: if staff.release_task(absolute_minute, &"ORPHAN_TASK"): released_staff.append(staff.staff_id) return { &"reopened_task_ids": reopened, &"released_staff_ids": released_staff, &"release_failed_task_ids": [], &"released_reservation_ids": all_reservation_ids, } func validate_invariants() -> Dictionary: var core := _validate_candidate_invariants(_staff, _tasks, _shifts, _departed_payroll) var payroll := _validate_payroll_history(_payroll_settlements) var errors: Array[String] = [] for error: String in core[&"errors"]: errors.append(error) for error: String in payroll[&"errors"]: errors.append(error) if _payroll_days != payroll.get(&"day_index", {}): errors.append("Payroll day index does not match settlement history.") return {&"ok": errors.is_empty(), &"errors": errors} func create_snapshot() -> Dictionary: var staff_snapshots: Array[Dictionary] = [] for staff_id: StringName in _sorted_keys(_staff): staff_snapshots.append((_staff[staff_id] as StaffRuntime).create_snapshot()) var shift_snapshots: Array[Dictionary] = [] for shift_id: StringName in _sorted_keys(_shifts): shift_snapshots.append((_shifts[shift_id] as ShiftInstance).create_snapshot()) var task_snapshots: Array[Dictionary] = [] for task_id: StringName in _sorted_keys(_tasks): task_snapshots.append((_tasks[task_id] as StaffTask).create_snapshot()) var override_snapshots: Array[Dictionary] = [] for override_id: StringName in _sorted_keys(_schedule_overrides): var entry: Dictionary = _schedule_overrides[override_id] override_snapshots.append({ &"staff_id": String(entry[&"staff_id"]), &"start_day": int(entry[&"start_day"]), &"patch": (entry[&"patch"] as Dictionary).duplicate(true), }) var departed_payroll: Array[Dictionary] = [] for staff_id: StringName in _sorted_keys(_departed_payroll): departed_payroll.append({ &"staff_id": String(staff_id), &"unpaid_wages": int(_departed_payroll[staff_id]), }) var payroll_settlements: Array[Dictionary] = [] var settlement_records: Array[Dictionary] = [] for settlement_id: StringName in _sorted_keys(_payroll_settlements): settlement_records.append(_payroll_settlements[settlement_id]) settlement_records.sort_custom(func(left: Dictionary, right: Dictionary) -> bool: if int(left[&"day"]) != int(right[&"day"]): return int(left[&"day"]) < int(right[&"day"]) return String(left[&"settlement_id"]) < String(right[&"settlement_id"]) ) for record: Dictionary in settlement_records: payroll_settlements.append(_serialize_payroll_settlement(record)) return { &"version": SNAPSHOT_VERSION, &"current_minute": current_minute, &"last_advanced_minute": _last_advanced_minute, &"next_shift_sequence": _next_shift_sequence, &"next_task_sequence": _next_task_sequence, &"staff": staff_snapshots, &"shifts": shift_snapshots, &"tasks": task_snapshots, &"schedule_overrides": override_snapshots, &"work_assignments": get_work_assignments(), &"departed_payroll": departed_payroll, &"payroll_settlements": payroll_settlements, &"dispatcher": _dispatcher.create_snapshot(), } func restore_snapshot(snapshot: Dictionary) -> bool: return _restore_snapshot_internal(snapshot, false) func restore_snapshot_with_orphan_recovery(snapshot: Dictionary) -> bool: # Parse and repair on an isolated service. No live Staff object, Dispatcher, # or signal is touched until the candidate and its external Reservation release # transaction have both completed and passed final invariants. var candidate := StaffService.new() candidate.configure_callbacks(_item_reserver, _item_releaser, _travel_estimator) if not candidate._restore_snapshot_internal(snapshot, true): last_error = candidate.last_error last_recovery_repair_report = candidate.last_recovery_repair_report.duplicate(true) return false _staff = candidate._staff _shifts = candidate._shifts _tasks = candidate._tasks _schedule_overrides = candidate._schedule_overrides _work_assignments = candidate._work_assignments _departed_payroll = candidate._departed_payroll _payroll_settlements = candidate._payroll_settlements _payroll_days = candidate._payroll_days current_minute = candidate.current_minute _last_advanced_minute = candidate._last_advanced_minute _next_shift_sequence = candidate._next_shift_sequence _next_task_sequence = candidate._next_task_sequence _dispatcher = candidate._dispatcher _dispatcher.configure_callbacks(_item_reserver, _item_releaser, _travel_estimator) last_dispatch_report = {} last_recovery_repair_report = candidate.last_recovery_repair_report.duplicate(true) last_error = "" for task_id: StringName in last_recovery_repair_report.get(&"reopened_task_ids", []): task_reopened.emit(task_id, &"ORPHAN_STAFF") return true func _restore_snapshot_internal(snapshot: Dictionary, recover_orphan_tasks: bool) -> bool: last_error = "" last_recovery_repair_report = { &"automatic_repairs_applied": false, &"reopened_task_ids": [], &"released_staff_ids": [], &"release_failed_task_ids": [], } var raw_version: Variant = snapshot.get(&"version", null) if ( typeof(raw_version) != TYPE_INT or int(raw_version) not in [ LEGACY_SNAPSHOT_VERSION, PAYROLL_SNAPSHOT_VERSION, EMPLOYMENT_SNAPSHOT_VERSION, SNAPSHOT_VERSION, ] ): last_error = "Unsupported StaffService snapshot version." return false var snapshot_version := int(raw_version) for key: StringName in [&"staff", &"shifts", &"tasks", &"schedule_overrides", &"departed_payroll"]: if not snapshot.get(key, null) is Array: last_error = "StaffService snapshot collection is malformed: %s" % key return false if ( snapshot_version >= PAYROLL_SNAPSHOT_VERSION and not snapshot.get(&"payroll_settlements", null) is Array ): last_error = "StaffService payroll settlement history is malformed." return false if ( snapshot_version == LEGACY_SNAPSHOT_VERSION and ( snapshot.has(&"payroll_settlements") or snapshot.has("payroll_settlements") or snapshot.has(&"last_advanced_minute") or snapshot.has("last_advanced_minute") ) ): last_error = "Legacy StaffService snapshot contains fields from a later schema." return false if not snapshot.get(&"dispatcher", null) is Dictionary: last_error = "StaffService dispatcher snapshot is malformed." return false var next_staff: Dictionary = {} for raw_staff: Variant in snapshot[&"staff"]: if not raw_staff is Dictionary: last_error = "Staff snapshot entry must be a Dictionary." return false var has_row_version: bool = ( raw_staff.has(&"snapshot_version") or raw_staff.has("snapshot_version") ) var has_effective_minute: bool = ( raw_staff.has(&"employment_effective_minute") or raw_staff.has("employment_effective_minute") ) if snapshot_version >= EMPLOYMENT_SNAPSHOT_VERSION: if not has_row_version or not has_effective_minute: last_error = "Current Staff snapshot entry is missing employment-time schema fields." return false elif has_row_version or has_effective_minute: last_error = "Legacy Staff snapshot entry contains employment-time schema fields." return false var staff := StaffRuntimeType.from_snapshot(raw_staff) if staff == null or next_staff.has(staff.staff_id): last_error = "Staff snapshot entry is invalid or duplicated." return false next_staff[staff.staff_id] = staff var next_shifts: Dictionary = {} for raw_shift: Variant in snapshot[&"shifts"]: if not raw_shift is Dictionary: last_error = "Shift snapshot entry must be a Dictionary." return false var shift := ShiftInstanceType.from_snapshot(raw_shift) if shift == null or next_shifts.has(shift.shift_instance_id): last_error = "Shift snapshot entry is invalid or duplicated." return false next_shifts[shift.shift_instance_id] = shift var next_tasks: Dictionary = {} for raw_task: Variant in snapshot[&"tasks"]: if not raw_task is Dictionary: last_error = "Task snapshot entry must be a Dictionary." return false var task := StaffTaskType.from_snapshot(raw_task) if task == null or next_tasks.has(task.task_id): last_error = "Task snapshot entry is invalid or duplicated." return false next_tasks[task.task_id] = task var next_overrides: Dictionary = {} for raw_override: Variant in snapshot[&"schedule_overrides"]: if not raw_override is Dictionary or not raw_override.get(&"patch", null) is Dictionary: last_error = "ScheduleOverride snapshot entry is malformed." return false var staff_id := StringName(raw_override.get(&"staff_id", "")) var start_day := int(raw_override.get(&"start_day", -1)) if staff_id.is_empty() or start_day < 0 or not next_staff.has(staff_id): last_error = "ScheduleOverride references invalid Staff or day." return false var key := _override_key(staff_id, start_day) if next_overrides.has(key): last_error = "ScheduleOverride is duplicated." return false next_overrides[key] = { &"staff_id": staff_id, &"start_day": start_day, &"patch": (raw_override[&"patch"] as Dictionary).duplicate(true), } var next_assignments: Dictionary = {} if snapshot_version >= SNAPSHOT_VERSION: if not snapshot.get(&"work_assignments", null) is Array: last_error = "Staff work assignments must be an Array." return false for raw_assignment: Variant in snapshot[&"work_assignments"]: if not raw_assignment is Dictionary or not _work_assignment_shape_is_valid(raw_assignment): last_error = "Staff work assignment is malformed." return false var owner := StringName(raw_assignment[&"staff_id"]) if not next_staff.has(owner) or next_assignments.has(owner) \ or int(raw_assignment[&"changed_at"]) > int(snapshot.get(&"current_minute", -1)): last_error = "Staff work assignment owner or time is invalid." return false var changed_at := int(raw_assignment[&"changed_at"]) var assigned_facility := StringName(raw_assignment[&"facility_id"]) var template := StringName(raw_assignment[&"template_id"]) var expected_effective := changed_at if assigned_facility.is_empty() else maxi(changed_at, _work_interval(template, changed_at).x) var next_start := int(raw_assignment[&"next_start_at"]) var current_interval := _work_interval(template, int(snapshot.get(&"current_minute", 0))) if int(raw_assignment[&"effective_from"]) != expected_effective \ or (not assigned_facility.is_empty() and ( next_start <= int(snapshot.get(&"current_minute", 0)) or next_start not in [current_interval.x, current_interval.x + MINUTES_PER_DAY] )): last_error = "Staff work assignment cadence is invalid." return false next_assignments[owner] = raw_assignment.duplicate(true) elif snapshot.has(&"work_assignments"): last_error = "Legacy Staff snapshot contains work assignments from a later schema." return false var next_payroll: Dictionary = {} for raw_payroll: Variant in snapshot[&"departed_payroll"]: if not raw_payroll is Dictionary: last_error = "Departed payroll snapshot entry is malformed." return false var raw_staff_id: Variant = raw_payroll.get(&"staff_id", null) if typeof(raw_staff_id) not in [TYPE_STRING, TYPE_STRING_NAME]: last_error = "Departed payroll Staff ID must be a string." return false var staff_id := StringName(raw_staff_id) var raw_amount: Variant = raw_payroll.get(&"unpaid_wages", null) if typeof(raw_amount) != TYPE_INT: last_error = "Departed payroll amount must be a non-negative integer." return false var amount := int(raw_amount) if staff_id.is_empty() or amount < 0 or next_payroll.has(staff_id): last_error = "Departed payroll snapshot entry is invalid or duplicated." return false next_payroll[staff_id] = amount var next_settlements: Dictionary = {} var next_payroll_days: Dictionary = {} if snapshot_version >= PAYROLL_SNAPSHOT_VERSION: var parsed_history := _parse_payroll_history_snapshot(snapshot[&"payroll_settlements"]) if not bool(parsed_history.get(&"ok", false)): last_error = String(parsed_history.get(&"message", "Payroll settlement history is invalid.")) return false next_settlements = parsed_history[&"settlements"] next_payroll_days = parsed_history[&"day_index"] var invariant_report := _validate_candidate_invariants( next_staff, next_tasks, next_shifts, next_payroll ) if not bool(invariant_report[&"ok"]): if not recover_orphan_tasks: last_error = "StaffService snapshot violates invariants: %s" % [str(invariant_report[&"errors"])] return false var recovery_scope := _validate_recovery_candidate_scope(next_staff, next_tasks, next_shifts) if not bool(recovery_scope[&"ok"]): last_error = "StaffService recovery contains non-repairable damage: %s" % [str(recovery_scope[&"errors"])] return false var next_dispatcher := TaskDispatcherType.new() if not next_dispatcher.restore_snapshot(snapshot[&"dispatcher"]): last_error = next_dispatcher.last_restore_error return false if ( typeof(snapshot.get(&"current_minute", null)) != TYPE_INT or typeof(snapshot.get(&"next_shift_sequence", null)) != TYPE_INT or typeof(snapshot.get(&"next_task_sequence", null)) != TYPE_INT ): last_error = "StaffService snapshot counters must be integers." return false var next_current_minute := int(snapshot[&"current_minute"]) var next_shift_sequence := int(snapshot[&"next_shift_sequence"]) var next_task_sequence := int(snapshot[&"next_task_sequence"]) if next_current_minute < 0 or next_shift_sequence <= 0 or next_task_sequence <= 0: last_error = "StaffService snapshot counters are invalid." return false var next_last_advanced_minute := next_current_minute if snapshot_version >= PAYROLL_SNAPSHOT_VERSION: var raw_last_advanced: Variant = snapshot.get(&"last_advanced_minute", null) if typeof(raw_last_advanced) != TYPE_INT: last_error = "StaffService last advanced minute must be an integer." return false next_last_advanced_minute = int(raw_last_advanced) if next_last_advanced_minute < -1 or next_last_advanced_minute > next_current_minute: last_error = "StaffService last advanced minute is invalid." return false _staff = next_staff _shifts = next_shifts _tasks = next_tasks _schedule_overrides = next_overrides _work_assignments = next_assignments _departed_payroll = next_payroll _payroll_settlements = next_settlements _payroll_days = next_payroll_days current_minute = next_current_minute _last_advanced_minute = next_last_advanced_minute _next_shift_sequence = next_shift_sequence _next_task_sequence = next_task_sequence _dispatcher = next_dispatcher _dispatcher.configure_callbacks(_item_reserver, _item_releaser, _travel_estimator) if recover_orphan_tasks: var repair_report := _audit_orphan_tasks_atomically(current_minute) last_recovery_repair_report = repair_report.duplicate(true) last_recovery_repair_report[&"automatic_repairs_applied"] = ( not repair_report[&"reopened_task_ids"].is_empty() or not repair_report[&"released_staff_ids"].is_empty() ) if not repair_report[&"release_failed_task_ids"].is_empty(): last_error = "StaffService recovery could not release orphan Task reservations." return false var repaired_invariants := _validate_candidate_invariants( _staff, _tasks, _shifts, _departed_payroll ) if not bool(repaired_invariants[&"ok"]): last_error = "StaffService recovery left invalid state: %s" % [str(repaired_invariants[&"errors"])] return false last_dispatch_report = {} last_error = "" return true func _update_shift_states(absolute_minute: int, only_staff: StringName = &"", record_duty := true) -> void: for staff_id: StringName in _sorted_keys(_staff): if not only_staff.is_empty() and only_staff != staff_id: continue var staff: StaffRuntime = _staff[staff_id] var shift := get_shift(staff.shift_id) if shift != null and shift.staff_id != staff.staff_id: shift = null staff.shift_id = &"" if shift != null and shift.has_ended(absolute_minute): var current_task := get_task(staff.current_task_id) if current_task != null and current_task.interrupt_level == 0: staff.is_overtime = true shift.record_duty_minute(absolute_minute, true) continue if current_task != null: var ids := current_task.reservation_ids.duplicate() if not _release_reservations(ids, current_task, absolute_minute): staff.is_overtime = true shift.record_duty_minute(absolute_minute, true) continue current_task.reopen( &"SHIFT_ENDED", absolute_minute, current_task.preserve_progress_on_interrupt ) task_reopened.emit(current_task.task_id, &"SHIFT_ENDED") staff.current_task_id = &"" staff.is_overtime = false if String(staff.absence_reason).begins_with("COMMUTE_"): staff.absence_reason = &"" staff.shift_id = &"" if staff.assigned_facility_id == shift.assigned_facility_id: staff.assigned_facility_id = &"" if staff.can_take_shift(): staff.transition_to(StaffRuntimeType.State.OFF_DUTY, &"SHIFT_ENDED", absolute_minute) shift.close(absolute_minute) shift_state_changed.emit(staff.staff_id, shift.shift_instance_id, staff.state) shift = null # A large clock jump may skip an entire Shift. Close such records at their # scheduled boundary so they cannot later be mistaken for pending work. for skipped: ShiftInstance in _shifts_for_staff(staff.staff_id): if skipped.actual_ended_at < 0 and skipped.has_ended(absolute_minute): skipped.close(skipped.end_at) if shift == null: shift = _find_relevant_shift(staff.staff_id, absolute_minute) if shift != null and staff.can_take_shift(): staff.shift_id = shift.shift_instance_id else: shift = null if shift == null or not staff.can_take_shift(): continue if not shift.assigned_facility_id.is_empty(): staff.assigned_facility_id = shift.assigned_facility_id if shift.is_commute_window(absolute_minute): if staff.current_task_id.is_empty() and staff.state == StaffRuntimeType.State.OFF_DUTY: staff.transition_to(StaffRuntimeType.State.COMMUTING, &"SHIFT_COMMUTE", absolute_minute) shift_state_changed.emit(staff.staff_id, shift.shift_instance_id, staff.state) continue if shift.contains_minute(absolute_minute): staff.is_overtime = false var attendance_required := requires_attendance(staff.staff_id) var attended := not attendance_required or bool(_attendance_driver.call(staff, shift, absolute_minute)) if attendance_required and attended and String(staff.absence_reason).begins_with("COMMUTE_"): staff.absence_reason = &"" if staff.current_task_id.is_empty() and staff.state in [ StaffRuntimeType.State.OFF_DUTY, StaffRuntimeType.State.COMMUTING, StaffRuntimeType.State.AVAILABLE, ]: var next_state := StaffRuntimeType.State.AVAILABLE if attended else StaffRuntimeType.State.COMMUTING if staff.state != next_state: var reason := &"SHIFT_STARTED" if attended else (&"RETURN_TO_POST" if shift.actual_started_at >= 0 else &"SHIFT_COMMUTE") staff.transition_to(next_state, reason, absolute_minute) shift_state_changed.emit(staff.staff_id, shift.shift_instance_id, staff.state) if record_duty and attended: shift.record_duty_minute(absolute_minute) if absolute_minute >= shift.start_at + 15 and staff.state == StaffRuntimeType.State.COMMUTING \ and (not attendance_required or shift.actual_started_at < 0): var late_reason := staff.absence_reason if attendance_required and not staff.absence_reason.is_empty() else DEFAULT_LATE_REASON shift.mark_late(absolute_minute, late_reason) func _find_relevant_shift(staff_id: StringName, absolute_minute: int) -> ShiftInstance: for shift: ShiftInstance in _shifts_for_staff(staff_id): if shift.actual_ended_at >= 0: continue if shift.is_commute_window(absolute_minute) or shift.contains_minute(absolute_minute): return shift return null func _shifts_for_staff(staff_id: StringName) -> Array[ShiftInstance]: var result: Array[ShiftInstance] = [] for shift_id: StringName in _sorted_keys(_shifts): var shift: ShiftInstance = _shifts[shift_id] if shift.staff_id == staff_id: result.append(shift) result.sort_custom( func(a: ShiftInstance, b: ShiftInstance) -> bool: if a.start_at != b.start_at: return a.start_at < b.start_at return String(a.shift_instance_id) < String(b.shift_instance_id) ) return result static func _shifts_overlap(a: ShiftInstance, b: ShiftInstance) -> bool: if a.cancelled or b.cancelled: return false return a.start_at < b.end_at and b.start_at < a.end_at func _apply_override_to_shift(shift: ShiftInstance) -> void: var entry := get_schedule_override(shift.staff_id, shift.start_day) if entry.is_empty(): return var patch: Dictionary = entry[&"patch"] var start_minute := int(patch.get(&"start_minute_of_day", shift.start_minute_of_day)) var end_minute := int(patch.get(&"end_minute_of_day", shift.end_minute_of_day)) shift.configure( shift.shift_instance_id, shift.staff_id, shift.template_id, shift.start_day, start_minute, end_minute, StringName(patch.get(&"assigned_facility_id", shift.assigned_facility_id)), {&"cancelled": patch.get(&"cancelled", shift.cancelled)} ) func _release_reservations(ids: Variant, task: StaffTask, absolute_minute: int) -> bool: var reservation_ids: Array[StringName] = [] if ids is Array: for value: Variant in ids: reservation_ids.append(StringName(value)) if reservation_ids.is_empty(): return true if not _item_releaser.is_valid(): return false var response: Variant = _item_releaser.call(reservation_ids, task, absolute_minute) if response is Dictionary: return bool(response.get(&"ok", false)) return bool(response) func _has_high_priority_open_task() -> bool: for task_id: StringName in _sorted_keys(_tasks): var task: StaffTask = _tasks[task_id] if task.is_dispatchable() and task.priority >= HIGH_PRIORITY_THRESHOLD: return true return false func _build_payroll_preview(day: int) -> Dictionary: var rows_by_staff: Dictionary = {} var new_wages_total := 0 var prior_arrears_total := 0 var total_due := 0 var period_end := day * MINUTES_PER_DAY if _staff.size() + _departed_payroll.size() > MAX_PAYROLL_ROWS: return _payroll_preview_error(&"ROW_BUDGET_EXCEEDED", "Payroll row budget is exceeded.") for staff_id: StringName in _sorted_keys(_staff): var staff: StaffRuntime = _staff[staff_id] var prior := staff.unpaid_wages if staff.employment_effective_minute < 0: return _payroll_preview_error( &"INVALID_EMPLOYMENT_EFFECTIVE_MINUTE", "Staff employment effective minute is invalid." ) # Daily close covers [0, day * 1440). A hire exactly on the end # boundary belongs to the next close; carried debt remains due now. var newly_accrued := ( staff.get_effective_daily_wage() if staff.employment_effective_minute < period_end else 0 ) if ( prior < 0 or newly_accrued < 0 or prior > MAX_PAYROLL_AMOUNT - newly_accrued ): return _payroll_preview_error(&"AMOUNT_OUT_OF_RANGE", "Active payroll amount is out of range.") var due := prior + newly_accrued if ( new_wages_total > MAX_PAYROLL_AMOUNT - newly_accrued or prior_arrears_total > MAX_PAYROLL_AMOUNT - prior or total_due > MAX_PAYROLL_AMOUNT - due ): return _payroll_preview_error(&"TOTAL_OUT_OF_RANGE", "Payroll total is out of range.") rows_by_staff[staff_id] = { &"staff_id": staff_id, &"payroll_status": &"ACTIVE", &"employment_kind": staff.get_employment_kind_name(), &"daily_wage": newly_accrued, &"prior_arrears": prior, &"new_wages": newly_accrued, &"total_due": due, } new_wages_total += newly_accrued prior_arrears_total += prior total_due += due for staff_id: StringName in _sorted_keys(_departed_payroll): var prior := int(_departed_payroll[staff_id]) if prior <= 0: continue if rows_by_staff.has(staff_id): return _payroll_preview_error(&"STAFF_ID_COLLISION", "Active and departed payroll IDs collide.") if prior > MAX_PAYROLL_AMOUNT: return _payroll_preview_error(&"AMOUNT_OUT_OF_RANGE", "Departed payroll amount is out of range.") if prior_arrears_total > MAX_PAYROLL_AMOUNT - prior or total_due > MAX_PAYROLL_AMOUNT - prior: return _payroll_preview_error(&"TOTAL_OUT_OF_RANGE", "Payroll total is out of range.") rows_by_staff[staff_id] = { &"staff_id": staff_id, &"payroll_status": &"DEPARTED", &"employment_kind": &"", &"daily_wage": 0, &"prior_arrears": prior, &"new_wages": 0, &"total_due": prior, } prior_arrears_total += prior total_due += prior var rows: Array[Dictionary] = [] for staff_id: StringName in _sorted_keys(rows_by_staff): rows.append((rows_by_staff[staff_id] as Dictionary).duplicate(true)) return { &"ok": true, &"day": day, &"rows": rows, &"new_wages_total": new_wages_total, &"prior_arrears_total": prior_arrears_total, &"total_due": total_due, } func _payroll_preview_error( reason: StringName, message: String, details: Dictionary = {} ) -> Dictionary: var result := { &"ok": false, &"reason": reason, &"message": message, &"rows": [], &"new_wages_total": 0, &"prior_arrears_total": 0, &"total_due": 0, } for key: Variant in details: result[key] = details[key] return result static func _stable_settlement_id_is_valid(value: Variant) -> bool: if typeof(value) not in [TYPE_STRING, TYPE_STRING_NAME]: return false var text := String(value) if text.is_empty() or text.length() > 128: return false for index: int in range(text.length()): var code := text.unicode_at(index) var allowed := ( code >= 48 and code <= 57 or code >= 65 and code <= 90 or code >= 97 and code <= 122 or code in [45, 46, 58, 64, 95] ) if not allowed: return false return true static func _parse_payroll_payments(source: Dictionary) -> Dictionary: if source.size() > MAX_PAYROLL_ROWS: return {&"ok": false, &"message": "Payroll payment row budget is exceeded."} var payments_by_id: Dictionary = {} for raw_staff_id: Variant in source: if typeof(raw_staff_id) not in [TYPE_STRING, TYPE_STRING_NAME]: return {&"ok": false, &"message": "Payroll Staff IDs must be strings."} var staff_id := StringName(raw_staff_id) var raw_amount: Variant = source[raw_staff_id] if staff_id.is_empty() or payments_by_id.has(staff_id): return {&"ok": false, &"message": "Payroll Staff ID is empty or duplicated."} if typeof(raw_amount) != TYPE_INT or int(raw_amount) < 0 or int(raw_amount) > MAX_PAYROLL_AMOUNT: return {&"ok": false, &"message": "Payroll payment must be a bounded non-negative integer."} payments_by_id[staff_id] = int(raw_amount) var payments: Array[Dictionary] = [] for staff_id: StringName in _sorted_keys(payments_by_id): payments.append({&"staff_id": staff_id, &"amount": int(payments_by_id[staff_id])}) return {&"ok": true, &"payments": payments} static func _payroll_fingerprint(day: int, payments: Array[Dictionary]) -> String: var canonical := "day=%d|" % day for payment: Dictionary in payments: var staff_text := String(payment[&"staff_id"]) canonical += "%d:%s=%d;" % [staff_text.length(), staff_text, int(payment[&"amount"])] return canonical.sha256_text() static func _serialize_payroll_settlement(record: Dictionary) -> Dictionary: var payments: Array[Dictionary] = [] for payment: Dictionary in record[&"input_payments"]: payments.append({ &"staff_id": String(payment[&"staff_id"]), &"amount": int(payment[&"amount"]), }) var rows: Array[Dictionary] = [] for source_row: Dictionary in record[&"rows"]: var row := source_row.duplicate(true) row[&"staff_id"] = String(source_row[&"staff_id"]) row[&"payroll_status"] = String(source_row[&"payroll_status"]) row[&"employment_kind"] = String(source_row[&"employment_kind"]) rows.append(row) return { &"settlement_id": String(record[&"settlement_id"]), &"day": int(record[&"day"]), &"fingerprint": String(record[&"fingerprint"]), &"input_payments": payments, &"rows": rows, &"new_wages_total": int(record[&"new_wages_total"]), &"prior_arrears_total": int(record[&"prior_arrears_total"]), &"total_due": int(record[&"total_due"]), &"paid_total": int(record[&"paid_total"]), &"unpaid_after_total": int(record[&"unpaid_after_total"]), } static func _parse_payroll_history_snapshot(source: Array) -> Dictionary: if source.size() > MAX_PAYROLL_SETTLEMENTS: return {&"ok": false, &"message": "Payroll settlement history budget is exceeded."} var settlements: Dictionary = {} var day_index: Dictionary = {} var previous_day := 0 for raw_record: Variant in source: if not raw_record is Dictionary: return {&"ok": false, &"message": "Payroll settlement entry must be a Dictionary."} var record: Dictionary = raw_record if not _dictionary_has_exact_keys(record, [ &"settlement_id", &"day", &"fingerprint", &"input_payments", &"rows", &"new_wages_total", &"prior_arrears_total", &"total_due", &"paid_total", &"unpaid_after_total", ]): return {&"ok": false, &"message": "Payroll settlement fields are malformed."} if not _stable_settlement_id_is_valid(record[&"settlement_id"]): return {&"ok": false, &"message": "Payroll settlement ID is invalid."} if typeof(record[&"day"]) != TYPE_INT: return {&"ok": false, &"message": "Payroll settlement day must be an integer."} var settlement_id := StringName(record[&"settlement_id"]) var day := int(record[&"day"]) if day <= 0 or day > MAX_PAYROLL_DAY or day <= previous_day: return {&"ok": false, &"message": "Payroll settlement days must be unique and strictly ordered."} if settlements.has(settlement_id) or day_index.has(day): return {&"ok": false, &"message": "Payroll settlement ID or day is duplicated."} if not record[&"input_payments"] is Array or not record[&"rows"] is Array: return {&"ok": false, &"message": "Payroll settlement collections are malformed."} if record[&"input_payments"].size() > MAX_PAYROLL_ROWS or record[&"rows"].size() > MAX_PAYROLL_ROWS: return {&"ok": false, &"message": "Payroll settlement row budget is exceeded."} var input_payments: Array[Dictionary] = [] var payment_index: Dictionary = {} var previous_payment_id := "" for raw_payment: Variant in record[&"input_payments"]: if not raw_payment is Dictionary or not _dictionary_has_exact_keys(raw_payment, [&"staff_id", &"amount"]): return {&"ok": false, &"message": "Payroll input payment row is malformed."} if typeof(raw_payment[&"staff_id"]) not in [TYPE_STRING, TYPE_STRING_NAME]: return {&"ok": false, &"message": "Payroll input payment Staff ID is malformed."} var staff_id := StringName(raw_payment[&"staff_id"]) var staff_text := String(staff_id) var raw_amount: Variant = raw_payment[&"amount"] if ( staff_id.is_empty() or (not previous_payment_id.is_empty() and staff_text <= previous_payment_id) or typeof(raw_amount) != TYPE_INT or int(raw_amount) < 0 or int(raw_amount) > MAX_PAYROLL_AMOUNT ): return {&"ok": false, &"message": "Payroll input payments are invalid or unordered."} input_payments.append({&"staff_id": staff_id, &"amount": int(raw_amount)}) payment_index[staff_id] = int(raw_amount) previous_payment_id = staff_text var rows: Array[Dictionary] = [] var row_ids: Dictionary = {} var previous_row_id := "" var derived_new := 0 var derived_prior := 0 var derived_due := 0 var derived_paid := 0 var derived_unpaid := 0 for raw_row: Variant in record[&"rows"]: if not raw_row is Dictionary or not _dictionary_has_exact_keys(raw_row, [ &"staff_id", &"payroll_status", &"employment_kind", &"daily_wage", &"prior_arrears", &"new_wages", &"total_due", &"payment", &"unpaid_after", ]): return {&"ok": false, &"message": "Payroll settlement result row is malformed."} if typeof(raw_row[&"staff_id"]) not in [TYPE_STRING, TYPE_STRING_NAME]: return {&"ok": false, &"message": "Payroll settlement row Staff ID is malformed."} var staff_id := StringName(raw_row[&"staff_id"]) var staff_text := String(staff_id) if staff_id.is_empty() or (not previous_row_id.is_empty() and staff_text <= previous_row_id): return {&"ok": false, &"message": "Payroll settlement rows are duplicated or unordered."} var status_value: Variant = raw_row[&"payroll_status"] var kind_value: Variant = raw_row[&"employment_kind"] if typeof(status_value) not in [TYPE_STRING, TYPE_STRING_NAME] or typeof(kind_value) not in [TYPE_STRING, TYPE_STRING_NAME]: return {&"ok": false, &"message": "Payroll settlement row labels are malformed."} var status := StringName(status_value) var employment_kind := StringName(kind_value) if status not in [&"ACTIVE", &"DEPARTED"]: return {&"ok": false, &"message": "Payroll settlement row status is invalid."} if status == &"ACTIVE" and StaffRuntimeType.EmploymentKind.keys().find(String(employment_kind)) < 0: return {&"ok": false, &"message": "Active payroll employment kind is invalid."} if status == &"DEPARTED" and not employment_kind.is_empty(): return {&"ok": false, &"message": "Departed payroll employment kind must be empty."} var numeric_keys: Array[StringName] = [ &"daily_wage", &"prior_arrears", &"new_wages", &"total_due", &"payment", &"unpaid_after", ] for numeric_key: StringName in numeric_keys: var raw_number: Variant = raw_row[numeric_key] if typeof(raw_number) != TYPE_INT or int(raw_number) < 0 or int(raw_number) > MAX_PAYROLL_AMOUNT: return {&"ok": false, &"message": "Payroll settlement row amount is invalid."} var daily_wage := int(raw_row[&"daily_wage"]) var prior := int(raw_row[&"prior_arrears"]) var newly_accrued := int(raw_row[&"new_wages"]) var due := int(raw_row[&"total_due"]) var payment := int(raw_row[&"payment"]) var unpaid_after := int(raw_row[&"unpaid_after"]) if ( due != prior + newly_accrued or payment > due or unpaid_after != due - payment or (status == &"ACTIVE" and newly_accrued != daily_wage) or (status == &"DEPARTED" and (daily_wage != 0 or newly_accrued != 0)) or payment != int(payment_index.get(staff_id, 0)) ): return {&"ok": false, &"message": "Payroll settlement row derivation is invalid."} var row: Dictionary = (raw_row as Dictionary).duplicate(true) row[&"staff_id"] = staff_id row[&"payroll_status"] = status row[&"employment_kind"] = employment_kind rows.append(row) row_ids[staff_id] = true previous_row_id = staff_text derived_new += newly_accrued derived_prior += prior derived_due += due derived_paid += payment derived_unpaid += unpaid_after if ( derived_new > MAX_PAYROLL_AMOUNT or derived_prior > MAX_PAYROLL_AMOUNT or derived_due > MAX_PAYROLL_AMOUNT or derived_paid > MAX_PAYROLL_AMOUNT or derived_unpaid > MAX_PAYROLL_AMOUNT ): return {&"ok": false, &"message": "Payroll settlement derived total is out of range."} for payment: Dictionary in input_payments: if not row_ids.has(payment[&"staff_id"]): return {&"ok": false, &"message": "Payroll input payment references an unknown result row."} var totals := { &"new_wages_total": derived_new, &"prior_arrears_total": derived_prior, &"total_due": derived_due, &"paid_total": derived_paid, &"unpaid_after_total": derived_unpaid, } for total_key: StringName in totals: if typeof(record[total_key]) != TYPE_INT or int(record[total_key]) != int(totals[total_key]): return {&"ok": false, &"message": "Payroll settlement total does not match its rows."} var expected_fingerprint := _payroll_fingerprint(day, input_payments) if typeof(record[&"fingerprint"]) != TYPE_STRING or String(record[&"fingerprint"]) != expected_fingerprint: return {&"ok": false, &"message": "Payroll settlement fingerprint is invalid."} var parsed := { &"settlement_id": settlement_id, &"day": day, &"fingerprint": expected_fingerprint, &"input_payments": input_payments, &"rows": rows, &"new_wages_total": derived_new, &"prior_arrears_total": derived_prior, &"total_due": derived_due, &"paid_total": derived_paid, &"unpaid_after_total": derived_unpaid, } settlements[settlement_id] = parsed day_index[day] = settlement_id previous_day = day return {&"ok": true, &"settlements": settlements, &"day_index": day_index} static func _validate_payroll_history(settlements: Dictionary) -> Dictionary: var records: Array[Dictionary] = [] for settlement_id: StringName in _sorted_keys(settlements): var value: Variant = settlements[settlement_id] if not value is Dictionary: return {&"ok": false, &"errors": ["Payroll settlement record is malformed."], &"day_index": {}} records.append(value) records.sort_custom(func(left: Dictionary, right: Dictionary) -> bool: if int(left.get(&"day", -1)) != int(right.get(&"day", -1)): return int(left.get(&"day", -1)) < int(right.get(&"day", -1)) return String(left.get(&"settlement_id", "")) < String(right.get(&"settlement_id", "")) ) var parsed := _parse_payroll_history_snapshot(records) if not bool(parsed.get(&"ok", false)): return { &"ok": false, &"errors": [String(parsed.get(&"message", "Payroll settlement history is invalid."))], &"day_index": {}, } return {&"ok": true, &"errors": [], &"day_index": parsed[&"day_index"]} static func _dictionary_has_exact_keys(source: Dictionary, expected: Array) -> bool: if source.size() != expected.size(): return false for key: Variant in expected: if not source.has(key): return false return true static func _validate_candidate_invariants( staff_by_id: Dictionary, tasks_by_id: Dictionary, shifts_by_id: Dictionary, departed_payroll: Dictionary = {} ) -> Dictionary: var errors: Array[String] = [] var reservation_owners: Dictionary = {} for task_id: StringName in _sorted_keys(tasks_by_id): var task: StaffTask = tasks_by_id[task_id] if not task.is_valid(): errors.append("Task %s is invalid." % task_id) continue for reservation_id: StringName in task.reservation_ids: if reservation_owners.has(reservation_id): errors.append( "Reservation %s is claimed by Tasks %s and %s." % [reservation_id, reservation_owners[reservation_id], task_id] ) else: reservation_owners[reservation_id] = task_id if task.assigned_staff_id.is_empty(): continue var staff: StaffRuntime = staff_by_id.get(task.assigned_staff_id) as StaffRuntime if staff == null: errors.append("Task %s references missing Staff." % task_id) elif staff.current_task_id != task.task_id: errors.append("Task %s and Staff %s do not reference each other." % [task_id, staff.staff_id]) for staff_id: StringName in _sorted_keys(staff_by_id): var staff: StaffRuntime = staff_by_id[staff_id] if not staff.is_valid(): errors.append("Staff %s is invalid." % staff_id) elif staff.daily_wage > MAX_PAYROLL_AMOUNT or staff.unpaid_wages > MAX_PAYROLL_AMOUNT: errors.append("Staff %s payroll amount exceeds the service budget." % staff_id) if not staff.current_task_id.is_empty(): var task: StaffTask = tasks_by_id.get(staff.current_task_id) as StaffTask if task == null or task.assigned_staff_id != staff.staff_id: errors.append("Staff %s references an orphan Task." % staff_id) if not staff.shift_id.is_empty(): var shift: ShiftInstance = shifts_by_id.get(staff.shift_id) as ShiftInstance if shift == null or shift.staff_id != staff.staff_id: errors.append("Staff %s references an invalid Shift." % staff_id) for shift_id: StringName in _sorted_keys(shifts_by_id): var shift: ShiftInstance = shifts_by_id[shift_id] if not shift.is_valid(): errors.append("Shift %s is invalid." % shift_id) elif not staff_by_id.has(shift.staff_id): errors.append("Shift %s references missing Staff." % shift_id) for departed_staff_id: StringName in _sorted_keys(departed_payroll): var raw_amount: Variant = departed_payroll[departed_staff_id] if ( departed_staff_id.is_empty() or typeof(raw_amount) != TYPE_INT or int(raw_amount) < 0 or int(raw_amount) > MAX_PAYROLL_AMOUNT ): errors.append("Departed payroll %s is invalid." % departed_staff_id) if staff_by_id.has(departed_staff_id): errors.append("Staff %s exists in both active and departed payroll." % departed_staff_id) var shift_ids := _sorted_keys(shifts_by_id) for left_index: int in range(shift_ids.size()): var left: ShiftInstance = shifts_by_id[shift_ids[left_index]] for right_index: int in range(left_index + 1, shift_ids.size()): var right: ShiftInstance = shifts_by_id[shift_ids[right_index]] if left.staff_id == right.staff_id and _shifts_overlap(left, right): errors.append( "Shifts %s and %s overlap for Staff %s." % [left.shift_instance_id, right.shift_instance_id, left.staff_id] ) return {&"ok": errors.is_empty(), &"errors": errors} ## Only bidirectional Staff/Task orphan links are recoverable. Invalid objects, ## Shift ownership, missing Shift staff, and overlaps remain hard corruption and ## are rejected before any external Reservation release is attempted. static func _validate_recovery_candidate_scope( staff_by_id: Dictionary, tasks_by_id: Dictionary, shifts_by_id: Dictionary ) -> Dictionary: var errors: Array[String] = [] var reservation_owners: Dictionary = {} for task_id: StringName in _sorted_keys(tasks_by_id): var task: StaffTask = tasks_by_id[task_id] if not task.is_valid(): errors.append("Task %s is invalid." % task_id) continue for reservation_id: StringName in task.reservation_ids: if reservation_owners.has(reservation_id): errors.append( "Reservation %s is claimed by Tasks %s and %s." % [reservation_id, reservation_owners[reservation_id], task_id] ) else: reservation_owners[reservation_id] = task_id for staff_id: StringName in _sorted_keys(staff_by_id): var staff: StaffRuntime = staff_by_id[staff_id] if not staff.is_valid(): errors.append("Staff %s is invalid." % staff_id) if not staff.shift_id.is_empty(): var shift: ShiftInstance = shifts_by_id.get(staff.shift_id) as ShiftInstance if shift == null or shift.staff_id != staff.staff_id: errors.append("Staff %s references an invalid Shift." % staff_id) for shift_id: StringName in _sorted_keys(shifts_by_id): var shift: ShiftInstance = shifts_by_id[shift_id] if not shift.is_valid(): errors.append("Shift %s is invalid." % shift_id) elif not staff_by_id.has(shift.staff_id): errors.append("Shift %s references missing Staff." % shift_id) var shift_ids := _sorted_keys(shifts_by_id) for left_index: int in range(shift_ids.size()): var left: ShiftInstance = shifts_by_id[shift_ids[left_index]] for right_index: int in range(left_index + 1, shift_ids.size()): var right: ShiftInstance = shifts_by_id[shift_ids[right_index]] if left.staff_id == right.staff_id and _shifts_overlap(left, right): errors.append( "Shifts %s and %s overlap for Staff %s." % [left.shift_instance_id, right.shift_instance_id, left.staff_id] ) return {&"ok": errors.is_empty(), &"errors": errors} static func _override_key(staff_id: StringName, start_day: int) -> StringName: return StringName("%s@%d" % [staff_id, start_day]) static func _sorted_keys(source: Dictionary) -> Array[StringName]: var result: Array[StringName] = [] for value: Variant in source.keys(): result.append(StringName(value)) result.sort_custom(func(a: StringName, b: StringName) -> bool: return String(a) < String(b)) return result func _success(details: Dictionary = {}) -> CommandResult: last_error = "" return CommandResultType.succeeded(0, details) func _invalid(message: String, details: Dictionary = {}) -> CommandResult: last_error = message return CommandResultType.failed(CommandResultType.Code.VALIDATION_FAILED, 0, message, details) func _not_found(label: String, entity_id: StringName) -> CommandResult: var message := "%s was not found: %s" % [label, entity_id] last_error = message return CommandResultType.failed( CommandResultType.Code.NOT_FOUND, 0, message, {&"entity_id": entity_id} ) func _duplicate(label: String, entity_id: StringName) -> CommandResult: var message := "%s ID already exists: %s" % [label, entity_id] last_error = message return CommandResultType.failed( CommandResultType.Code.DUPLICATE_COMMAND_ID, 0, message, {&"entity_id": entity_id} ) func _conflict(message: String) -> CommandResult: last_error = message return CommandResultType.failed(CommandResultType.Code.TRANSACTION_APPLY_FAILED, 0, message)