Owns tenant announcements from draft through publication, withdrawal, and archive, including audience targeting and idempotent per-user read state.
Primary path at a glance
The coral node marks the module’s central decision or state boundary.
Capability boundary
- Draft, publish, withdraw, and archive transitions
- All-user, role, and department audience filters
- Unique per-user read facts and management read counts
The module explicitly does not own the following: Announcements decides announcement lifecycle and visibility. Identity and organization membership remain authoritative for the current user, roles, and departments; Notifications remains the owner of out-of-band delivery.
Code map
| Project | Primary responsibility |
|---|---|
BitzOrcas.Platform.Announcements.Contracts | Public contracts, ports, DTOs, and events |
BitzOrcas.Platform.Announcements.Application | Commands, queries, handlers, and application policy |
BitzOrcas.Platform.Announcements.Infrastructure | Persistence, connectors, and framework adapters |
Source location: src/Platform/Announcements
Core use cases
CreateAnnouncementCommandUpdateAnnouncementCommandPublishAnnouncementCommandWithdrawAnnouncementCommandArchiveAnnouncementCommandListMyAnnouncementsQueryMarkAnnouncementReadCommand
When reading a use case, start with its request contract and permission declaration, follow how the handler coordinates an aggregate or port, then inspect post-commit events, cache changes, and audit.
Key models and contracts
| Key type | What to inspect |
|---|---|
AnnouncementDto | Management lifecycle projection |
AnnouncementView | Audience-filtered user projection |
IAnnouncementStore | Lifecycle and visibility store port |
IAnnouncementReadStore | Idempotent read-state port |
Use-case boundary example
| Field | Value |
|---|---|
| Representative contract | PublishAnnouncementCommand |
| Result | Result<AnnouncementDto> |
| Review focus | Publish only a draft, validate the effective window against the server clock, and persist the new concurrency version before it becomes visible. |
Pseudocode
var existing = await store.FindByIdAsync(command.AnnouncementId, cancellationToken);// ① Publish is legal only from Draft and every decision uses the server clock.if (existing is null) return AnnouncementErrors.NotFound;if (existing.Status != AnnouncementStatus.Draft.Name) return AnnouncementErrors.InvalidTransition;var now = clock.UtcNow;var effectiveAt = existing.EffectiveAt ?? now;// ② A window that expired or starts after expiry must be rejected.if (existing.ExpiresAt is not null && (existing.ExpiresAt <= effectiveAt || existing.ExpiresAt <= now)) return AnnouncementErrors.InvalidEffectiveWindow;// ③ The transition builds a new snapshot via a record with-expression; only a saved// snapshot becomes visible to user-facing queries.var published = existing with{ Status = AnnouncementStatus.Published.Name, PublishedAt = now, EffectiveAt = effectiveAt, WithdrawnAt = null,};return await store.SaveAsync(published, cancellationToken);Integration relationships
The user-facing query resolves trusted department IDs through the organization-member repository and combines them with the current user’s roles. The module does not accept department membership from request input.
Cross-module collaboration follows these directions:
- Callers depend on this module’s public Contracts or a narrow port.
- State changes propagate through versioned integration events.
- Database, messaging, cache, file, and vendor SDK details stay in Infrastructure.
- The composition root registers fail-closed defaults before production adapters.
Security, tenancy, and privacy
Management routes require announcement permissions and the default-off announcements.manage feature. User routes derive tenant, user, roles, and departments from trusted context and fail closed when the announcement is outside the audience or active window.
Every tenant-scoped write persists TenantId, and list/detail paths share one authorization boundary. Logs, events, and audit retain only the minimum fields needed for diagnosis.
Failure semantics, idempotency, and observability
Draft updates use an explicit concurrency version. Published content is immutable, withdrawal takes effect immediately, archive is terminal, and the read store must enforce one read fact per tenant, user, and announcement.
| Situation | Required behavior |
|---|---|
| Validation, not found, conflict, or forbidden | Return typed Result/Error data and let the shared mapper produce Problem Details |
| Adapter or critical state unavailable | Fail closed for security/high-value work; mark read-only degradation explicitly |
| Duplicate request or event | Use a stable idempotency key with a database uniqueness constraint as the final guard |
| External timeout | Propagate cancellation, record redacted diagnostics, and retry only safe operations with bounds |
Test matrix
| # | Required scenario | Suggested level |
|---|---|---|
| 1 | legal lifecycle and stale concurrency version | unit + integration |
| 2 | role/department audience filtering fails closed | integration/contract |
| 3 | effective, expiry, and server-clock boundaries | integration/contract |
| 4 | idempotent reads and management read count | integration/contract |
Keep one universal red line under test: no identifier, cache key, event, or query condition from tenant A may let tenant B read or mutate data.
Source navigation and change checks
Run from the BitzOrcasVNext repository root:
# List public types and confirm that new contracts belong to the module boundary.rg -n "^public (sealed |abstract |static |partial )*(record|class|interface|enum)" src/Platform/Announcements -g '*.cs'
# Inspect cross-layer references; callers must not depend on another module's Infrastructure.rg -n "ProjectReference|PackageReference" src/Platform/Announcements -g '*.csproj'
# Find unfinished or elided implementation; expect no matches.rg -n "TODO|FIXME|// \.\.\." src/Platform/Announcements -g '*.cs'Extension and release checklist
- Is the new capability expressed through explicit Contracts, permissions, and a feature key?
- Can persistence, connectors, and providers be replaced through narrow adapters?
- Does production still resolve a Null/Unavailable default, and could health checks report a false green?
- Is there evidence for transaction, event, cache-invalidation, audit, and background-job failure/retry behavior?
- Did bilingual docs, diagrams, contract tests, and runbooks change with the code?
Back to module catalog · Module dependency diagram · Add a module guide