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.
75 lines
2.9 KiB
Go
75 lines
2.9 KiB
Go
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
|
|
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
|
|
|
|
package server
|
|
|
|
import (
|
|
"context"
|
|
"html/template"
|
|
"log/slog"
|
|
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/web"
|
|
)
|
|
|
|
// The rule-less warning (design.md A8, M1): one sentence, identical on the
|
|
// five surfaces that hand a rank-0 product to a population or grant one, with
|
|
// the set's name linked to the set's page so the operator can add a rule
|
|
// without losing the preview. It is a warning and nothing more: every commit
|
|
// stays enabled and no control renders disabled.
|
|
//
|
|
// The condition is read from core.product_shape's active_rule_count, which is
|
|
// the sole source of the answer to whether a product's set provides anything
|
|
// (product-catalog; design.md M8, Decision 148). Nothing here counts
|
|
// entitlement_set_rules.
|
|
|
|
// ruleLessWarning returns the sentence for productID, or "" when the product
|
|
// carries a set with at least one active rule, carries no set at all, or
|
|
// cannot be read. A read failure is silent by design: the sentence is a
|
|
// warning, and a failed read must not invent one or block the preview.
|
|
func (h *OperatorPartialsHandler) ruleLessWarning(ctx context.Context, productID string) template.HTML {
|
|
if productID == "" || h.BillingQ == nil || h.EntitlementsQ == nil {
|
|
return ""
|
|
}
|
|
shape, err := h.BillingQ.GetProductShape(ctx, productID)
|
|
if err != nil {
|
|
h.Logger.Error("failed to read product shape for the rule-less warning",
|
|
slog.Any("error", err), slog.String("product_id", productID))
|
|
return ""
|
|
}
|
|
if shape.ActiveRuleCount > 0 {
|
|
return ""
|
|
}
|
|
product, err := h.BillingQ.GetProductByID(ctx, productID)
|
|
if err != nil {
|
|
h.Logger.Error("failed to read the product for the rule-less warning",
|
|
slog.Any("error", err), slog.String("product_id", productID))
|
|
return ""
|
|
}
|
|
// No set at all is a different fact, reported by the readiness surfaces'
|
|
// entitlement-set row; this sentence names a set.
|
|
if !product.EntitlementSetID.Valid {
|
|
return ""
|
|
}
|
|
setID := product.EntitlementSetID.UUID.String()
|
|
set, err := h.EntitlementsQ.GetEntitlementSetByID(ctx, setID)
|
|
if err != nil {
|
|
h.Logger.Error("failed to read the entitlement set for the rule-less warning",
|
|
slog.Any("error", err), slog.String("set_id", setID))
|
|
return ""
|
|
}
|
|
return ruleLessWarningSentence(product.Name, setID, set.Name)
|
|
}
|
|
|
|
// ruleLessWarningSentence is the one wording, in one place, so the five
|
|
// surfaces cannot drift apart.
|
|
func ruleLessWarningSentence(productName, setID, setName string) template.HTML {
|
|
setURL, err := web.RouteURL("/operator/entitlement-sets/{setID}", setID)
|
|
if err != nil {
|
|
setURL = "/operator/entitlement-sets"
|
|
}
|
|
return template.HTML(template.HTMLEscapeString(productName) +
|
|
` provides nothing: its entitlement set <a href="` +
|
|
template.HTMLEscapeString(setURL) + `">` + template.HTMLEscapeString(setName) +
|
|
`</a> has no active rule.`)
|
|
}
|