Files
member-console/internal/server/rule_less_warning.go
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

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.`)
}