Summary
Cycle.Version exists to guard against lost updates but is never read, so concurrent cycle edits silently clobber each other.
Where
model.Cycle.Version is set to 1 on create (internal/service/cycle.go) and never referenced again. CycleStore.Update is a full-row Save with no WHERE version = ? and no increment. CycleService.Update and CompleteCycle both load the cycle, mutate the struct, and full-row Save.
Impact
An admin editing a cycle's dates while CompleteCycle writes the progress snapshot (or two concurrent edits) lost-update each other. The version column that exists precisely to prevent this is inert. (webhook.Version is similarly a static string, not a concurrency token.)
Suggested fix
Either enforce optimistic concurrency using the version column (compare-and-increment in the update WHERE, return a conflict on mismatch), or switch these writes to UpdateFields with a changed-columns map like the issue PATCH path already does.
Summary
Cycle.Versionexists to guard against lost updates but is never read, so concurrent cycle edits silently clobber each other.Where
model.Cycle.Versionis set to 1 on create (internal/service/cycle.go) and never referenced again.CycleStore.Updateis a full-rowSavewith noWHERE version = ?and no increment.CycleService.UpdateandCompleteCycleboth load the cycle, mutate the struct, and full-row Save.Impact
An admin editing a cycle's dates while
CompleteCyclewrites the progress snapshot (or two concurrent edits) lost-update each other. Theversioncolumn that exists precisely to prevent this is inert. (webhook.Versionis similarly a static string, not a concurrency token.)Suggested fix
Either enforce optimistic concurrency using the
versioncolumn (compare-and-increment in the updateWHERE, return a conflict on mismatch), or switch these writes toUpdateFieldswith a changed-columns map like the issue PATCH path already does.