Reporting is a read-side projection module, not a general-purpose BI engine. It owns two denormalized Mart tables: Rpt_TicketSummary stores the current ticket snapshot, while Rpt_AuditActivityDaily stores daily counts by tenant, date, user, and action. Its public business surface consists of two RFC 10008 QUERY endpoints and one asynchronous ticket-summary export submission endpoint.
1. Implemented scope
| Capability | Current implementation |
|---|---|
| Ticket summary query | QUERY /api/reports/tickets/summary; POST fallback at /api/reports/tickets/summary/_query |
| Audit activity query | QUERY /api/reports/activities/daily; POST fallback at /api/reports/activities/daily/_query |
| Ticket summary export | POST /api/reports/tickets/summary/export; csv and excel only; always asynchronous |
| Permissions | .read for queries and .export for export submission |
| Feature | The runtime maps resource module reporting to platform.reporting |
| Data scope | Each handler evaluates authorization again and accepts only DataScope.Tenant |
| Ticket projection | Subscribes to opened, assigned, started, resolved, closed, and reopened |
| Storage | Two owner-local Mart rows accessed through IEntitySet<T> and Query Shape |
| Host without persistence | An unavailable IReportingMartStore fails closed instead of returning fabricated empty data |
The module does not contain a report designer, chart/dashboard engine, cross-tenant analytics, an audit-daily aggregation job, projection checkpoint, inbox, versioned replay, reconciliation, shadow rebuild, freshness API, or Mart lifecycle cleanup.
2. Runtime flow
Business handlers do not manually publish Ticket*IntegrationEvent DTOs. Ticket domain events also implement IIntegrationEvent and carry [IntegrationTopic]. The source generator builds the topic and scalar-payload mapping, and the command transaction writes that message to the CAP Outbox before commit. Reporting’s Ticket*IntegrationEvent records are consumer-binding views of that wire shape.
3. Query protocol
List and paginated reads do not have GET aliases. Clients should send a JSON-body QUERY request. When a proxy or client tool cannot send QUERY, use the POST fallback with the same body.
| Report | Request fields | Response |
|---|---|---|
| Ticket summary | status?, from?, to?, pageIndex=1, pageSize=20 | PagedResult<TicketSummaryRow> |
| Audit daily | required from and to, plus pageIndex=1, pageSize=20 | PagedResult<AuditActivityDailyRow> |
QUERY /api/reports/tickets/summary HTTP/1.1Authorization: Bearer eyJhbGciOiJSUzI1NiJ9Content-Type: application/jsonAccept: application/json
{ "status": "Closed", "from": "2026-07-01T00:00:00Z", "to": "2026-07-31T23:59:59Z", "pageIndex": 1, "pageSize": 50}Only method and path change for the compatibility request:
POST /api/reports/tickets/summary/_query HTTP/1.1Authorization: Bearer eyJhbGciOiJSUzI1NiJ9Content-Type: application/json
{"status":"Closed","from":"2026-07-01T00:00:00Z","to":"2026-07-31T23:59:59Z","pageIndex":1,"pageSize":50}Request rules are explicit:
pageIndexmust be at least 1;pageSizemust be between 1 and 1000;- status is trimmed, may contain at most 64 characters, and may not contain control characters; it is not currently resolved against
TicketStatus; - from must not be later than to, and the span may not exceed 366 days;
- both boundaries are inclusive; ticket queries filter
OpenedAt, while audit queries filterActivityDate; - ticket ranges use
DateTimeOffsetbut are not forced to UTC; audit ranges useDateTimewithout a stricter Kind contract.
4. Permission, Feature, and data scope
| Entry point | Resource and action | Derived permission |
|---|---|---|
| Ticket summary query | reporting/ticket-summary + Read | reporting.ticket-summary.read |
| Audit daily query | reporting/activity + Read | reporting.activity.read |
| Ticket summary export | reporting/ticket-summary + Export | reporting.ticket-summary.export |
The shared Feature evaluator maps reporting to the central platform.reporting entitlement and returns Deny when it is disabled. Reporting also contributes an owner catalog definition named reporting.mart, but the query authorization chain does not evaluate that code. Deployment and plan configuration must therefore treat platform.reporting as the current runtime fact. The duplicate feature concepts remain a source-level consistency issue.
Query and export handlers require an authenticated user or delegated user, a positive stable UserId, and a valid TenantId. They then call IAuthorizationDecisionService; execution continues only when IsAllowed is true and DataScope == Tenant. Own, Department, Office, and other narrower scopes fail closed instead of becoming row predicates.
This avoids loading a tenant-wide data set and filtering it in memory, but it also means only tenant-scope users can use these reports. Public rows still expose Subject, RequesterId, AssigneeId, or per-user activity counts. No field masker is wired today.
5. Asynchronous ticket-summary export
POST /api/reports/tickets/summary/export HTTP/1.1Authorization: Bearer eyJhbGciOiJSUzI1NiJ9Content-Type: application/json
{ "status": "Closed", "from": "2026-07-01T00:00:00Z", "to": "2026-07-31T23:59:59Z", "format": "csv", "idempotencyKey": "reporting-202607-closed-v1"}Success returns { "jobId": "..." }. The implementation fixes the following boundaries:
- the trimmed idempotency key must contain 8–128 non-control characters; filters and format are part of the request fingerprint;
- the Builder Key is fixed as
reporting.ticket-summary, and the server owns the columns; - only
ExportScope.Allis accepted; client-selected CheckedIds are rejected; ForceAsync=true; raw Mart reads default to 5000 rows and cap at 100000, with a 1000-row authorization scan batch;- the builder calls the Tickets owner’s
ITicketResourceAuthorizationReaderfor every Ticket; a revoked row is omitted from later batches; - retry re-evaluates
.exportand tenant DataScope; - task display fields expose only normalized status/from/to, not the original parameter JSON.
History, status, download, cancellation, and retry are provided by the platform Export Center under /api/export/..., not by Reporting-specific routes.
6. Ticket projection
| Topic | Projection change |
|---|---|
ticket.opened | Create New with requester, subject, priority, and OpenedAt |
ticket.assigned | Set AssigneeId and Assigned |
ticket.started | Set InProgress |
ticket.resolved | Set Resolved |
ticket.closed | Set Closed and ClosedAt |
ticket.reopened | Set Reopened and clear ClosedAt |
The consumer skips an immediate duplicate of the last EventId and skips non-opened events whose OccurredAt is earlier than LastUpdatedAt. A missing opened predecessor, Mart read/write failure, or other processing exception is rethrown so CAP can retry. An integration test directly constructs all six consumer DTOs and verifies the lifecycle, direct duplicate, and stale-close cases.
Three important boundaries remain:
Ticket.OpencallsRaise(new TicketOpened(ticket.Id, ...))while Id is still"0"; the repository assigns the final ID later. Opened cannot join subsequent events by TicketId.- The Mart stores only the most recent EventId. It handles A,A but cannot recognize A,B,A, and events carry no aggregate version or sequence.
- Upsert is read-then-write without compare-and-set; equal timestamps and concurrent delivery can still be resolved by commit order.
See Ticket projection and event ordering.
7. Audit daily status
Rpt_AuditActivityDaily and UpsertAuditActivityDailyAsync exist, but production source has no consumer, JobHost executor, SQL aggregation service, checkpoint, or backfill command. The handler test returns a manually configured substitute row and proves mapping only.
ActivityDate is a full DateTime, and the store does not normalize it to UTC midnight. Upsert replaces the complete ActivityCount; it is not an atomic increment. Different times or DateTime kinds can therefore split one business day into multiple unique rows. See Audit activity daily aggregation.
8. Mart query and consistency semantics
Both standard lists use Query Shape and add a trusted TenantId predicate:
- tickets sort by
(OpenedAt desc, TicketId desc); - audit daily sorts by
(ActivityDate desc, UserId desc, ActionType desc); - From and To are inclusive;
- public DTOs omit Mart
LastEventIdandLastUpdatedAt, so clients cannot determine a watermark; - both Upserts use check-then-insert/update with no atomic database Upsert or version condition.
See Mart models, paging, and data scope.
9. Test evidence and production gaps
Existing evidence covers query-handler rules, Read actions, tenant DataScope, Feature mapping, export submission and row-level owner rechecks, the six-event consumer sequence, ORM-neutral Infrastructure, Mart metadata, and the API Shell unavailable adapter.
Still missing are a real Ticket-create-to-Outbox-to-CAP binding test, a final opened-ID guard, a dual-ORM ReportingMartStore behavior suite, version/inbox/gap semantics, concurrent Upsert tests, the audit-daily producer, real HTTP authorization coverage, projection watermarks, reconciliation, rebuild drills, and production-scale query plans.
See Testing, observability, rebuild, and GA for the acceptance checklist.
10. Source map
| Topic | Source entry |
|---|---|
| Queries | src/Platform/Reporting/...Application/Queries |
| Export submission and builder | ...Application/Commands/ExportTicketSummary, TicketSummaryExportBuilder.cs |
| Request and DataScope rules | ...Application/ReportingApplicationRules.cs |
| Permissions and owner Feature | ReportingConstants.cs, ReportingFeatures.cs |
| Runtime Feature mapping | src/Framework/BitzOrcas.Application/Authorization/Feature/FeaturePolicyEvaluator.cs |
| Ticket consumer | ...Reporting.Infrastructure/TicketReportingEventConsumer.cs |
| Mart Store and Query Shape | ...Reporting.Infrastructure/ReportingMartStore.cs |
| Ticket publisher events | src/Platform/Tickets/...Contracts/Tickets/Events/Ticket*.cs |
| Audit daily row | ...Reporting.Infrastructure/Persistence/RptAuditActivityDailyRecord.cs |
Back to platform modules · Tickets · Auditing · Authorization