Skip to content

Epic saas-EPIC-MET — Metering gap implementation

Epic ID: saas-EPIC-MET
Repository: ConnectSoft.Saas.MeteringTemplate
Aggregate root: UsageMeter
Last verified: 2026-05-27
Source analysis: saas-gap-deep-analysis.md

Epic summary

Close evidence-based gaps between the Metering template and the canonical DDD blueprint, cross-repo published language, and E2E integration path. Priority P0 items (saas-MET-F03, F04) block quota enforcement and Billing consumer wiring until inbound topology and publisher-side Dimension field align with Billing's consumer-side MeterKey expectation (resolved jointly with saas-BIL-F02 and saas-INTEG-F02). DefaultUsageMetersProcessor currently uses flat counter semantics without UsageRecord idempotency or Window VO roll model.

Rollup (from gap analysis): 9 Implemented · 8 Partial · 8 Missing · 1 Deferred


Feature index

Order ID Title
060 saas-MET-F01 UsageRecord value object and idempotency
061 saas-MET-F02 Window value object and roll semantics
062 saas-MET-F03 UsageReportedForQuota inbound topology
063 saas-MET-F04 Quota contract alignment (Dimension vs MeterKey)
064 saas-MET-F05 Orleans grain write-path wiring
065 saas-MET-F06 Dead code and misnamed surrogate removal
066 saas-MET-F07 Processor, saga, and aggregate tests
067 saas-MET-F08 Optional Entitlements reaction registration
068 saas-MET-F09 Architecture test enforcement

[060] saas-MET-F01 — UsageRecord value object and idempotency

Type: Feature
Parent: saas-EPIC-MET
Implementation order: 060
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, idempotency, P1
Priority: P1
Effort: L
Dependencies:
Blocks: saas-MET-F07
Source gap analysis: metering-analysis · Finding M-001

Description (full):

Blueprint requires UsageRecord VO with fields such as OccurredAtUtc, Quantity, Source, IdempotencyKey, and append-only usage log semantics. Current ingest path: RecordUsageInput / RecordUsageRequest with TenantId + Dimension + Delta only. DefaultUsageMetersProcessor.RecordUsageAsync always increments CounterValue, bumps AggregateVersion, publishes UsageRecordedIntegrationEvent — no idempotency key, no dedupe store, no usage record history.

Published language rule #6: at-least-once delivery requires (tenantId, dimension, idempotencyKey) dedupe so replays do not double-count. JSON descriptor ConnectSoft.Saas.Metering.json has "valueObjects": [].

Acceptance criteria (testable):

  • AC-1: UsageRecord VO defined in EntityModel with idempotency key and metadata fields.
  • AC-2: RecordUsageAsync accepts idempotency key on input and request DTOs.
  • AC-3: Duplicate (tenantId, dimension, idempotencyKey) returns success without double increment or duplicate event publish.
  • AC-4: Dedupe store implemented (NHibernate table, Redis, or aggregate collection with documented limits).
  • AC-5: JSON descriptor lists UsageRecord in valueObjects.
  • AC-6: Unit tests prove replay safety.

Implementation notes (full):

  • Files: RecordUsageInput.cs, UsageMeterContracts.cs (RecordUsageRequest), DefaultUsageMetersProcessor.cs, UsageRecordedIntegrationEvent.cs, EntityModel, mappings
  • ADR: docs/adr/0001-one-aggregate-root-per-repo.md may note VO addition
  • Tests: replace placeholder UsageMeterAggregateTests.cs

Out of scope:

  • External usage ingestion API gateway (platform)
  • Billing rating-window trigger (BIL-F07)

Definition of done:

  • All AC pass
  • Tests green
  • Descriptor updated

saas-MET-S01.1 — As a metering operator, I need idempotent usage ingestion so at-least-once delivery does not inflate counters

Type: User Story
Parent: saas-MET-F01
Implementation order: 060.1
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, idempotency
Priority: P1
Effort: L
Dependencies:
Blocks:
Source gap analysis: metering-analysis

Description (full):

Implement UsageRecord-backed idempotent record path in DefaultUsageMetersProcessor and expose idempotency key on public ServiceModel contract.

Acceptance criteria (testable):

  • AC-1: REST/gRPC record usage accepts optional/required idempotency key per API design.
  • AC-2: Second call with same key is no-op for counter and events.
  • AC-3: Different keys same dimension accumulate correctly.

Implementation notes (full):

  • Processor: RecordUsageAsync
  • Public API: UsageMetersController, GrpcUsageMeterManagementService

Out of scope: Cross-dimension idempotency

Definition of done:

  • All AC pass

saas-MET-T01.1.1 — Add UsageRecord VO and extend RecordUsageInput/Request

Type: Task
Parent: saas-MET-S01.1
Implementation order: 060.1.1
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, entity
Priority: P1
Effort: M
Dependencies:
Blocks: saas-MET-T01.1.2
Source gap analysis: metering-analysis

Description (full):

Create UsageRecord value object and embed or associate with UsageMeterEntity. Add IdempotencyKey to domain input and ServiceModel request. Update NHibernate mapping and JSON descriptor.

