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.
83 lines
2.9 KiB
Go
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
|
|
}
|