feat: Updated plans and catalogue pages

This commit is contained in:
2026-08-25 14:35:47 +00:00
parent 270d55e6a6
commit 3e4865884c
18 changed files with 825 additions and 390 deletions
+21 -1
View File
@@ -485,6 +485,7 @@ func staffListCatalogue(c *gin.Context) {
func staffUpdateCatalogue(c *gin.Context) {
var body struct {
Kind string `json:"kind"`
Scope string `json:"scope"`
Deployment string `json:"deployment"`
Tier string `json:"tier"`
LimitKey string `json:"limit_key"`
@@ -500,11 +501,19 @@ func staffUpdateCatalogue(c *gin.Context) {
// mean a resolved self-hosted monthly price later, which the resolver treats
// as a configuration error — better to refuse it at the point somebody
// pastes it, while they are looking at the screen.
//
// A shared row is sold by both deployments, so both terms are legitimate on
// it: the cloud checkout takes the monthly price and the self-hosted one
// never asks for it. Only a plan row can name a term its own deployment
// does not sell.
for env, byTerm := range body.PriceIDs {
for term, id := range byTerm {
if id == "" {
continue
}
if body.Scope == models.ScopeShared {
continue
}
if !termSold(body.Deployment, term) {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("%s does not sell %s (environment %s)",
@@ -514,6 +523,8 @@ func staffUpdateCatalogue(c *gin.Context) {
}
}
// Addressed by its natural key, so the staff UI never holds a Mongo id. A
// shared row's empty deployment and tier are part of that key.
filter := bson.M{
"kind": body.Kind,
"deployment": body.Deployment,
@@ -534,12 +545,21 @@ func staffUpdateCatalogue(c *gin.Context) {
audit.Write(c.Request.Context(), models.AuditEntry{
Actor: auth.Current(c).Email,
Action: "catalogue.updated",
Target: body.Deployment + "/" + body.Tier + "/" + body.Kind,
Target: catalogueTarget(body.Scope, body.Deployment, body.Tier, body.Kind),
Detail: body.LimitKey + body.FeatureKey,
})
c.JSON(http.StatusOK, gin.H{"updated": true})
}
// catalogueTarget names an edited component in the audit log. A shared row has
// no plan to name, so it says so rather than logging "//feature".
func catalogueTarget(scope, deployment, tier, kind string) string {
if scope == models.ScopeShared {
return "shared/" + kind
}
return deployment + "/" + tier + "/" + kind
}
func termSold(deployment, term string) bool {
for _, t := range license.TermsFor(deployment) {
if t == term {