Acceptance criteria (testable):

  • AC-1: VO types compile and map to persistence.
  • AC-2: API contracts include idempotency key field.

Implementation notes (full):

  • EntityModel, PersistenceModel.NHibernate, ServiceModel
  • Reference canonical DDD entities doc for field list

Out of scope: Processor dedupe logic

Definition of done:

  • All AC pass

saas-MET-T01.1.2 — Implement dedupe in DefaultUsageMetersProcessor.RecordUsageAsync

Type: Task
Parent: saas-MET-S01.1
Implementation order: 060.1.2
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, processor
Priority: P1
Effort: M
Dependencies: saas-MET-T01.1.1
Blocks:
Source gap analysis: metering-analysis

Description (full):

Before incrementing counter, check dedupe index for (tenantId, dimension, idempotencyKey). On duplicate, return existing result without publish. On new key, append UsageRecord, increment, publish UsageRecordedIntegrationEvent.

Acceptance criteria (testable):

  • AC-1: Unit test: two identical keys → single increment.
  • AC-2: Unit test: two different keys → counter sum correct.
  • AC-3: Event published once per unique key.

Implementation notes (full):

  • Symbol: DefaultUsageMetersProcessor.RecordUsageAsync
  • Consider saga replay calling same processor

Out of scope: Redis dedupe (unless chosen over DB)

Definition of done:

  • All AC pass
  • Tests green

[061] saas-MET-F02 — Window value object and roll semantics

Type: Feature
Parent: saas-EPIC-MET
Implementation order: 061
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, window, P1
Priority: P1
Effort: L
Dependencies: saas-BIL-F07
Blocks:
Source gap analysis: metering-analysis

Description (full):

Blueprint defines Window VO (Fixed/Rolling, size, anchor) and per-window counters. Current code: single scalar CounterValue per tenant+dimension; "window" approximated by boolean ThresholdCrossedEmitted / QuotaExceededEmitted flags reset on roll in RollUsageCounterInnerAsync. Roll is manual/API-driven, not tied to billing period boundaries. Events lack windowId.

Replace flat counter model with Window VO semantics: non-decreasing counters within open window, ResetWindow, CounterRolledIntegrationEvent tied to window boundaries. Optional inbound reaction to Billing rating-window event (BIL-F07).

Acceptance criteria (testable):

  • AC-1: Window VO in EntityModel with kind, size, anchor, windowId.
  • AC-2: Counter scoped to active window; roll closes window and opens new.
  • AC-3: RollUsageCounterAsync uses Window semantics not bare flag reset.
  • AC-4: Outbound events include windowId where applicable.
  • AC-5: Unit tests for roll invariants and non-decreasing counter within window.

Implementation notes (full):

  • Files: IUsageMeter.cs, UsageMeterEntity.cs, DefaultUsageMetersProcessor.cs (RollUsageCounterInnerAsync, PrepareQuotaTransitions)
  • Cross-service: BIL-F07 rating-window event as optional roll trigger
  • JSON descriptor valueObjects

Out of scope:

  • Multi-dimensional window aggregation across tenants

Definition of done:

  • All AC pass
  • Tests green

saas-MET-S02.1 — As a billing-period consumer, I need usage counters scoped to windows so quota resets align with subscription periods

Type: User Story
Parent: saas-MET-F02
Implementation order: 061.1
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, window
Priority: P1
Effort: L
Dependencies:
Blocks:
Source gap analysis: metering-analysis

Description (full):

Model Window on UsageMeter aggregate and refactor roll/quota logic to reference active window instead of boolean emission flags.

Acceptance criteria (testable):

  • AC-1: Active window identifiable on aggregate.
  • AC-2: Roll produces new windowId and zeros counter for new window only.
  • AC-3: Quota flags scoped per window cycle.

Implementation notes (full):

  • Processor methods: RollUsageCounterInnerAsync, PrepareQuotaTransitions

Out of scope: Billing event consumer (optional hook)

Definition of done:

  • All AC pass

saas-MET-T02.1.1 — Introduce Window VO and persist on UsageMeterEntity

Type: Task
Parent: saas-MET-S02.1
Implementation order: 061.1.1
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, entity
Priority: P1
Effort: M
Dependencies:
Blocks: saas-MET-T02.1.2
Source gap analysis: metering-analysis

Description (full):

Add Window value object (or embedded columns) to usage meter entity with NHibernate mapping. Migrate existing counter data to initial window on upgrade script.

Acceptance criteria (testable):

  • AC-1: Window fields persisted.
  • AC-2: Migration or default window for existing rows documented.

Implementation notes (full):

  • EntityModel, PersistenceModel.NHibernate, DatabaseModel.Migrations

Out of scope: Roll processor refactor

Definition of done:

  • All AC pass

saas-MET-T02.1.2 — Refactor DefaultUsageMetersProcessor roll and quota for Window semantics

Type: Task
Parent: saas-MET-S02.1
Implementation order: 061.1.2
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, processor
Priority: P1
Effort: M
Dependencies: saas-MET-T02.1.1
Blocks:
Source gap analysis: metering-analysis

