Skip to content

Commit

Permalink
feat: expose issueAfterResetPriority API (#1149)
Browse files Browse the repository at this point in the history
  • Loading branch information
GAlexIHU authored Jul 5, 2024
1 parent 1c444a8 commit 01de191
Show file tree
Hide file tree
Showing 23 changed files with 664 additions and 328 deletions.
244 changes: 125 additions & 119 deletions api/api.gen.go

Large diffs are not rendered by default.

244 changes: 125 additions & 119 deletions api/client/go/client.gen.go

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions api/client/node/schemas/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,13 @@ export interface components {
* @description You can issue usage automatically after reset. This usage is not rolled over.
*/
issueAfterReset?: number
/**
* @description Defines the grant priority for the default grant. If provided, issueAfterReset must have a value.
*
* @default 1
* @example 1
*/
issueAfterResetPriority?: number
}
EntitlementCreateInputs:
| components['schemas']['EntitlementMeteredCreateInputs']
Expand Down
7 changes: 7 additions & 0 deletions api/client/web/src/client/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,13 @@ export interface components {
* @description You can issue usage automatically after reset. This usage is not rolled over.
*/
issueAfterReset?: number
/**
* @description Defines the grant priority for the default grant. If provided, issueAfterReset must have a value.
*
* @default 1
* @example 1
*/
issueAfterResetPriority?: number
}
EntitlementCreateInputs:
| components['schemas']['EntitlementMeteredCreateInputs']
Expand Down
9 changes: 8 additions & 1 deletion api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1560,12 +1560,19 @@ components:
If unlimited=true the subject can use the feature an unlimited amount.
usagePeriod:
$ref: "#/components/schemas/RecurringPeriodCreateInput"
# Mixed feelings about adding this, rest is not an intent driven convention
issueAfterReset:
type: number
format: double
description: |
You can issue usage automatically after reset. This usage is not rolled over.
issueAfterResetPriority:
type: integer
minimum: 1
maximum: 255
default: 1
example: 1
description: |
Defines the grant priority for the default grant. If provided, issueAfterReset must have a value.
EntitlementCreateInputs:
oneOf:
- $ref: "#/components/schemas/EntitlementMeteredCreateInputs"
Expand Down
44 changes: 42 additions & 2 deletions e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,46 @@ func TestCredit(t *testing.T) {
require.Equal(t, *entitlementId, resp.ApplicationproblemJSON409.Extensions.ConflictingEntityId)
})

t.Run("Create a Entitlement With Default Grants", func(t *testing.T) {
randSubject := ulid.Make().String()
meteredEntitlement := api.EntitlementMeteredCreateInputs{
Type: "metered",
FeatureId: featureId,
UsagePeriod: api.RecurringPeriodCreateInput{
Anchor: convert.ToPointer(time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC)),
Interval: "MONTH",
},
IssueAfterReset: convert.ToPointer(100.0),
IssueAfterResetPriority: convert.ToPointer(6),
}
body := &api.CreateEntitlementJSONRequestBody{}
err := body.FromEntitlementMeteredCreateInputs(meteredEntitlement)
require.NoError(t, err)
resp, err := client.CreateEntitlementWithResponse(context.Background(), randSubject, *body)

require.NoError(t, err)
require.Equal(t, http.StatusCreated, resp.StatusCode(), "Invalid status code [response_body=%s]", string(resp.Body))

metered, err := resp.JSON201.AsEntitlementMetered()
require.NoError(t, err)

require.Equal(t, randSubject, metered.SubjectKey)

// fetch grants for entitlement
grantListResp, err := client.ListEntitlementGrantsWithResponse(context.Background(), randSubject, *metered.Id, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, grantListResp.StatusCode())
require.NotNil(t, grantListResp.JSON200)
require.Len(t, *grantListResp.JSON200, 1)

require.Equal(t, *metered.IssueAfterReset, (*grantListResp.JSON200)[0].Amount)
require.Equal(t, metered.IssueAfterResetPriority, (*grantListResp.JSON200)[0].Priority)
require.Equal(t, metered.Id, (*grantListResp.JSON200)[0].EntitlementId)
require.Equal(t, map[string]string{
"issueAfterReset": "true",
}, *(*grantListResp.JSON200)[0].Metadata)
})

