Skip to content

Commit 8519e29

Browse files
refactor(governance): extract query logic into openmeter/governance service
Move the governance access-query orchestration out of the v3 HTTP handler into a new openmeter/governance service that composes the customer, entitlement, and feature services (no persistence of its own) — mirroring the customerbalance facade/service pattern. The service returns domain types (CustomerAccess, FeatureAccess, QueryError, paged QueryResult); the handler now only decodes the request, validates and decodes cursors, calls the service, and maps the domain result to api.* types. Service tests (Postgres-backed) move to the new package alongside the entitlement→access mapping unit tests. Wired idiomatically via app/common (NewGovernanceService + wire set) threaded through router.Config → cmd/server → openmeter/server → api/v3/server Config.
1 parent 7639ac7 commit 8519e29

15 files changed

Lines changed: 934 additions & 640 deletions

File tree

api/v3/handlers/governance/handler.go

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@ package governance
33
import (
44
"context"
55

6-
"github.com/openmeterio/openmeter/openmeter/customer"
7-
"github.com/openmeterio/openmeter/openmeter/entitlement"
8-
"github.com/openmeterio/openmeter/openmeter/productcatalog/feature"
6+
"github.com/openmeterio/openmeter/openmeter/governance"
97
"github.com/openmeterio/openmeter/pkg/framework/transport/httptransport"
108
)
119

@@ -14,25 +12,19 @@ type Handler interface {
1412
}
1513

1614
type handler struct {
17-
resolveNamespace func(ctx context.Context) (string, error)
18-
customerService customer.Service
19-
entitlementService entitlement.Service
20-
featureConnector feature.FeatureConnector
21-
options []httptransport.HandlerOption
15+
resolveNamespace func(ctx context.Context) (string, error)
16+
governanceService governance.Service
17+
options []httptransport.HandlerOption
2218
}
2319

2420
func New(
2521
resolveNamespace func(ctx context.Context) (string, error),
26-
customerService customer.Service,
27-
entitlementService entitlement.Service,
28-
featureConnector feature.FeatureConnector,
22+
governanceService governance.Service,
2923
options ...httptransport.HandlerOption,
3024
) Handler {
3125
return &handler{
32-
resolveNamespace: resolveNamespace,
33-
customerService: customerService,
34-
entitlementService: entitlementService,
35-
featureConnector: featureConnector,
36-
options: options,
26+
resolveNamespace: resolveNamespace,
27+
governanceService: governanceService,
28+
options: options,
3729
}
3830
}
Lines changed: 89 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,104 @@
11
package governance
22

33
import (
4+
"github.com/oapi-codegen/nullable"
5+
"github.com/samber/lo"
6+
47
api "github.com/openmeterio/openmeter/api/v3"
5-
"github.com/openmeterio/openmeter/openmeter/entitlement"
6-
booleanentitlement "github.com/openmeterio/openmeter/openmeter/entitlement/boolean"
7-
meteredentitlement "github.com/openmeterio/openmeter/openmeter/entitlement/metered"
8-
staticentitlement "github.com/openmeterio/openmeter/openmeter/entitlement/static"
8+
customershandler "github.com/openmeterio/openmeter/api/v3/handlers/customers"
9+
"github.com/openmeterio/openmeter/openmeter/governance"
910
)
1011