Description (full):

Replace boolean flag reset pattern with window close/open in RollUsageCounterInnerAsync. Update PrepareQuotaTransitions to emit threshold/quota events with windowId. Publish CounterRolledIntegrationEvent with window metadata.

Acceptance criteria (testable):

  • AC-1: Roll changes windowId.
  • AC-2: Quota emission flags reset on new window only.
  • AC-3: Unit tests for roll + quota sequence.

Implementation notes (full):

  • DefaultUsageMetersProcessor.cs

Out of scope: Rating-window inbound saga

Definition of done:

  • All AC pass
  • Tests green

[062] saas-MET-F03 — UsageReportedForQuota inbound topology

Type: Feature
Parent: saas-EPIC-MET
Implementation order: 062
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, masstransit, P0
Priority: P0
Effort: M
Dependencies: saas-INTEG-F01
Blocks:
Source gap analysis: metering-analysis · Finding M-002

Description (full):

UsageReportedForQuotaInboundEvent and UsageMeterQuotaEnforcementStateMachine exist — saga handles UsageReported and calls RecordUsageAsync. Inbound topology gap: MeteringMassTransitTopology.ConfigureInboundConsumedMessageTopology registers only TenantActivatedMeteringInboundEvent, SubscriptionCreatedMeteringInboundEvent, EntitlementsChangedQuotaInboundEventnot UsageReportedForQuotaInboundEvent. No topic constant in UsageMetersConstants.InboundEventTopics / MeteringConstants.

Cross-repo publishers cannot bind to the saga consumer without matching SetEntityName. Gap analysis: "Event + saga exist, no SetEntityName."

Acceptance criteria (testable):

  • AC-1: Inbound topic constant added to MeteringConstants/UsageMetersConstants.
  • AC-2: Topology registers SetEntityName for usage-reported message matching saga consumer.
  • AC-3: JSON descriptor consumedEvents includes usage-reported inbound event.
  • AC-4: Integration test: publish usage-reported message → saga → processor record invoked.
  • AC-5: Topic aligns with published language canonical name.

Implementation notes (full):

  • Files: UsageReportedForQuotaInboundEvent.cs, UsageMeterQuotaEnforcementStateMachine.cs, MeteringMassTransitTopology.cs, MassTransitExtensions.cs, constants
  • Coordinate topic name with platform published language

Out of scope:

  • External publisher implementation (other services)

Definition of done:

  • All AC pass
  • Topology verified

saas-MET-S03.1 — As an upstream usage reporter, I need a registered inbound topic so usage-reported messages reach the quota enforcement saga

Type: User Story
Parent: saas-MET-F03
Implementation order: 062.1
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, masstransit
Priority: P0
Effort: M
Dependencies: saas-INTEG-F01
Blocks:
Source gap analysis: metering-analysis

Description (full):

Wire missing inbound topology so existing saga is reachable on the bus.

Acceptance criteria (testable):

  • AC-1: Saga consumer endpoint binds to canonical entity name.
  • AC-2: Manual test message consumed successfully.

Implementation notes (full):

  • Pattern: other three inbound events in same topology class

Out of scope: Saga logic changes

Definition of done:

  • All AC pass

saas-MET-T03.1.1 — Add UsageReported inbound topic constant and descriptor entry

Type: Task
Parent: saas-MET-S03.1
Implementation order: 062.1.1
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, constants
Priority: P0
Effort: S
Dependencies:
Blocks: saas-MET-T03.1.2
Source gap analysis: metering-analysis

Description (full):

Add constant to MeteringConstants.InboundEventTopics (or UsageMetersConstants). Update ConnectSoft.Saas.Metering.json consumedEvents.

Acceptance criteria (testable):

  • AC-1: Constant value matches published language.
  • AC-2: Descriptor synchronized.

Implementation notes (full):

  • Constants file, JSON descriptor

Out of scope: Topology code

Definition of done:

  • All AC pass

saas-MET-T03.1.2 — Register SetEntityName for UsageReportedForQuota in MeteringMassTransitTopology

Type: Task
Parent: saas-MET-S03.1
Implementation order: 062.1.2
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, topology
Priority: P0
Effort: S
Dependencies: saas-MET-T03.1.1
Blocks:
Source gap analysis: metering-analysis

Description (full):

Extend ConfigureInboundConsumedMessageTopology to include UsageReportedForQuotaInboundEvent with entity name from constant. Verify saga state machine consumer configuration matches.

Acceptance criteria (testable):

  • AC-1: Topology configuration includes fourth inbound event.
  • AC-2: Saga test or harness receives message type.

Implementation notes (full):

  • MeteringMassTransitTopology.cs, UsageMeterQuotaEnforcementStateMachine.cs

Out of scope: F01 idempotency in saga path (follow-up)

Definition of done:

  • All AC pass

[063] saas-MET-F04 — Quota contract alignment (Dimension vs MeterKey)

Type: Feature
Parent: saas-EPIC-MET
Implementation order: 063
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, quota, P0
Priority: P0
Effort: M
Dependencies: saas-INTEG-F02
Blocks: saas-BIL-F06
Source gap analysis: cross-quota-payload

