Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions api/v3/handlers/governance/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package governance

import (
"context"

"github.com/openmeterio/openmeter/openmeter/customer"
"github.com/openmeterio/openmeter/openmeter/entitlement"
"github.com/openmeterio/openmeter/openmeter/productcatalog/feature"
"github.com/openmeterio/openmeter/pkg/framework/transport/httptransport"
)

type Handler interface {
QueryGovernanceAccess() QueryGovernanceAccessHandler
}

type handler struct {
resolveNamespace func(ctx context.Context) (string, error)
customerService customer.Service
entitlementService entitlement.Service
featureConnector feature.FeatureConnector
options []httptransport.HandlerOption
}

func New(
resolveNamespace func(ctx context.Context) (string, error),
customerService customer.Service,
entitlementService entitlement.Service,
featureConnector feature.FeatureConnector,
options ...httptransport.HandlerOption,
) Handler {
return &handler{
resolveNamespace: resolveNamespace,
customerService: customerService,
entitlementService: entitlementService,
featureConnector: featureConnector,
options: options,
}
}
61 changes: 61 additions & 0 deletions api/v3/handlers/governance/mapping.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package governance

import (
api "github.com/openmeterio/openmeter/api/v3"
"github.com/openmeterio/openmeter/openmeter/entitlement"
booleanentitlement "github.com/openmeterio/openmeter/openmeter/entitlement/boolean"
meteredentitlement "github.com/openmeterio/openmeter/openmeter/entitlement/metered"
staticentitlement "github.com/openmeterio/openmeter/openmeter/entitlement/static"
)

// mapEntitlementToAccess converts an entitlement value to a governance feature access result.
// When has_access is false, the reason code is derived from the entitlement type.
func mapEntitlementToAccess(v entitlement.EntitlementValue) api.GovernanceFeatureAccess {
switch ent := v.(type) {
case *meteredentitlement.MeteredEntitlementValue:
if ent.HasAccess() {
return api.GovernanceFeatureAccess{HasAccess: true}
}
return api.GovernanceFeatureAccess{
HasAccess: false,
Reason: &api.GovernanceFeatureAccessReason{
Code: api.GovernanceFeatureAccessReasonCodeUsageLimitReached,
Message: "usage limit for feature reached",
},
}

case *booleanentitlement.BooleanEntitlementValue:
if ent.HasAccess() {
return api.GovernanceFeatureAccess{HasAccess: true}
}
return api.GovernanceFeatureAccess{
HasAccess: false,
Reason: &api.GovernanceFeatureAccessReason{
Code: api.GovernanceFeatureAccessReasonCodeFeatureUnavailable,
Message: "feature is not available for customer",
},
}

case *staticentitlement.StaticEntitlementValue:
if ent.HasAccess() {
return api.GovernanceFeatureAccess{HasAccess: true}
}
return api.GovernanceFeatureAccess{
HasAccess: false,
Reason: &api.GovernanceFeatureAccessReason{
Code: api.GovernanceFeatureAccessReasonCodeFeatureUnavailable,
Message: "feature is not available for customer",
},
}

default:
// NoAccessValue or unknown type
return api.GovernanceFeatureAccess{
HasAccess: false,
Reason: &api.GovernanceFeatureAccessReason{
Code: api.GovernanceFeatureAccessReasonCodeFeatureUnavailable,
Message: "feature is not available for customer",
},
}
}
}
70 changes: 70 additions & 0 deletions api/v3/handlers/governance/mapping_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package governance

import (
"testing"

"github.com/stretchr/testify/assert"

api "github.com/openmeterio/openmeter/api/v3"
"github.com/openmeterio/openmeter/openmeter/entitlement"
booleanentitlement "github.com/openmeterio/openmeter/openmeter/entitlement/boolean"
meteredentitlement "github.com/openmeterio/openmeter/openmeter/entitlement/metered"
staticentitlement "github.com/openmeterio/openmeter/openmeter/entitlement/static"
)

func TestMapEntitlementToAccess(t *testing.T) {
tests := []struct {
name string
value entitlement.EntitlementValue
wantHasAccess bool
wantCode *api.GovernanceFeatureAccessReasonCode
}{
{
name: "metered with balance — has access",
value: &meteredentitlement.MeteredEntitlementValue{Balance: 10},
wantHasAccess: true,
},
{
name: "metered exhausted — usage limit reached",
value: &meteredentitlement.MeteredEntitlementValue{Balance: 0},
wantHasAccess: false,
wantCode: ptr(api.GovernanceFeatureAccessReasonCodeUsageLimitReached),
},
{
// BooleanEntitlementValue is always HasAccess=true; the gateway returns
// NoAccessValue when the entitlement is inactive/not in plan.
name: "boolean — has access",
value: &booleanentitlement.BooleanEntitlementValue{},
wantHasAccess: true,
},
{
// StaticEntitlementValue is always HasAccess=true.
name: "static — has access",
value: &staticentitlement.StaticEntitlementValue{Config: `{"limit":100}`},
wantHasAccess: true,
},
{
// NoAccessValue is returned when the entitlement is inactive (not in current period).
name: "no access value — feature unavailable",
value: &entitlement.NoAccessValue{},
wantHasAccess: false,
wantCode: ptr(api.GovernanceFeatureAccessReasonCodeFeatureUnavailable),
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := mapEntitlementToAccess(tc.value)
assert.Equal(t, tc.wantHasAccess, got.HasAccess)
if tc.wantCode != nil {
if assert.NotNil(t, got.Reason) {
assert.Equal(t, *tc.wantCode, got.Reason.Code)
}
} else {
assert.Nil(t, got.Reason)
}
})
}
}

func ptr[T any](v T) *T { return &v }
Loading
Loading