Files
member-console/internal/server/operator_entitlement_sets.go

341 lines
10 KiB
Go

package server
import (
"database/sql"
"log/slog"
"net/http"
"strconv"
"git.coopcloud.tech/wiki-cafe/member-console/internal/entitlements"
)
// EntitlementSetViewModel represents an entitlement set for operator template rendering.
type EntitlementSetViewModel struct {
SetID string
Name string
Description string
IsActive bool
RuleCount int
CreatedAt string
}
// EntitlementSetsData holds data for the entitlement sets list partial.
type EntitlementSetsData struct {
EntitlementSets []EntitlementSetViewModel
Success string
Error string
}
// EntitlementSetEditData holds data for the entitlement set edit form.
type EntitlementSetEditData struct {
EntitlementSet EntitlementSetViewModel
Success string
Error string
}
// RuleViewModel represents an entitlement set rule for template rendering.
type RuleViewModel struct {
RuleID string
ResourceKey string
ResourceValue int64
ResourcePerUnit bool
StackingPolicy string
IsActive bool
}
// EntitlementSetRulesData holds data for the rules detail partial.
type EntitlementSetRulesData struct {
EntitlementSet EntitlementSetViewModel
Rules []RuleViewModel
ResourceKeys []ResourceKeyOption
Success string
Error string
}
// ResourceKeyOption represents a resource key for dropdown selection.
type ResourceKeyOption struct {
ResourceKey string
DisplayName string
}
// GetEntitlementSets handles GET /partials/operator/entitlement-sets
func (h *OperatorPartialsHandler) GetEntitlementSets(w http.ResponseWriter, r *http.Request) {
h.renderEntitlementSetsPage(w, r, "", "")
}
// CreateEntitlementSet handles POST /partials/operator/entitlement-sets
func (h *OperatorPartialsHandler) CreateEntitlementSet(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
h.renderEntitlementSetsPage(w, r, "", "Invalid request")
return
}
name := r.FormValue("name")
description := r.FormValue("description")
if name == "" {
h.renderEntitlementSetsPage(w, r, "", "Name is required")
return
}
_, err := h.EntitlementsQ.CreateEntitlementSet(r.Context(), entitlements.CreateEntitlementSetParams{
Name: name,
Description: sql.NullString{String: description, Valid: description != ""},
IsActive: true,
})
if err != nil {
h.Logger.Error("failed to create entitlement set", slog.Any("error", err))
h.renderEntitlementSetsPage(w, r, "", "Failed to create entitlement set: "+err.Error())
return
}
// Signal dependent tabs (Products, Grants) to re-fetch — they show entitlement set dropdowns
w.Header().Set("HX-Trigger", "entitlementMutation")
h.renderEntitlementSetsPage(w, r, "Entitlement set created successfully.", "")
}
// GetEntitlementSetEdit handles GET /partials/operator/entitlement-sets/{setID}/edit
func (h *OperatorPartialsHandler) GetEntitlementSetEdit(w http.ResponseWriter, r *http.Request) {
setID := r.PathValue("setID")
h.renderEntitlementSetEditPage(w, r, setID, "", "")
}
// UpdateEntitlementSet handles PUT /partials/operator/entitlement-sets/{setID}
func (h *OperatorPartialsHandler) UpdateEntitlementSet(w http.ResponseWriter, r *http.Request) {
setID := r.PathValue("setID")
if err := r.ParseForm(); err != nil {
h.renderEntitlementSetEditPage(w, r, setID, "", "Invalid request")
return
}
name := r.FormValue("name")
description := r.FormValue("description")
if name == "" {
h.renderEntitlementSetEditPage(w, r, setID, "", "Name is required")
return
}
isActive := r.FormValue("is_active") == "true"
_, err := h.EntitlementsQ.UpdateEntitlementSet(r.Context(), entitlements.UpdateEntitlementSetParams{
SetID: setID,
Name: name,
Description: sql.NullString{String: description, Valid: description != ""},
IsActive: isActive,
})
if err != nil {
h.Logger.Error("failed to update entitlement set", slog.Any("error", err))
h.renderEntitlementSetEditPage(w, r, setID, "", "Failed to update entitlement set: "+err.Error())
return
}
// Signal dependent tabs (Products, Grants) to re-fetch — they show entitlement set dropdowns
w.Header().Set("HX-Trigger", "entitlementMutation")
h.renderEntitlementSetsPage(w, r, "Entitlement set updated successfully.", "")
}
// GetEntitlementSetRules handles GET /partials/operator/entitlement-sets/{setID}/rules
func (h *OperatorPartialsHandler) GetEntitlementSetRules(w http.ResponseWriter, r *http.Request) {
setID := r.PathValue("setID")
h.renderEntitlementSetRulesPage(w, r, setID, "", "")
}
// CreateEntitlementSetRule handles POST /partials/operator/entitlement-sets/{setID}/rules
func (h *OperatorPartialsHandler) CreateEntitlementSetRule(w http.ResponseWriter, r *http.Request) {
setID := r.PathValue("setID")
if err := r.ParseForm(); err != nil {
h.renderEntitlementSetRulesPage(w, r, setID, "", "Invalid request")
return
}
resourceKey := r.FormValue("resource_key")
resourceValueStr := r.FormValue("resource_value")
stackingPolicy := r.FormValue("stacking_policy")
resourcePerUnit := r.FormValue("resource_per_unit") == "true"
if resourceKey == "" || resourceValueStr == "" || stackingPolicy == "" {
h.renderEntitlementSetRulesPage(w, r, setID, "", "Resource key, value, and stacking policy are required")
return
}
resourceValue, err := strconv.ParseInt(resourceValueStr, 10, 64)
if err != nil {
h.renderEntitlementSetRulesPage(w, r, setID, "", "Invalid resource value")
return
}
_, err = h.EntitlementsQ.CreateEntitlementSetRule(r.Context(), entitlements.CreateEntitlementSetRuleParams{
SetID: setID,
RuleType: "limit",
ResourceKey: sql.NullString{String: resourceKey, Valid: true},
ResourceValue: sql.NullInt64{Int64: resourceValue, Valid: true},
ResourcePerUnit: sql.NullBool{Bool: resourcePerUnit, Valid: true},
StackingPolicy: sql.NullString{String: stackingPolicy, Valid: true},
})
if err != nil {
h.Logger.Error("failed to create rule", slog.Any("error", err))
h.renderEntitlementSetRulesPage(w, r, setID, "", "Failed to create rule: "+err.Error())
return
}
h.renderEntitlementSetRulesPage(w, r, setID, "Rule added successfully.", "")
}
// DeleteEntitlementSetRule handles DELETE /partials/operator/entitlement-sets/{setID}/rules/{ruleID}
func (h *OperatorPartialsHandler) DeleteEntitlementSetRule(w http.ResponseWriter, r *http.Request) {
setID := r.PathValue("setID")
ruleID := r.PathValue("ruleID")
err := h.EntitlementsQ.DeleteEntitlementSetRule(r.Context(), ruleID)
if err != nil {
h.Logger.Error("failed to delete rule", slog.Any("error", err))
h.renderEntitlementSetRulesPage(w, r, setID, "", "Failed to delete rule: "+err.Error())
return
}
h.renderEntitlementSetRulesPage(w, r, setID, "Rule deleted.", "")
}
// Rendering helpers
func (h *OperatorPartialsHandler) renderEntitlementSetsPage(w http.ResponseWriter, r *http.Request, success string, errMsg string) {
data := EntitlementSetsData{
Success: success,
Error: errMsg,
}
if errMsg == "" {
sets, err := h.EntitlementsQ.ListEntitlementSets(r.Context())
if err != nil {
h.Logger.Error("failed to list entitlement sets", slog.Any("error", err))
data.Error = "Failed to load entitlement sets"
} else {
data.EntitlementSets = make([]EntitlementSetViewModel, len(sets))
for i, s := range sets {
ruleCount := 0
rules, err := h.EntitlementsQ.GetActiveRulesBySetID(r.Context(), s.SetID)
if err == nil {
ruleCount = len(rules)
}
desc := ""
if s.Description.Valid {
desc = s.Description.String
}
data.EntitlementSets[i] = EntitlementSetViewModel{
SetID: s.SetID,
Name: s.Name,
Description: desc,
IsActive: s.IsActive,
RuleCount: ruleCount,
CreatedAt: s.CreatedAt.Format("Jan 2, 2006"),
}
}
}
}
h.Templates.Render(w, "operator_entitlement_sets.html", data)
}
func (h *OperatorPartialsHandler) renderEntitlementSetEditPage(w http.ResponseWriter, r *http.Request, setID string, success string, errMsg string) {
data := EntitlementSetEditData{
Success: success,
Error: errMsg,
}
es, err := h.EntitlementsQ.GetEntitlementSetByID(r.Context(), setID)
if err != nil {
h.Logger.Error("failed to get entitlement set", slog.Any("error", err))
h.renderEntitlementSetsPage(w, r, "", "Entitlement set not found")
return
}
desc := ""
if es.Description.Valid {
desc = es.Description.String
}
data.EntitlementSet = EntitlementSetViewModel{
SetID: es.SetID,
Name: es.Name,
Description: desc,
IsActive: es.IsActive,
CreatedAt: es.CreatedAt.Format("Jan 2, 2006"),
}
h.Templates.Render(w, "operator_entitlement_set_edit.html", data)
}
func (h *OperatorPartialsHandler) renderEntitlementSetRulesPage(w http.ResponseWriter, r *http.Request, setID string, success string, errMsg string) {
data := EntitlementSetRulesData{
Success: success,
Error: errMsg,
}
es, err := h.EntitlementsQ.GetEntitlementSetByID(r.Context(), setID)
if err != nil {
h.Logger.Error("failed to get entitlement set", slog.Any("error", err))
h.renderEntitlementSetsPage(w, r, "", "Entitlement set not found")
return
}
desc := ""
if es.Description.Valid {
desc = es.Description.String
}
data.EntitlementSet = EntitlementSetViewModel{
SetID: es.SetID,
Name: es.Name,
Description: desc,
IsActive: es.IsActive,
}
rules, err := h.EntitlementsQ.GetActiveRulesBySetID(r.Context(), setID)
if err != nil {
h.Logger.Error("failed to list rules", slog.Any("error", err))
} else {
data.Rules = make([]RuleViewModel, len(rules))
for i, rule := range rules {
rk := ""
if rule.ResourceKey.Valid {
rk = rule.ResourceKey.String
}
var rv int64
if rule.ResourceValue.Valid {
rv = rule.ResourceValue.Int64
}
rpu := false
if rule.ResourcePerUnit.Valid {
rpu = rule.ResourcePerUnit.Bool
}
sp := ""
if rule.StackingPolicy.Valid {
sp = rule.StackingPolicy.String
}
data.Rules[i] = RuleViewModel{
RuleID: rule.RuleID,
ResourceKey: rk,
ResourceValue: rv,
ResourcePerUnit: rpu,
StackingPolicy: sp,
IsActive: rule.IsActive,
}
}
}
// Load resource keys for dropdown
resourceKeys, err := h.EntitlementsQ.ListResourceKeys(r.Context())
if err != nil {
h.Logger.Error("failed to list resource keys", slog.Any("error", err))
} else {
data.ResourceKeys = make([]ResourceKeyOption, len(resourceKeys))
for i, rk := range resourceKeys {
data.ResourceKeys[i] = ResourceKeyOption{
ResourceKey: rk.ResourceKey,
DisplayName: rk.DisplayName,
}
}
}
h.Templates.Render(w, "operator_entitlement_set_rules.html", data)
}