Description (full):

Publisher-side (Metering) vs consumer-side (Billing) contract mismatch. Metering publishes QuotaExceededIntegrationEvent and QuotaThresholdCrossedIntegrationEvent with property Dimension. Billing inbound DTO MeteringQuotaExceededInboundIntegrationEvent expects MeterKey. MassTransit JSON deserialization will not map across different property names without explicit mapping.

Metering topic string on publish side is aligned: MeteringConstants.EventTopics.QuotaExceeded = metering.quota.v1.quota-exceeded. Billing topic drift is separate (BIL-F02). This feature fixes payload field name on publisher side (or dual-publish alias) in coordination with Billing consumer fix.

Acceptance criteria (testable):

  • AC-1: Canonical field name agreed and documented (Dimension or MeterKey per INTEG-F02).
  • AC-2: QuotaExceededIntegrationEvent and threshold event use canonical property; obsolete name removed or aliased with deprecation.
  • AC-3: DefaultUsageMetersProcessor.PrepareQuotaTransitions populates canonical field.
  • AC-4: Cross-repo contract test: Metering publish → Billing saga deserializes meter identity (with BIL-F02).
  • AC-5: CrossRepoPublishedLanguageTests (F09) includes quota payload rule when implemented.

Implementation notes (full):

  • Files: QuotaExceededIntegrationEvent.cs, QuotaThresholdCrossedIntegrationEvent.cs, DefaultUsageMetersProcessor.cs
  • Billing peer: MeteringQuotaExceededInboundIntegrationEvent.cs (saas-BIL-F02)
  • Prefer single name in published language glossary

Out of scope:

  • Billing inbound topic strings (BIL-F02)

Definition of done:

  • All AC pass
  • E2E quota path with Billing verified

saas-MET-S04.1 — As Billing service (consumer), I need quota events with a field name I can deserialize so suspend-on-quota works E2E

Type: User Story
Parent: saas-MET-F04
Implementation order: 063.1
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, billing, quota
Priority: P0
Effort: M
Dependencies: saas-INTEG-F02
Blocks: saas-BIL-F06
Source gap analysis: cross-quota-payload

Description (full):

Align Metering outbound quota event payload with Billing consumer DTO. Metering is the publisher; Billing is the consumer — both must agree on Dimension vs MeterKey.

Acceptance criteria (testable):

  • AC-1: Published JSON sample matches Billing inbound DTO shape.
  • AC-2: Billing ReactToQuotaExceededAsync receives non-null meter identity in E2E test.

Implementation notes (full):

  • Coordinate merge order with BIL-F02 T02.1.2
  • Update published language doc table if needed

Out of scope: Entitlements quota reactions

Definition of done:

  • All AC pass

saas-MET-T04.1.1 — Rename or alias Dimension to canonical MeterKey/Dimension on quota events

Type: Task
Parent: saas-MET-S04.1
Implementation order: 063.1.1
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, messaging
Priority: P0
Effort: S
Dependencies: saas-INTEG-F02
Blocks: saas-MET-T04.1.2
Source gap analysis: cross-quota-payload

Description (full):

Apply agreed field name to QuotaExceededIntegrationEvent and threshold event. Update processor mapping in PrepareQuotaTransitions. If renaming to MeterKey, update internal Dimension references for outbound-only clarity or keep Dimension as domain term with JSON property alias.

Acceptance criteria (testable):

  • AC-1: Event serialization sample reviewed against Billing DTO.
  • AC-2: No breaking unpublished consumers within template solution.

Implementation notes (full):

  • MessagingModel/Events/
  • System.Text.Json or Newtonsoft attributes if alias approach

Out of scope: Billing changes

Definition of done:

  • All AC pass

saas-MET-T04.1.2 — Add cross-repo quota payload contract test

Type: Task
Parent: saas-MET-S04.1
Implementation order: 063.1.2
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, tests
Priority: P0
Effort: S
Dependencies: saas-MET-T04.1.1, saas-BIL-F02
Blocks:
Source gap analysis: cross-quota-payload

Description (full):

Add integration or contract test that serializes Metering quota event and deserializes into Billing inbound type (test project reference or shared test package). Prevents Dimension/MeterKey regression.

Acceptance criteria (testable):

  • AC-1: Test fails if property names diverge.
  • AC-2: Test runs in CI.

Implementation notes (full):

  • ArchitectureTests or new ContractTests project
  • May reference Billing MessagingModel as test-only dependency

Out of scope: Full MassTransit bus test

Definition of done:

  • All AC pass
  • CI green

[064] saas-MET-F05 — Orleans grain write-path wiring

Type: Feature
Parent: saas-EPIC-MET
Implementation order: 064
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, orleans, P1
Priority: P1
Effort: M
Dependencies: saas-INTEG-F06
Blocks: saas-MET-F09
Source gap analysis: cross-orleans

Description (full):