11-
// mapEntitlementToAccess converts an entitlement value to a governance feature access result.
12-
// When has_access is false, the reason code is derived from the entitlement type.
13-
func mapEntitlementToAccess(v entitlement.EntitlementValue) api.GovernanceFeatureAccess {
14-
switch ent := v.(type) {
15-
case *meteredentitlement.MeteredEntitlementValue:
16-
if ent.HasAccess() {
17-
return api.GovernanceFeatureAccess{HasAccess: true}
18-
}
19-
return api.GovernanceFeatureAccess{
20-
HasAccess: false,
21-
Reason: &api.GovernanceFeatureAccessReason{
22-
Code: api.GovernanceFeatureAccessReasonCodeUsageLimitReached,
23-
Message: "usage limit for feature reached",
24-
},
12+
// ToAPIGovernanceQueryResponse maps a domain QueryResult to the API response.
13+
func ToAPIGovernanceQueryResponse(res governance.QueryResult, pageSize int) api.GovernanceQueryResponse {
14+
data := make([]api.GovernanceQueryResult, 0, len(res.Customers))
15+
for _, c := range res.Customers {
16+
features := make(map[string]api.GovernanceFeatureAccess, len(c.Features))
17+
for key, fa := range c.Features {
18+
features[key] = toAPIFeatureAccess(fa)
2519
}
2620

27-
case *booleanentitlement.BooleanEntitlementValue:
28-
if ent.HasAccess() {
29-
return api.GovernanceFeatureAccess{HasAccess: true}
30-
}
31-
return api.GovernanceFeatureAccess{
32-
HasAccess: false,
33-
Reason: &api.GovernanceFeatureAccessReason{
34-
Code: api.GovernanceFeatureAccessReasonCodeFeatureUnavailable,
35-
Message: "feature is not available for customer",
36-
},
37-
}
21+
data = append(data, api.GovernanceQueryResult{
22+
Matched: c.Matched,
23+
Customer: customershandler.ToAPIBillingCustomer(c.Customer),
24+
Features: features,
25+
UpdatedAt: c.UpdatedAt,
26+
})
27+
}
3828

39-
case *staticentitlement.StaticEntitlementValue:
40-
if ent.HasAccess() {
41-
return api.GovernanceFeatureAccess{HasAccess: true}
42-
}
43-
return api.GovernanceFeatureAccess{
44-
HasAccess: false,
45-
Reason: &api.GovernanceFeatureAccessReason{
46-
Code: api.GovernanceFeatureAccessReasonCodeFeatureUnavailable,
47-
Message: "feature is not available for customer",
48-
},
29+
errs := make([]api.GovernanceQueryError, 0, len(res.Errors))
30+
for _, e := range res.Errors {
31+
errs = append(errs, api.GovernanceQueryError{
32+
Customer: lo.ToPtr(e.CustomerKey),
33+
Code: toAPIQueryErrorCode(e.Code),
34+
Message: e.Message,
35+
})
36+
}
37+
38+
return api.GovernanceQueryResponse{
39+
Data: data,
40+
Errors: errs,
41+
Meta: toAPICursorMeta(res, pageSize),
42+
}
43+
}
44+
45+
func toAPIFeatureAccess(fa governance.FeatureAccess) api.GovernanceFeatureAccess {
46+
out := api.GovernanceFeatureAccess{HasAccess: fa.HasAccess}
47+
if fa.Reason != nil {
48+
out.Reason = &api.GovernanceFeatureAccessReason{
49+
Code: toAPIReasonCode(fa.Reason.Code),
50+
Message: fa.Reason.Message,
4951
}
52+
}
53+
return out
54+
}
5055

56+
func toAPIReasonCode(code governance.ReasonCode) api.GovernanceFeatureAccessReasonCode {
57+
switch code {
58+
case governance.ReasonUsageLimitReached:
59+
return api.GovernanceFeatureAccessReasonCodeUsageLimitReached
60+
case governance.ReasonFeatureUnavailable:
61+
return api.GovernanceFeatureAccessReasonCodeFeatureUnavailable
62+
case governance.ReasonFeatureNotFound:
63+
return api.GovernanceFeatureAccessReasonCodeFeatureNotFound
64+
case governance.ReasonNoCreditAvailable:
65+
return api.GovernanceFeatureAccessReasonCodeNoCreditAvailable
5166
default:
52-
// NoAccessValue or unknown type
53-
return api.GovernanceFeatureAccess{
54-
HasAccess: false,
55-
Reason: &api.GovernanceFeatureAccessReason{
56-
Code: api.GovernanceFeatureAccessReasonCodeFeatureUnavailable,
57-
Message: "feature is not available for customer",
58-
},
67+
return api.GovernanceFeatureAccessReasonCodeUnknown
68+
}
69+
}
70+
71+
func toAPIQueryErrorCode(code governance.QueryErrorCode) api.GovernanceQueryErrorCode {
72+
switch code {
73+
case governance.QueryErrorCustomerNotFound:
74+
return api.GovernanceQueryErrorCodeCustomerNotFound
75+
default:
76+
return api.GovernanceQueryErrorCodeUnknown
77+
}
78+
}
79+
80+
// toAPICursorMeta builds cursor pagination metadata from the domain result.
81+
func toAPICursorMeta(res governance.QueryResult, pageSize int) api.CursorMeta {
82+
meta := api.CursorMeta{
83+
Page: api.CursorMetaPage{
84+
Next: nullable.NewNullNullable[string](),
85+
Previous: nullable.NewNullNullable[string](),
86+
Size: float32(pageSize),
87+
},
88+
}
89+
90+
if res.First != nil {
91+
meta.Page.First = lo.ToPtr(res.First.Encode())
92+
if res.HasPrev {
93+
meta.Page.Previous = nullable.NewNullableWithValue(res.First.Encode())
5994
}
6095
}
96+
if res.Last != nil {
97+
meta.Page.Last = lo.ToPtr(res.Last.Encode())
98+
if res.HasNext {
99+
meta.Page.Next = nullable.NewNullableWithValue(res.Last.Encode())
100+
}
101+
}
102+
103+
return meta
61104
}

0 commit comments

Comments
 (0)