Files
vantage/shared/license/plans.go
T
mrhid6andClaude Opus 5 3a6c5ebae8
Server Deploy / deploy (push) Successful in 5m22s
fix(admin): licence and plan features serialise as [], never null
Free has no features, so a nil Go slice served "features": null. The
portal's type said string[], called .length on it, and took the page down.

The guarantee lives on a named Features type with its own MarshalJSON
rather than at each of the six places a licence or plan is serialised,
because the seventh is the one that would have been forgotten. Applying it
at marshal time also fixes rows already holding null in Mongo, which a
write-side fix alone would not.

Same class, found while checking the rest: staffListInstances built its
expiring filter from a nil []string, marshalling to $in: null, which Mongo
rejects — so the quiet week when nothing is expiring is precisely when the
staff Operations page's own query would have failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 18:10:07 +01:00

50 lines
1.5 KiB
Go

package license
// Plan is the contents of a tier at issue time.
//
// This table is the seed. Once the admin service exists (spec 3) it owns the
// authoritative copy in its `plans` collection, and every issued licence
// snapshots the plan it was cut from — so editing a plan never rewrites an
// existing licence, the same rule as workflow_runs.steps_snapshot.
//
// lkctl uses this table to issue by hand until then.
type Plan struct {
Tier string
Name string
Deployment string
Limits Limits
Features []string
}
var plans = map[string]Plan{
TierFree: {
Tier: TierFree,
Name: "Free",
Deployment: DeploymentCloud, // cloud only, by construction
Limits: Limits{MaxServers: 3, MaxSecretGroups: 1, MaxChannels: 1},
// Empty rather than nil: nil marshals as JSON null, and this table is
// the seed every plan and licence is cut from.
Features: []string{},
},
TierProfessional: {
Tier: TierProfessional,
Name: "Professional",
Deployment: DeploymentCloud,
Limits: Limits{MaxServers: Unlimited, MaxSecretGroups: Unlimited, MaxChannels: Unlimited},
Features: []string{FeatureConsole, FeatureOIDC},
},
TierSelfHosted: {
Tier: TierSelfHosted,
Name: "Self Hosted",
Deployment: DeploymentSelfHosted,
Limits: Limits{MaxServers: Unlimited, MaxSecretGroups: Unlimited, MaxChannels: Unlimited},
Features: []string{FeatureConsole, FeatureOIDC},
},
}
// PlanFor returns the seed plan for a tier.
func PlanFor(tier string) (Plan, bool) {
p, ok := plans[tier]
return p, ok
}