Files
T
cgalo5758 3727ff31d8 Add entitlement set rule change ledger and preview flow
Add an append-only ledger of entitlement set rule changes with per-pool
effect rows, a preview-and-commit rule change flow, and an automatic
drain that settles deferred recomputations. Rules gain a tier reduction
policy, resource keys declare over-limit behavior, and the materializer
now lowers limits when a rule stops applying.
Add entitlement set rule change ledger and preview flow

Add an append-only ledger of entitlement set rule changes with a
preview-and-commit operator flow. Rule writes now go through an enclosed
`core.commit_rule_change` function that files an act row and one
obligation per carrying pool, with a drain workflow settling deferred
recomputations. The preview dry-runs the materializer with a rule
overlay and renders per-pool buckets, reduction-policy disclosures, and
provider over-limit consequences. Materializing transactions take a
shared advisory rendezvous that rule changes hold exclusively, enforced
by a possession assertion. Add History and Entitlement changes surfaces,
a rule-less warning on five product-selection surfaces, and a
`tier_reduction_policy` column that gates FedWiki parking.
2026-09-15 03:53:28 -05:00

83 lines
2.9 KiB
Go

// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
package entitlements
import (
"context"
"database/sql"
"errors"
"fmt"
"log/slog"
"git.coopcloud.tech/wiki-cafe/member-console/internal/entitlements"
)
// Activities holds dependencies for entitlement-lifecycle workflows.
type Activities struct {
DB *sql.DB
Logger *slog.Logger
}
// NewActivities constructs an Activities ready for registration.
func NewActivities(db *sql.DB, logger *slog.Logger) *Activities {
return &Activities{DB: db, Logger: logger}
}
// ExpireGrantInput identifies the grant whose pools should transition off it.
type ExpireGrantInput struct {
GrantID string
}
// ExpireGrantOutput reports what the activity did. TransitionsRecorded is the
// number of positions ended by EndConferral for this grant source; Skipped is
// true when the grant was no longer active (already revoked/expired or absent),
// indicating the activity was a no-op.
type ExpireGrantOutput struct {
TransitionsRecorded int
Skipped bool
}
// ExpireGrantActivity decrees the grant expired (status active -> expired),
// ends every position conferred by this grant source via EndConferral, then
// re-materializes the org's default pool so entitlements settle to the default
// (or detach when no default is configured).
//
// The activity is branch-free and idempotent: ExpireGrant returns
// sql.ErrNoRows when the grant is already non-active (Skipped=true, a no-op),
// and EndConferral treats a source that resolves to nothing live as success
// (empty slice), so re-running against an already-ended grant records zero
// transitions rather than erroring.
func (a *Activities) ExpireGrantActivity(ctx context.Context, input ExpireGrantInput) (ExpireGrantOutput, error) {
if input.GrantID == "" {
return ExpireGrantOutput{}, fmt.Errorf("grant_id is required")
}
tx, err := entitlements.BeginMaterializing(ctx, a.DB, nil)
if err != nil {
return ExpireGrantOutput{}, fmt.Errorf("begin tx: %w", err)
}
defer tx.Rollback()
// The act (decree, end every position this grant conferred, restore the
// org-type default if that left the ladder vacant, materialize) is the
// shared entitlements helper the operator surface and the demo seed run.
result, err := entitlements.ExpireGrantTx(ctx, tx, input.GrantID)
if err != nil {
if errors.Is(err, entitlements.ErrGrantNotActive) {
a.Logger.Info("ExpireGrantActivity: grant not active, skipping",
slog.String("grant_id", input.GrantID))
return ExpireGrantOutput{Skipped: true}, nil
}
return ExpireGrantOutput{}, err
}
if err := tx.Commit(); err != nil {
return ExpireGrantOutput{}, fmt.Errorf("commit: %w", err)
}
// Skipped stays false: the grant was active when this activity ran. A
// zero count is a legitimate idempotent re-run against nothing live.
return ExpireGrantOutput{TransitionsRecorded: len(result.Ended)}, nil
}