feat(api): emit event.status.changed business events#1468
Conversation
There was a problem hiding this comment.
Pull request overview
Adds emission of a new event.status.changed business event whenever an event's status transitions, both for operator-initiated PUT/PATCH updates via EventManagementService.UpdateEventAsync and for the auto-close path in RegistrationManagementService.CreateRegistrationAsync (when filling the last spot flips the event to RegistrationsClosed). The auto-close emission is tagged so consumers can distinguish it from manual operator actions, and the actor UUID is taken from the current HttpContext.
Changes:
- New
BusinessEventSubjects.ForEvent(Guid)helper plusevent.status.changedemission fromEventManagementService.UpdateEventAsync(with anAsNoTrackingsnapshot of pre-update status) and from the auto-close branch ofRegistrationManagementService.CreateRegistrationAsync. UpdateEventAsyncinterface and implementation now accept an optionalCancellationToken(callers inEventsControllerstill pass none).- New
EventManagementServiceTestsand added cases inRegistrationManagementServiceTestscovering emit/no-emit paths for both sites; test helper extended to set up an authenticatedHttpContextand seed anOrganizationso the org UUID lookup resolves.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| .changeset/api-event-status-business-events.md | Patch changeset describing the new business event emission. |
| apps/api/src/Eventuras.Domain/BusinessEventSubject.cs | Adds ForEvent(Guid) subject factory. |
| apps/api/src/Eventuras.Services/Events/IEventManagementService.cs | Adds optional CancellationToken to UpdateEventAsync. |
| apps/api/src/Eventuras.Services/Events/EventManagementService.cs | Injects IBusinessEventService/IHttpContextAccessor, snapshots pre-update status, threads CT through saves, emits event.status.changed on delta. |
| apps/api/src/Eventuras.Services/Registrations/RegistrationManagementService.cs | Emits event.status.changed when the auto-close flip changes the status, tagging the message with the MaxParticipants auto-close annotation. |
| apps/api/tests/Eventuras.Services.Tests/Events/EventManagementServiceTests.cs | New tests covering emit on status change and no-emit on unchanged status. |
| apps/api/tests/Eventuras.Services.Tests/Registrations/RegistrationManagementServiceTests.cs | Adds emit/no-emit tests for the auto-close path; extends BuildService to authenticate the caller and seed an org. |
| Task CreateNewEventAsync(EventInfo info); | ||
|
|
||
| Task UpdateEventAsync(EventInfo info); | ||
| Task UpdateEventAsync(EventInfo info, CancellationToken cancellationToken = default); |
| // Pre-populate the event organisation so the auto-close emission can | ||
| // look up the org UUID via DbContext. | ||
| if (!_context.Organizations.Any(o => o.OrganizationId == eventInfo.OrganizationId)) | ||
| { | ||
| _context.Organizations.Add(new Organization | ||
| { | ||
| OrganizationId = eventInfo.OrganizationId == 0 ? 1 : eventInfo.OrganizationId, | ||
| Name = "Test Org", | ||
| }); | ||
| _context.SaveChanges(); | ||
| eventInfo.OrganizationId = _context.Organizations.First().OrganizationId; | ||
| } |
45c476c to
4636b17
Compare
|
Addressed both Copilot review comments in 4636b17 (amended + force-pushed):
All 132 EventsController integration tests + 4 unit tests still pass locally. |
Every status transition on `EventInfo` now produces an `event.status.changed`
business event keyed by the event's UUID, so the audit trail records who
changed an event's status, when, and the from→to transition. Two emission
sites:
- `EventManagementService.UpdateEventAsync` snapshots the pre-update
status (AsNoTracking) and emits after `_context.UpdateAsync` whenever
the new value differs. Covers operator-initiated transitions made via
PUT/PATCH `/v3/events/{id}`.
- `RegistrationManagementService.CreateRegistrationAsync` emits when
the filling registration triggers the auto-flip to
`RegistrationsClosed`. The message includes `(auto: reached
MaxParticipants N)` so consumers can distinguish from manual changes.
The actor user UUID comes from `IHttpContextAccessor.HttpContext?.User?
.GetUserId()` — same pattern as the existing
`registration.status.changed` emission. Null for background-job paths.
New `BusinessEventSubjects.ForEvent(Guid)` helper next to `ForOrder` /
`ForRegistration` / `ForUser`.
Tests:
- `EventManagementServiceTests.UpdateEventAsync_EmitsStatusChangedEvent_WhenStatusChanges`
- `EventManagementServiceTests.UpdateEventAsync_DoesNotEmit_WhenStatusUnchanged`
- `RegistrationManagementServiceTests.CreateRegistrationAsync_EmitsBusinessEvent_WhenEventAutoCloses`
- `RegistrationManagementServiceTests.CreateRegistrationAsync_DoesNotEmitBusinessEvent_WhenStatusDoesNotChange`
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4636b17 to
13f387d
Compare
|



Stacked on #1462 — base retargets to
mainautomatically when that lands.Summary
BusinessEventSubjects.ForEvent(Guid)helper.EventManagementService.UpdateEventAsyncnow snapshots pre-update status (AsNoTracking) and emitsevent.status.changedon any delta. Covers PUT/PATCH/v3/events/{id}.RegistrationManagementService.CreateRegistrationAsyncemits the same event type when its filling-the-last-spot auto-flip changes the status. Message tags the auto-close path as(auto: reached MaxParticipants N)so consumers can distinguish from operator actions.HttpContext(GetUserId()), same pattern as the existingregistration.status.changedemission. Null for background paths.Why
The audit trail already covers
registration.status.changed/registration.type.changed/ order events, but had no signal for event-level state. Auto-closing an event when it fills up is a meaningful state change that downstream consumers (webhooks, analytics, oncall) should be able to see. Adding emission for manual changes in the same PR keeps the audit trail symmetric — same shape regardless of trigger.Tests
Unit tests covering both sites:
EventManagementServiceTests.UpdateEventAsync_EmitsStatusChangedEvent_WhenStatusChangesEventManagementServiceTests.UpdateEventAsync_DoesNotEmit_WhenStatusUnchangedRegistrationManagementServiceTests.CreateRegistrationAsync_EmitsBusinessEvent_WhenEventAutoClosesRegistrationManagementServiceTests.CreateRegistrationAsync_DoesNotEmitBusinessEvent_WhenStatusDoesNotChangeFull integration suite (744 tests, 742 passing, 2 skipped pre-existing) runs green locally.
Test plan
/v3/events/{id}with a differentstatus— Sentry/audit log showsevent.status.changedkeyed by event UUID with the acting admin's UUID.status— no business event emitted.🤖 Generated with Claude Code