Fix firm default task sync sequence conflicts

This commit is contained in:
A R R R Associates
2026-09-01 14:03:01 +05:30
parent 2a8a829428
commit d0d9b7b2eb
+41 -22
View File
@@ -176,6 +176,36 @@ def sync_firm_tasks_from_system_defaults(
firm_task_ids=[int(row.id) for row in firm_rows if getattr(row, "id", None)],
)
# ------------------------------------------------------------------
# IMPORTANT: two-phase sequence re-numbering
#
# The database enforces a UNIQUE constraint on:
# (tenant_id, service_catalogue_id, sequence_no)
#
# Existing duplicate imports may contain several generations of the same
# task at different sequence numbers. During a sync, moving a canonical
# row directly onto the latest system-default sequence can therefore
# collide with another historical row that still owns that sequence.
#
# Marking that historical row inactive does NOT release the unique key.
# Park every existing firm template on a guaranteed-unique temporary
# sequence first, flush, and only then apply final/default sequences.
# ------------------------------------------------------------------
max_existing_sequence = max(
[int(getattr(row, "sequence_no", 0) or 0) for row in firm_rows] + [0]
)
max_default_sequence = max(
[int(getattr(row, "sequence_no", 0) or 0) for row in defaults] + [0]
)
parking_base = max(max_existing_sequence, max_default_sequence, 0) + 100000
for offset, row in enumerate(sorted(firm_rows, key=lambda item: int(item.id)), start=1):
row.sequence_no = parking_base + offset
# This flush is intentional and must happen before any canonical row is
# assigned a system-default sequence number.
db.flush()
# First clean pre-existing duplicate groups, regardless of whether the task
# remains in the current system defaults. A unique firm-only task is untouched.
for candidates in by_name.values():
@@ -188,6 +218,10 @@ def sync_firm_tasks_from_system_defaults(
for duplicate in candidates:
if duplicate.id == canonical.id:
continue
# Duplicate rows are retained for historical FK safety but remain
# on their unique parked sequence number. The UNIQUE sequence
# constraint therefore remains satisfied even after deactivation.
if getattr(duplicate, "is_active", True):
duplicate.is_active = False
if (
@@ -221,28 +255,13 @@ def sync_firm_tasks_from_system_defaults(
# No name match. A unique sequence match is a conservative fallback for a
# system-default rename while still avoiding arbitrary replacement.
sequence_no = getattr(default, "sequence_no", None)
sequence_candidates = [
row
for row in firm_rows
if getattr(row, "sequence_no", None) == sequence_no
and getattr(row, "is_active", True)
]
if len(sequence_candidates) == 1:
target = sequence_candidates[0]
changed = _copy_default_columns(default, target)
if (
updated_by_user_id is not None
and hasattr(target, "updated_by_user_id")
):
target.updated_by_user_id = updated_by_user_id
if changed:
result.updated += 1
else:
result.unchanged += 1
# Make subsequent defaults see the new name.
by_name[_normalise_name(getattr(target, "task_name", None))].append(target)
continue
# Do NOT use the pre-sync sequence number as an identity fallback here.
# All existing rows have deliberately been parked on temporary sequence
# numbers to satisfy the DB unique constraint. More importantly,
# sequence number is ordering metadata, not a stable task identity.
# A renamed system-default task without a name match is therefore
# treated as a new task rather than risking replacement of the wrong
# firm-specific task.
# Missing task: construct from the intersection of mapped columns.
source_cols = _column_names(ServiceDefaultTaskTemplate)