Finish TEN-WP-0010-T03/T04: audited grouping mutation
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 1m5s
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 1m5s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
2e11b6a155
commit
b998ca2332
6 changed files with 422 additions and 3 deletions
|
|
@ -174,6 +174,39 @@ class Tenant:
|
|||
|
||||
return replace(self, version=self.version + 1, updated_at=at, **changes) # type: ignore[arg-type]
|
||||
|
||||
def with_grouping(self, grouping: str, *, at: datetime) -> "Tenant":
|
||||
"""Reclassify the tenant (TEN-WP-0010).
|
||||
|
||||
Deliberately *not* part of `with_metadata`. `display_name` and
|
||||
`contact_email` are cosmetic; grouping resolves spend ceilings, so a
|
||||
change here moves money. It gets its own method, its own route, and its
|
||||
own flex-auth action so policy can permit a rename without permitting
|
||||
a reclassification, and so the audit trail shows which one happened.
|
||||
|
||||
The identifier's grouping segment is *historical* -- onboarding-time,
|
||||
immutable, and not authoritative for current grouping (ADR-0013
|
||||
amendment proposed under TEN-WP-0010-T01). This field is the
|
||||
authoritative one, which is why it may diverge from the identifier.
|
||||
"""
|
||||
if self.is_reserved:
|
||||
# tenant:platform and tenant:coulomb are ungrouped by design and
|
||||
# resolve guardrails through the reserved profile. Giving one a
|
||||
# grouping would silently move the platform's own identity onto
|
||||
# the grouping ladder.
|
||||
raise ImmutableFieldError(
|
||||
"reserved tenants are ungrouped and cannot be reclassified"
|
||||
)
|
||||
if self.lifecycle is not TenantLifecycle.ACTIVE:
|
||||
raise InvalidLifecycleTransitionError(
|
||||
"grouping of a retired tenant cannot be changed; reactivate first"
|
||||
)
|
||||
if grouping not in GROUPINGS:
|
||||
raise InvalidTenantIdentifierError(f"Unknown tenant grouping: {grouping!r}")
|
||||
if grouping == self.grouping:
|
||||
raise EmptyUpdateError("update would not change the grouping")
|
||||
|
||||
return replace(self, grouping=grouping, version=self.version + 1, updated_at=at)
|
||||
|
||||
def retire(self, *, at: datetime) -> "Tenant":
|
||||
if self.lifecycle is TenantLifecycle.RETIRED:
|
||||
raise InvalidLifecycleTransitionError("tenant is already retired")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue