Organization identity is inconsistent across the services, and it is why only one organization shows a trial in the Admin listing.
What is wrong today
Admin holds 7 OrganizationTrialGranted events but organizationTrials has a single row. Six of them are keyed by organization GUID; one is keyed by tenant name. OrganizationTrial is keyed by tenant name, so only the last one materializes. TrialReportingReactor was changed to key by tenant name at some point, which silently orphaned everything granted before it.
Six organizations (Cratis, Elvion, Hageskal, HiveConsulting, Identigo, Vybraze) have no OrganizationTrialStarted at all - they predate the trial flow, and are exactly the ones carrying manual SeatsGranted events as a workaround.
The old events cannot be salvaged by replaying: a projection keyed by tenant name cannot recover the tenant from a GUID, and projections cannot join to organizations to look it up.
The model we want
The organization's GUID is its event source id everywhere, and never appears as a payload property. Keying on the GUID rather than the name is what would let an organization be renamed later without orphaning its history - which is precisely the failure above, in a different guise.
Changes
Core has to learn the organization's identity. It currently has no notion of the GUID at all: Admin mints it and publishes OrganizationOnboardingAccepted(OrganizationId, TenantName), but only Lobby consumes it. Core should consume it from the Admin inbox and record the organization in its tenant namespace, so it can key on the GUID afterwards.
Contract events drop the tenant from their payload, carrying it as the event source id instead:
Core.Contracts TrialGrantedForOrganization(Tenant, Sku, Expiration) -> (Sku, Expiration)
Admin.Contracts SeatsGrantedToTenant(Tenant, Sku, Quantity, Expiration) -> (Sku, Quantity, Expiration)
Organization-level facts key on the organization, not the user. OrganizationTrialStarted is appended against the user id today; it is an organization fact and belongs on the organization's stream.
Admin's reactors and read models key on OrganizationId. TrialReportingReactor then needs no lookup at all - the event source is already the organization.
Existing events get migrated. Few enough to rewrite in place, and we are in beta.
Integration specs
End to end, one flow: invite from Admin -> acceptance in Lobby -> user registered and seat assigned in Core for the target tenant -> organization registered in Admin with the correct seat assignment. The existing onboarding spec covers part of this path already and is the natural place to extend.
Note
Cratis stays at 3 seats - a SingleUser trial grants one, and its manual grant is what covers the other two.
Organization identity is inconsistent across the services, and it is why only one organization shows a trial in the Admin listing.
What is wrong today
Admin holds 7
OrganizationTrialGrantedevents butorganizationTrialshas a single row. Six of them are keyed by organization GUID; one is keyed by tenant name.OrganizationTrialis keyed by tenant name, so only the last one materializes.TrialReportingReactorwas changed to key by tenant name at some point, which silently orphaned everything granted before it.Six organizations (Cratis, Elvion, Hageskal, HiveConsulting, Identigo, Vybraze) have no
OrganizationTrialStartedat all - they predate the trial flow, and are exactly the ones carrying manualSeatsGrantedevents as a workaround.The old events cannot be salvaged by replaying: a projection keyed by tenant name cannot recover the tenant from a GUID, and projections cannot join to
organizationsto look it up.The model we want
The organization's GUID is its event source id everywhere, and never appears as a payload property. Keying on the GUID rather than the name is what would let an organization be renamed later without orphaning its history - which is precisely the failure above, in a different guise.
Changes
Core has to learn the organization's identity. It currently has no notion of the GUID at all: Admin mints it and publishes
OrganizationOnboardingAccepted(OrganizationId, TenantName), but only Lobby consumes it. Core should consume it from the Admin inbox and record the organization in its tenant namespace, so it can key on the GUID afterwards.Contract events drop the tenant from their payload, carrying it as the event source id instead:
Core.ContractsTrialGrantedForOrganization(Tenant, Sku, Expiration)->(Sku, Expiration)Admin.ContractsSeatsGrantedToTenant(Tenant, Sku, Quantity, Expiration)->(Sku, Quantity, Expiration)Organization-level facts key on the organization, not the user.
OrganizationTrialStartedis appended against the user id today; it is an organization fact and belongs on the organization's stream.Admin's reactors and read models key on
OrganizationId.TrialReportingReactorthen needs no lookup at all - the event source is already the organization.Existing events get migrated. Few enough to rewrite in place, and we are in beta.
Integration specs
End to end, one flow: invite from Admin -> acceptance in Lobby -> user registered and seat assigned in Core for the target tenant -> organization registered in Admin with the correct seat assignment. The existing onboarding spec covers part of this path already and is the natural place to extend.
Note
Cratis stays at 3 seats - a SingleUser trial grants one, and its manual grant is what covers the other two.