UsageMeterGrain implements grain interface, enforces composite key tenantId:dimension via UsageMeterGrainKey, delegates to DefaultUsageMetersProcessor. All write APIs (RecordUsage, RollCounter, EnsureProvisioned, ApplyQuota) in UsageMetersController and GrpcUsageMeterManagementService call IUsageMetersProcessor directly — no GetGrain<IUsageMeterGrain>. Sagas also use processor directly. Orleans host wired; grain unused on hot path.

Per INTEG-F06 / ADR-INTEG-001, route writes through grain for partition affinity and single-writer semantics.

Acceptance criteria (testable):

  • AC-1: REST/gRPC writes resolve IUsageMeterGrain with composite key.
  • AC-2: Sagas invoke grain or documented exception with ADR if sagas stay processor-direct.
  • AC-3: Grain still delegates to processor (no duplicated logic).
  • AC-4: Enable MeteringOrleansGrainPartitionTests (F09/F07) with real assertions.

Implementation notes (full):

  • Files: UsageMeterGrain.cs, UsageMetersController.cs, GrpcUsageMeterManagementService.cs, OrleansExtensions.cs, saga classes in FlowModel.MassTransit
  • Key: UsageMeterGrainKey.Format / EnsureMatchesGrainKey

Out of scope:

  • Read-only query grain caching

Definition of done:

  • All AC pass
  • Grain partition test enabled

saas-MET-S05.1 — As a platform architect, I need Metering writes routed through UsageMeterGrain per actor model

Type: User Story
Parent: saas-MET-F05
Implementation order: 064.1
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, orleans
Priority: P1
Effort: M
Dependencies: saas-INTEG-F06
Blocks:
Source gap analysis: cross-orleans

Description (full):

Refactor adapters to use IGrainFactory and composite grain key for all mutating operations.

Acceptance criteria (testable):

  • AC-1: No direct processor injection in write controller methods.
  • AC-2: Grain key validation enforced on record usage.

Implementation notes (full):

  • Pattern: Billing F10, Tenants F08

Out of scope: Processor internal refactor

Definition of done:

  • All AC pass

saas-MET-T05.1.1 — Refactor UsageMetersController and gRPC service to use IUsageMeterGrain

Type: Task
Parent: saas-MET-S05.1
Implementation order: 064.1.1
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, api, orleans
Priority: P1
Effort: M
Dependencies: saas-INTEG-F06
Blocks: saas-MET-T05.1.2
Source gap analysis: cross-orleans

Description (full):

Replace processor calls with grain factory resolution using tenantId:dimension key on RecordUsage, RollCounter, EnsureProvisioned, ApplyQuota endpoints.

Acceptance criteria (testable):

  • AC-1: All four write operations use grain.
  • AC-2: Layering tests pass.

Implementation notes (full):

  • ServiceModel.RestApi/UsageMetersController.cs
  • ServiceModel.Grpc/GrpcUsageMeterManagementService.cs

Out of scope: Saga grain routing

Definition of done:

  • All AC pass

saas-MET-T05.1.2 — Route saga write calls through grain or document processor-direct ADR exception

Type: Task
Parent: saas-MET-S05.1
Implementation order: 064.1.2
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, saga, orleans
Priority: P1
Effort: S
Dependencies: saas-MET-T05.1.1
Blocks:
Source gap analysis: cross-orleans

Description (full):

Update MassTransit sagas (UsageMeterQuotaEnforcementStateMachine, entitlement/quota sagas) to resolve grain before record/quota operations, OR add ADR note if sagas intentionally bypass grain for durability reasons (must align with INTEG-F06 decision).

Acceptance criteria (testable):

  • AC-1: Decision documented in code or ADR.
  • AC-2: Saga integration test passes with chosen path.

Implementation notes (full):

  • FlowModel.MassTransit state machines
  • Inject IGrainFactory into saga definition if needed

Out of scope: In-memory saga repo → durable outbox (INTEG-F05)

Definition of done:

  • All AC pass

[065] saas-MET-F06 — Dead code and misnamed surrogate removal

Type: Feature
Parent: saas-EPIC-MET
Implementation order: 065
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, cleanup, P1
Priority: P1
Effort: M
Dependencies:
Blocks:
Source gap analysis: metering-analysis · Finding M-003

Description (full):

Orphan domain inputs from Entitlements scaffold not on IUsageMetersProcessor:

File Actual type Status
AssignEditionInput.cs SetQuotaInput Orphan
ActivateUsageMeterInput.cs RollCounterInput Orphan
OverrideTenantFeatureInput.cs ResetCounterInput Orphan
CatalogProductRetiredReactionInput.cs same Orphan

Live API uses RollUsageCounterInput, ApplyUsageQuotaInput. Orleans surrogates misnamed: e.g. AssignEditionInputSurrogate.csSetQuotaInputSurrogate. MeteringMetrics.cs has RecordCatalogProductRetiredReaction* for unused input. build/scaffold-replacements-metering.ps1 documents copy-paste origin.

Remove dead surface or wire to processor with saga if product-required.