t.Run("Create Grant", func(t *testing.T) {
effectiveAt := time.Now().Truncate(time.Minute)

Expand Down Expand Up @@ -679,7 +719,7 @@ func TestCredit(t *testing.T) {

t.Run("Entitlement Value", func(t *testing.T) {
// Get grants
grantListResp, err := client.ListGrantsWithResponse(context.Background(), &api.ListGrantsParams{})
grantListResp, err := client.ListEntitlementGrantsWithResponse(context.Background(), subject, *entitlementId, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, grantListResp.StatusCode())
require.NotNil(t, grantListResp.JSON200)
Expand Down Expand Up @@ -714,7 +754,7 @@ func TestCredit(t *testing.T) {
effectiveAt := time.Now().Truncate(time.Minute)

// Get grants
grantListResp, err := client.ListGrantsWithResponse(context.Background(), &api.ListGrantsParams{})
grantListResp, err := client.ListEntitlementGrantsWithResponse(context.Background(), subject, *entitlementId, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, grantListResp.StatusCode())
require.NotNil(t, grantListResp.JSON200)
Expand Down
4 changes: 0 additions & 4 deletions internal/credit/grant.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ func (n NamespacedGrantOwner) NamespacedID() models.NamespacedID {
}
}

const (
GrantPriorityDefault uint8 = 1
)

// Grant is an immutable definition used to increase balance.
type Grant struct {
models.ManagedModel
Expand Down
20 changes: 11 additions & 9 deletions internal/entitlement/entitlement.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ type CreateEntitlementInputs struct {
EntitlementType EntitlementType `json:"type"`
Metadata map[string]string `json:"metadata,omitempty"`

MeasureUsageFrom *time.Time `json:"measureUsageFrom,omitempty"`
IssueAfterReset *float64 `json:"issueAfterReset,omitempty"`
IsSoftLimit *bool `json:"isSoftLimit,omitempty"`
Config []byte `json:"config,omitempty"`
UsagePeriod *UsagePeriod `json:"usagePeriod,omitempty"`
MeasureUsageFrom *time.Time `json:"measureUsageFrom,omitempty"`
IssueAfterReset *float64 `json:"issueAfterReset,omitempty"`
IssueAfterResetPriority *uint8 `json:"issueAfterResetPriority,omitempty"`
IsSoftLimit *bool `json:"isSoftLimit,omitempty"`
Config []byte `json:"config,omitempty"`
UsagePeriod *UsagePeriod `json:"usagePeriod,omitempty"`
}

func (c CreateEntitlementInputs) GetType() EntitlementType {
Expand All @@ -37,10 +38,11 @@ type Entitlement struct {

// All none-core fields are optional
// metered
MeasureUsageFrom *time.Time `json:"_,omitempty"`
IssueAfterReset *float64 `json:"issueAfterReset,omitempty"`
IsSoftLimit *bool `json:"isSoftLimit,omitempty"`
LastReset *time.Time `json:"lastReset,omitempty"`
MeasureUsageFrom *time.Time `json:"_,omitempty"`
IssueAfterReset *float64 `json:"issueAfterReset,omitempty"`
IssueAfterResetPriority *uint8 `json:"issueAfterResetPriority,omitempty"`
IsSoftLimit *bool `json:"isSoftLimit,omitempty"`
LastReset *time.Time `json:"lastReset,omitempty"`

// static
Config []byte `json:"config,omitempty"`
Expand Down
3 changes: 3 additions & 0 deletions internal/entitlement/httpdriver/entitlement.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ func (h *entitlementHandler) CreateEntitlement() CreateEntitlementHandler {
EntitlementType: entitlement.EntitlementTypeMetered,
IsSoftLimit: v.IsSoftLimit,
IssueAfterReset: v.IssueAfterReset,
IssueAfterResetPriority: convert.SafeDeRef(v.IssueAfterResetPriority, func(i int) *uint8 {
return convert.ToPointer(uint8(i))
}),
UsagePeriod: &entitlement.UsagePeriod{
Anchor: defaultx.WithDefault(v.UsagePeriod.Anchor, clock.Now()), // TODO: shouldn't we truncate this?
Interval: recurrence.RecurrenceInterval(v.UsagePeriod.Interval),
Expand Down
23 changes: 15 additions & 8 deletions internal/entitlement/httpdriver/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,21 @@ func (parser) ToMetered(e *entitlement.Entitlement) (*api.EntitlementMetered, er
}

return &api.EntitlementMetered{
CreatedAt: &metered.CreatedAt,
DeletedAt: metered.DeletedAt,
FeatureId: metered.FeatureID,
FeatureKey: metered.FeatureKey,
Id: &metered.ID,
IsSoftLimit: convert.ToPointer(metered.IsSoftLimit),
IsUnlimited: convert.ToPointer(false), // implement
IssueAfterReset: metered.IssuesAfterReset,
CreatedAt: &metered.CreatedAt,
DeletedAt: metered.DeletedAt,
FeatureId: metered.FeatureID,
FeatureKey: metered.FeatureKey,
Id: &metered.ID,
IsSoftLimit: convert.ToPointer(metered.IsSoftLimit),
IsUnlimited: convert.ToPointer(false), // implement
IssueAfterReset: convert.SafeDeRef(metered.IssueAfterReset, func(i meteredentitlement.IssueAfterReset) *float64 {
return &i.Amount
}),
IssueAfterResetPriority: convert.SafeDeRef(metered.IssueAfterReset, func(i meteredentitlement.IssueAfterReset) *int {
return convert.SafeDeRef(i.Priority, func(p uint8) *int {
return convert.ToPointer(int(p))
})
}),
Metadata: convert.MapToPointer(metered.Metadata),
SubjectKey: metered.SubjectKey,
Type: api.EntitlementMeteredType(metered.EntitlementType),
Expand Down
39 changes: 25 additions & 14 deletions internal/entitlement/metered/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ func (c *connector) BeforeCreate(model entitlement.CreateEntitlementInputs, feat
return nil, &entitlement.InvalidFeatureError{FeatureID: feature.ID, Message: "Feature has no meter"}
}

if model.IssueAfterResetPriority != nil && model.IssueAfterReset == nil {
return nil, &entitlement.InvalidValueError{Type: model.EntitlementType, Message: "IssueAfterResetPriority requires IssueAfterReset"}
}

model.MeasureUsageFrom = convert.ToPointer(defaultx.WithDefault(model.MeasureUsageFrom, clock.Now().Truncate(c.granularity)))
model.IsSoftLimit = convert.ToPointer(defaultx.WithDefault(model.IsSoftLimit, false))
model.IssueAfterReset = convert.ToPointer(defaultx.WithDefault(model.IssueAfterReset, 0.0))
Expand All @@ -138,17 +142,18 @@ func (c *connector) BeforeCreate(model entitlement.CreateEntitlementInputs, feat
currentPeriod.From = *model.MeasureUsageFrom

return &entitlement.CreateEntitlementRepoInputs{
Namespace: model.Namespace,
FeatureID: feature.ID,
FeatureKey: feature.Key,
SubjectKey: model.SubjectKey,
EntitlementType: model.EntitlementType,
Metadata: model.Metadata,
MeasureUsageFrom: model.MeasureUsageFrom,
IssueAfterReset: model.IssueAfterReset,
IsSoftLimit: model.IsSoftLimit,
UsagePeriod: model.UsagePeriod,
CurrentUsagePeriod: &currentPeriod,
Namespace: model.Namespace,
FeatureID: feature.ID,
FeatureKey: feature.Key,
SubjectKey: model.SubjectKey,
EntitlementType: model.EntitlementType,
Metadata: model.Metadata,
MeasureUsageFrom: model.MeasureUsageFrom,
IssueAfterReset: model.IssueAfterReset,
IssueAfterResetPriority: model.IssueAfterResetPriority,
IsSoftLimit: model.IsSoftLimit,
UsagePeriod: model.UsagePeriod,
CurrentUsagePeriod: &currentPeriod,
}, nil
}

Expand All @@ -162,15 +167,18 @@ func (c *connector) AfterCreate(ctx context.Context, end *entitlement.Entitlemen
// Until we refactor and fix this, to avoid any potential errors due to changes in downstream connectors, the code is inlined here.
// issue default grants
if metered.HasDefaultGrant() {
amountToIssue := *metered.IssuesAfterReset
effectiveAt := metered.CurrentUsagePeriod.From
if metered.IssueAfterReset == nil {
return fmt.Errorf("inconsistency error: entitlement %s should have default grant but has no IssueAfterReset", metered.ID)
}

effectiveAt := metered.CurrentUsagePeriod.From
amountToIssue := metered.IssueAfterReset.Amount
_, err := c.grantConnector.CreateGrant(ctx, credit.NamespacedGrantOwner{
Namespace: metered.Namespace,
ID: credit.GrantOwner(metered.ID),
}, credit.CreateGrantInput{
Amount: amountToIssue,
Priority: credit.GrantPriorityDefault,
Priority: defaultx.WithDefault(metered.IssueAfterReset.Priority, DefaultIssueAfterResetPriority),
EffectiveAt: effectiveAt,
Expiration: credit.ExpirationPeriod{
Count: 100, // This is a bit of an issue... It would make sense for recurring tags to not have an expiration
Expand All @@ -179,6 +187,9 @@ func (c *connector) AfterCreate(ctx context.Context, end *entitlement.Entitlemen
// These two in conjunction make the grant always have `amountToIssue` balance after a reset
ResetMaxRollover: amountToIssue,
ResetMinRollover: amountToIssue,
Metadata: map[string]string{
IssueAfterResetMetaTag: "true",
},
})
if err != nil {
return err
Expand Down
38 changes: 30 additions & 8 deletions internal/entitlement/metered/entitlement.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,26 @@ import (
"github.com/openmeterio/openmeter/pkg/recurrence"
)

const (
DefaultIssueAfterResetPriority = 1
IssueAfterResetMetaTag = "issueAfterReset"
)

// IssueAfterReset defines a default grant's parameters that can be created alongside an entitlement to set up a default balance.
type IssueAfterReset struct {
Amount float64 `json:"amount"`
Priority *uint8 `json:"priority,omitempty"`
}

type Entitlement struct {
entitlement.GenericProperties

// MeasureUsageFrom defines the time from which usage should be measured.
// This is a global value, in most cases the same value as `CreatedAt` should be fine.
MeasureUsageFrom time.Time `json:"measureUsageFrom,omitempty"`

// IssueAfterReset defines an amount of usage that will be issued after a reset.
// This affordance will only be usable until the next reset.
IssuesAfterReset *float64 `json:"issueAfterReset,omitempty"`
// Sets up a default grant
IssueAfterReset *IssueAfterReset `json:"issueAfterReset,omitempty"`

// IsSoftLimit defines if the entitlement is a soft limit. By default when balance falls to 0
// access will be disabled. If this is a soft limit, access will be allowed nonetheless.
Expand All @@ -33,9 +43,9 @@ type Entitlement struct {
}

// HasDefaultGrant returns true if the entitlement has a default grant.
// This is the case when `IssuesAfterReset` is set and greater than 0.
// This is the case when `IssueAfterReset` is set and greater than 0.
func (e *Entitlement) HasDefaultGrant() bool {
return e.IssuesAfterReset != nil && *e.IssuesAfterReset > 0
return e.IssueAfterReset != nil && e.IssueAfterReset.Amount > 0
}

func ParseFromGenericEntitlement(model *entitlement.Entitlement) (*Entitlement, error) {
Expand Down Expand Up @@ -63,14 +73,26 @@ func ParseFromGenericEntitlement(model *entitlement.Entitlement) (*Entitlement,
return nil, &entitlement.InvalidValueError{Message: "CurrentUsagePeriod is required", Type: model.EntitlementType}
}

return &Entitlement{
if model.IssueAfterResetPriority != nil && model.IssueAfterReset == nil {
return nil, &entitlement.InvalidValueError{Message: "IssueAfterReset is required for IssueAfterResetPriority", Type: model.EntitlementType}
}

ent := Entitlement{
GenericProperties: model.GenericProperties,

MeasureUsageFrom: *model.MeasureUsageFrom,
IssuesAfterReset: model.IssueAfterReset,
IsSoftLimit: *model.IsSoftLimit,
UsagePeriod: *model.UsagePeriod,
LastReset: *model.LastReset,
CurrentUsagePeriod: *model.CurrentUsagePeriod,
}, nil
}

if model.IssueAfterReset != nil {
ent.IssueAfterReset = &IssueAfterReset{
Amount: *model.IssueAfterReset,
Priority: model.IssueAfterResetPriority,
}
}

return &ent, nil
}
16 changes: 16 additions & 0 deletions internal/entitlement/postgresadapter/ent/db/entitlement.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 01de191

Please sign in to comment.