From 28ca42517c832b82df6dc8f176299d1ab3d94d4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Manciot?= Date: Sun, 14 Jun 2026 09:42:44 +0200 Subject: [PATCH] feat(audit): cross-service correlation id on schedules (Story 13.7 Phase B gate #2) Schedule model gains correlation_id (#9): set when a schedule is added (e.g. a license renewal scheduled at issuance carries the checkout's id), preserved through copy/withLastTriggered. It is the durable hop to the consumer, since Scheduler2EntityProcessorStream forwards only evt.schedule to triggerSchedule. ScheduleTriggeredEvent extends AuditableEvent + correlation_id (#2) for the scheduler's own journal/audit; SchedulerBehavior stamps it from updatedSchedule.correlationId at the trigger site. Bumps genericPersistence -> 0.9-SNAPSHOT (Auditable carriers) and scheduler version -> 0.8-SNAPSHOT. SchedulerHandlerSpec: +cid add->trigger propagation test (7 green); +compile (2.12+2.13) + scalafmtAll clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- build.sbt | 2 +- common/src/main/protobuf/event/schedule.proto | 6 ++++++ common/src/main/protobuf/model/schedule.proto | 6 ++++++ .../persistence/typed/SchedulerBehavior.scala | 4 ++++ project/Versions.scala | 2 +- .../scheduler/handlers/SchedulerHandlerSpec.scala | 15 +++++++++++++++ 6 files changed, 33 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 405be64..0df34e3 100644 --- a/build.sbt +++ b/build.sbt @@ -18,7 +18,7 @@ ThisBuild / organization := "app.softnetwork" name := "scheduler" -ThisBuild / version := "0.8.0" +ThisBuild / version := "0.8-SNAPSHOT" ThisBuild / scalaVersion := scala212 diff --git a/common/src/main/protobuf/event/schedule.proto b/common/src/main/protobuf/event/schedule.proto index 395a7e5..8cbcba2 100644 --- a/common/src/main/protobuf/event/schedule.proto +++ b/common/src/main/protobuf/event/schedule.proto @@ -44,7 +44,13 @@ message ScheduleAddedEvent { message ScheduleTriggeredEvent { option (scalapb.message).extends = "ProtobufEvent"; option (scalapb.message).extends = "SchedulerEvent"; + // Story 13.7 — AuditableEvent (resolves via the file-level import of + // app.softnetwork.persistence.message._). The event carries the correlation id for the scheduler's + // own journal/audit; the durable hop to the consumer is the id on `schedule` (model #9), since + // Scheduler2EntityProcessorStream forwards only evt.schedule. + option (scalapb.message).extends = "AuditableEvent"; required app.softnetwork.scheduler.model.Schedule schedule = 1; + optional string correlation_id = 2; } message ScheduleRemovedEvent { diff --git a/common/src/main/protobuf/model/schedule.proto b/common/src/main/protobuf/model/schedule.proto index d5941ae..02a8a41 100644 --- a/common/src/main/protobuf/model/schedule.proto +++ b/common/src/main/protobuf/model/schedule.proto @@ -28,6 +28,12 @@ message Schedule { optional google.protobuf.Timestamp scheduledDate = 6 [(scalapb.field).type = "java.util.Date"]; optional google.protobuf.Timestamp lastTriggered = 7 [(scalapb.field).type = "java.util.Date"]; optional string cronTab = 8; + // Story 13.7 — cross-service correlation id. Set when the schedule is added (e.g. a license + // renewal scheduled at issuance carries the checkout's id); preserved through copy / + // withLastTriggered and onto ScheduleTriggeredEvent so the base consumer (which forwards only + // evt.schedule) reaches the triggered action with the originating id. ScalaPB → correlationId: + // Option[String] + withCorrelationId. + optional string correlation_id = 9; } // CronTab diff --git a/core/src/main/scala/app/softnetwork/scheduler/persistence/typed/SchedulerBehavior.scala b/core/src/main/scala/app/softnetwork/scheduler/persistence/typed/SchedulerBehavior.scala index e7c46f7..cb8478a 100644 --- a/core/src/main/scala/app/softnetwork/scheduler/persistence/typed/SchedulerBehavior.scala +++ b/core/src/main/scala/app/softnetwork/scheduler/persistence/typed/SchedulerBehavior.scala @@ -223,7 +223,11 @@ private[scheduler] trait SchedulerBehavior .persist( List( ScheduleAddedEvent(updatedSchedule), // update schedule + // Story 13.7 — carry the correlation id onto the triggered event: the schedule + // (model #9) is the durable hop to the consumer; the event's own #2 field is for + // the scheduler's journal/audit. ScheduleTriggeredEvent(updatedSchedule) + .copy(correlationId = updatedSchedule.correlationId) ) ) .thenRun(_ => { diff --git a/project/Versions.scala b/project/Versions.scala index 1f75f83..eea3125 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -1,6 +1,6 @@ object Versions { - val genericPersistence = "0.8.1" + val genericPersistence = "0.9-SNAPSHOT" val scalatest = "3.2.16" } diff --git a/testkit/src/test/scala/app/softnetwork/scheduler/handlers/SchedulerHandlerSpec.scala b/testkit/src/test/scala/app/softnetwork/scheduler/handlers/SchedulerHandlerSpec.scala index a07f64d..7f3f7bb 100644 --- a/testkit/src/test/scala/app/softnetwork/scheduler/handlers/SchedulerHandlerSpec.scala +++ b/testkit/src/test/scala/app/softnetwork/scheduler/handlers/SchedulerHandlerSpec.scala @@ -166,5 +166,20 @@ class SchedulerHandlerSpec case other => fail(other.getClass) } } + "preserve the correlation id through add and trigger (Story 13.7)" in { + val cid = "corr-13-7" + // a schedule carrying a cross-service correlation id (e.g. a license renewal scheduled at + // issuance) must keep it through add -> state -> trigger, so the base consumer (which forwards + // only evt.schedule) reaches the triggered action with the originating id. + val schedule = Schedule("p", "corr", "add", 1).withCorrelationId(cid) + this !? AddSchedule(schedule) assert { + case r: ScheduleAdded => assert(r.schedule.correlationId.contains(cid)) + case other => fail(other.getClass) + } + this !? TriggerSchedule(schedule.persistenceId, schedule.entityId, schedule.key) assert { + case r: ScheduleTriggered => assert(r.schedule.correlationId.contains(cid)) + case other => fail(other.getClass) + } + } } }