Acceptance criteria (testable):

  • AC-1: No orphan input types without processor method or explicit removal.
  • AC-2: Surrogate filenames match domain type names.
  • AC-3: Unused metrics helpers removed or wired.
  • AC-4: scaffold-replacements-metering.ps1 updated or removed.
  • AC-5: Build and architecture tests pass after deletion.

Implementation notes (full):

  • DomainModel inputs, ActorModel.Orleans/Surrogates, Metrics/MeteringMetrics.cs
  • Grep for references before delete

Out of scope:

  • Catalog product retired saga (unless explicitly in scope)

Definition of done:

  • All AC pass
  • No dead public API surface

saas-MET-S06.1 — As a maintainer, I need Metering free of Entitlements scaffold orphans so the domain surface matches IUsageMetersProcessor

Type: User Story
Parent: saas-MET-F06
Implementation order: 065.1
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, cleanup
Priority: P1
Effort: M
Dependencies:
Blocks:
Source gap analysis: metering-analysis

Description (full):

Audit and remove or implement orphan inputs and misnamed surrogates.

Acceptance criteria (testable):

  • AC-1: DomainModel input count matches processor public methods.
  • AC-2: Surrogate folder naming consistent.

Implementation notes (full):

  • Compare with IUsageMetersProcessor interface

Out of scope: New catalog retirement feature

Definition of done:

  • All AC pass

saas-MET-T06.1.1 — Delete orphan domain inputs and unused metrics

Type: Task
Parent: saas-MET-S06.1
Implementation order: 065.1.1
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, cleanup
Priority: P1
Effort: S
Dependencies:
Blocks: saas-MET-T06.1.2
Source gap analysis: metering-analysis

Description (full):

Remove SetQuotaInput, misnamed roll/reset inputs, CatalogProductRetiredReactionInput if no saga planned. Remove RecordCatalogProductRetiredReaction* from MeteringMetrics.

Acceptance criteria (testable):

  • AC-1: Solution builds with no references to removed types.
  • AC-2: Grep confirms removal.

Implementation notes (full):

  • DomainModel/, Metrics/MeteringMetrics.cs

Out of scope: Surrogate rename

Definition of done:

  • All AC pass

saas-MET-T06.1.2 — Rename Orleans surrogates to match live domain types

Type: Task
Parent: saas-MET-S06.1
Implementation order: 065.1.2
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, orleans
Priority: P1
Effort: S
Dependencies: saas-MET-T06.1.1
Blocks:
Source gap analysis: metering-analysis

Description (full):

Rename surrogate files and types to match RecordUsageInput, RollUsageCounterInput, ApplyUsageQuotaInput, etc. Update surrogate registration. Refresh scaffold script.

Acceptance criteria (testable):

  • AC-1: Filename/type alignment for all surrogates.
  • AC-2: Orleans serialization tests or smoke test pass.

Implementation notes (full):

  • ActorModel.Orleans/Surrogates/
  • build/scaffold-replacements-metering.ps1

Out of scope: New surrogates for removed types

Definition of done:

  • All AC pass

[066] saas-MET-F07 — Processor, saga, and aggregate tests

Type: Feature
Parent: saas-EPIC-MET
Implementation order: 066
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, tests, P1
Priority: P1
Effort: L
Dependencies: saas-MET-F01, saas-MET-F02, saas-MET-F03
Blocks:
Source gap analysis: metering-analysis

Description (full):

Test gap across Metering template:

Test Status
UsageMeterAggregateTests.cs Placeholder Assert.IsTrue(true)
MeteringDomainBoundaryTests.cs MeteringException only
MeteringOrleansGrainPartitionTests.cs [Ignore], empty
AcceptanceTests BaseTemplate welcome scaffold only

No processor, saga, idempotency, or quota transition tests. Domain aggregate invariants not verified.

Acceptance criteria (testable):

  • AC-1: UsageMeterAggregateTests covers record, roll, quota threshold/exceeded transitions.
  • AC-2: Idempotency tests after F01.
  • AC-3: Saga topology or handler test for UsageReported path after F03.
  • AC-4: MeteringOrleansGrainPartitionTests enabled with composite key assertions when F05 lands.
  • AC-5: No placeholder Assert.IsTrue(true) in UnitTests processor coverage.

Implementation notes (full):

  • UnitTests, ArchitectureTests
  • Use test doubles for repository and event bus

Out of scope:

  • Full Playwright acceptance scenarios

Definition of done:

  • All AC pass
  • CI green

saas-MET-S07.1 — As a developer, I need automated tests for DefaultUsageMetersProcessor so quota and idempotency behavior cannot regress

Type: User Story
Parent: saas-MET-F07
Implementation order: 066.1
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, tests
Priority: P1
Effort: L
Dependencies: saas-MET-F01
Blocks:
Source gap analysis: metering-analysis

Description (full):

Build real unit test suite for processor invariants replacing placeholders.

Acceptance criteria (testable):

  • AC-1: Minimum 8 processor tests covering happy paths and edge cases.
  • AC-2: Quota exceeded publishes event with correct Dimension/MeterKey field (F04).

Implementation notes (full):

  • DefaultUsageMetersProcessor.cs test harness

Out of scope: Silo cluster tests (F05/F09)

Definition of done:

  • All AC pass

saas-MET-T07.1.1 — Replace UsageMeterAggregateTests placeholder with processor unit tests

Type: Task
Parent: saas-MET-S07.1
Implementation order: 066.1.1
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, unit-tests
Priority: P1
Effort: M
Dependencies: saas-MET-F01
Blocks: saas-MET-T07.1.2
Source gap analysis: metering-analysis

Description (full):

Implement tests: record usage, roll counter, apply quota, threshold cross, quota exceed, idempotent replay.

Acceptance criteria (testable):

  • AC-1: At least 6 meaningful tests.
  • AC-2: No Assert.IsTrue(true).

Implementation notes (full):

  • tests/ConnectSoft.Saas.Metering.UnitTests/UsageMeterAggregateTests.cs

Out of scope: Saga tests

Definition of done:

  • All AC pass

saas-MET-T07.1.2 — Add saga/topology test for UsageReported quota enforcement path

Type: Task
Parent: saas-MET-S07.1
Implementation order: 066.1.2
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, saga-tests
Priority: P1
Effort: M
Dependencies: saas-MET-F03, saas-MET-T07.1.1
Blocks:
Source gap analysis: metering-analysis

Description (full):

Add test harness verifying UsageMeterQuotaEnforcementStateMachine consumes usage-reported message and invokes record usage (in-memory MassTransit test or focused saga unit test).

Acceptance criteria (testable):

  • AC-1: Test proves saga → processor call chain.
  • AC-2: Fails if topology registration removed (F03 regression).

Implementation notes (full):

  • UsageMeterQuotaEnforcementStateMachine.cs
  • MassTransit test harness pattern from BaseTemplate if available

Out of scope: Full bus integration test

Definition of done:

  • All AC pass

[067] saas-MET-F08 — Optional Entitlements reaction registration

Type: Feature
Parent: saas-EPIC-MET
Implementation order: 067
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, entitlements, P2
Priority: P2
Effort: M
Dependencies: saas-BIL-F02
Blocks:
Source gap analysis: priority-rationale

Description (full):

EntitlementsChangedQuotaStateMachine and EntitlementsChangedQuotaInboundEvent are always registered in MassTransitExtensions.AddMicroserviceMassTransit with in-memory saga repository. Topology binds MeteringConstants.InboundEventTopics.EntitlementsEffectiveSnapshotChangedentitlements.v1.effective-entitlements-updated. No feature flag for deployments without Entitlements bounded context.

Gap priority table lists F08 as P2 optional cross-repo edge: make entitlements consumption configurable — skip saga/topology when Entitlements BC absent.

Acceptance criteria (testable):

  • AC-1: Configuration option (e.g. MeteringOptions:EnableEntitlementsQuotaSync) defaults to true for full stack, false documented for metering-only.
  • AC-2: When disabled, saga not registered and topology skips entitlements inbound binding.
  • AC-3: When enabled, behavior unchanged from current.
  • AC-4: README or docs describe deployment modes.
  • AC-5: Test proves conditional registration.

Implementation notes (full):

  • Files: EntitlementsChangedQuotaStateMachine.cs, MassTransitExtensions.cs, MeteringMassTransitTopology.cs, options class
  • Topic already canonical vs Billing drift — verify constant value

Out of scope:

  • Entitlements publisher changes

Definition of done:

  • All AC pass
  • Docs updated

saas-MET-S08.1 — As a deployer running Metering-only, I need to disable Entitlements inbound sagas so the service starts without missing publishers

Type: User Story
Parent: saas-MET-F08
Implementation order: 067.1
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, configuration
Priority: P2
Effort: M
Dependencies:
Blocks:
Source gap analysis: priority-rationale

Description (full):

Introduce feature flag controlling entitlements quota sync saga and topology.

Acceptance criteria (testable):

  • AC-1: Metering-only docker-compose profile documented with flag false.
  • AC-2: No startup errors when entitlements topic unused.

Implementation notes (full):

  • appsettings.json, Docker compose samples

Out of scope: Other optional inbound sagas

Definition of done:

  • All AC pass

saas-MET-T08.1.1 — Add MeteringOptions flag and conditional saga registration

Type: Task
Parent: saas-MET-S08.1
Implementation order: 067.1.1
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, options
Priority: P2
Effort: S
Dependencies:
Blocks: saas-MET-T08.1.2
Source gap analysis: priority-rationale

Description (full):

Add options binding and wrap EntitlementsChangedQuotaStateMachine registration in MassTransitExtensions with flag check.

Acceptance criteria (testable):

  • AC-1: Flag false → saga type not in registration list.
  • AC-2: Flag true → current behavior.

Implementation notes (full):

  • MassTransitExtensions.cs, new options in ApplicationModel or InfrastructureModel

Out of scope: Topology skip

Definition of done:

  • All AC pass

saas-MET-T08.1.2 — Conditionally skip entitlements inbound topology and add registration test

Type: Task
Parent: saas-MET-S08.1
Implementation order: 067.1.2
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, topology
Priority: P2
Effort: S
Dependencies: saas-MET-T08.1.1
Blocks:
Source gap analysis: priority-rationale

Description (full):

Update MeteringMassTransitTopology to skip entitlements inbound when flag false. Add unit test asserting registration count differs by configuration.

Acceptance criteria (testable):

  • AC-1: Topology method respects flag.
  • AC-2: Automated test covers both modes.

Implementation notes (full):

  • MeteringMassTransitTopology.cs

Out of scope: Tenant/subscription optional flags

Definition of done:

  • All AC pass

[068] saas-MET-F09 — Architecture test enforcement

Type: Feature
Parent: saas-EPIC-MET
Implementation order: 068
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, architecture-tests, P1
Priority: P1
Effort: M
Dependencies: saas-MET-F04, saas-MET-F05
Blocks:
Source gap analysis: metering-analysis

Description (full):

Metering ArchitectureTests status:

File Status
EntityIsolationNetArchTests.cs Implemented
ServiceModelLayeringNetArchTests.cs Implemented
OneAggregateRootPerRepoTests.cs Placeholder
CrossRepoPublishedLanguageTests.cs Placeholder
MeteringOrleansGrainPartitionTests.cs Ignored, empty

ADR-0001 claims CI enforcement; placeholders and ignored tests undermine exit criteria. Implement rules for single UsageMeter aggregate root, published language topic validation (including quota Dimension/MeterKey and inbound topology constants post F03/F04), and composite grain key partition test when F05 completes.

Acceptance criteria (testable):

  • AC-1: OneAggregateRootPerRepoTests enforces single aggregate root.
  • AC-2: CrossRepoPublishedLanguageTests validates MeteringConstants topics and quota event public surface.
  • AC-3: MeteringOrleansGrainPartitionTests enabled with tenantId:dimension key assertions.
  • AC-4: No placeholder or empty ignored tests without linked work item.
  • AC-5: CI runs ArchitectureTests on PR.

Implementation notes (full):

  • Path: tests/ConnectSoft.Saas.Metering.ArchitectureTests/
  • Exemplar: Products Catalog CrossRepo and OneAggregate tests
  • Include regression for F04 payload property name

Out of scope:

  • Functional processor tests (F07)

Definition of done:

  • All AC pass
  • CI green

saas-MET-S09.1 — As a platform maintainer, I need Metering architecture tests to enforce aggregate and cross-repo contract rules

Type: User Story
Parent: saas-MET-F09
Implementation order: 068.1
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, netarch
Priority: P1
Effort: M
Dependencies: saas-MET-F04
Blocks:
Source gap analysis: metering-analysis

Description (full):

Replace placeholders and enable ignored grain test with real assertions.

Acceptance criteria (testable):

  • AC-1: All five arch test files have meaningful content or are merged intentionally.
  • AC-2: Intentional violation fails tests locally once.

Implementation notes (full):

  • NetArchTest.Rules

Out of scope: Shared cross-repo test NuGet (INTEG-F07)

Definition of done:

  • All AC pass

saas-MET-T09.1.1 — Implement OneAggregateRootPerRepoTests and CrossRepoPublishedLanguageTests

Type: Task
Parent: saas-MET-S09.1
Implementation order: 068.1.1
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, netarch
Priority: P1
Effort: S
Dependencies: saas-MET-F04
Blocks: saas-MET-T09.1.2
Source gap analysis: cross-topic-harmonization

Description (full):

Replace placeholders. CrossRepo test validates outbound QuotaExceeded topic, inbound entitlements/tenant/subscription/usage-reported constants, and quota event property name matches Billing consumer contract (post F04).

Acceptance criteria (testable):

  • AC-1: Single aggregate root asserted.
  • AC-2: Topic constants match canonical list.
  • AC-3: Quota event exposes agreed meter identity property.

Implementation notes (full):

  • OneAggregateRootPerRepoTests.cs, CrossRepoPublishedLanguageTests.cs
  • Reference Billing inbound type in test if needed for F04

Out of scope: Envelope fields

Definition of done:

  • All AC pass

saas-MET-T09.1.2 — Enable MeteringOrleansGrainPartitionTests for composite tenantId:dimension key

Type: Task
Parent: saas-MET-S09.1
Implementation order: 068.1.2
Status: Not Started
Area path: ConnectSoft\SaaS\Metering
Iteration: TBD
Tags: saas-platform, gap, metering, orleans, netarch
Priority: P1
Effort: S
Dependencies: saas-MET-F05, saas-MET-T09.1.1
Blocks:
Source gap analysis: cross-orleans

Description (full):

Remove [Ignore], implement tests verifying UsageMeterGrainKey.Format and EnsureMatchesGrainKey reject malformed keys and accept valid composite keys. Optional: silo test grain activation per partition.

Acceptance criteria (testable):

  • AC-1: Test not ignored in CI.
  • AC-2: Invalid key patterns fail validation test cases.

Implementation notes (full):

  • MeteringOrleansGrainPartitionTests.cs, UsageMeterGrainKey.cs

Out of scope: Full silo cluster load test

Definition of done:

  • All AC pass
  • CI green