Skip to content

Commit

Permalink
feat: add measureUsageFrom input OM-751 (#1170)
Browse files Browse the repository at this point in the history
* feat: add measureUsageFrom input

* style: fix lint

* feat: add enum value for the default behavior

* fix: lint

* fix(e2e): increase waitTime to 10s
  • Loading branch information
GAlexIHU authored Jul 29, 2024
1 parent d03ec4a commit c78b4b4
Show file tree
Hide file tree
Showing 12 changed files with 977 additions and 568 deletions.
647 changes: 370 additions & 277 deletions api/api.gen.go

Large diffs are not rendered by default.

647 changes: 370 additions & 277 deletions api/client/go/client.gen.go

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions api/client/node/schemas/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,17 @@ export interface components {
usagePeriod?: components['schemas']['RecurringPeriod']
currentUsagePeriod?: components['schemas']['Period']
}
/** @enum {string} */
MeasureUsageFromEnum: 'CURRENT_PERIOD_START' | 'NOW'
/** Format: date-time */
MeasureUsageFromTime: string
/**
* @description The time from which usage is measured, defaults to the entitlement creation time.
* The provided value is truncated to the granularity of the underlying meter.
*/
MeasureUsageFrom:
| components['schemas']['MeasureUsageFromEnum']
| components['schemas']['MeasureUsageFromTime']
EntitlementMeteredCreateInputs: components['schemas']['EntitlementCreateSharedFields'] & {
/**
* @example metered
Expand All @@ -727,6 +738,7 @@ export interface components {
*/
isUnlimited?: boolean
usagePeriod: components['schemas']['RecurringPeriodCreateInput']
measureUsageFrom?: components['schemas']['MeasureUsageFrom']
/**
* Format: double
* @description You can grant usage automatically alongside the entitlement, the example scenario would be creating a starting balance. If an amount is specified here, a grant will be created alongside the entitlement with the specified amount.
Expand Down Expand Up @@ -761,6 +773,11 @@ export interface components {
*/
lastReset: string
currentUsagePeriod: components['schemas']['Period']
/**
* Format: date-time
* @description The time from which usage is measured. If not specified on creation, defaults to the entitlement creation time.
*/
measureUsageFrom: string
}
EntitlementStaticCreateInputs: components['schemas']['EntitlementCreateSharedFields'] & {
/**
Expand Down
1 change: 1 addition & 0 deletions api/client/node/test/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export const mockEntitlement: Entitlement = {
from: '2024-01-01T00:00:00Z',
to: '2024-01-01T00:00:00Z',
},
measureUsageFrom: '2024-01-01T00:00:00Z',
issueAfterReset: mockCreateEntitlementInput.issueAfterReset,
lastReset: '2024-01-01T00:00:00Z',
createdAt: '2024-01-01T00:00:00Z',
Expand Down
17 changes: 17 additions & 0 deletions api/client/web/src/client/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,17 @@ export interface components {
usagePeriod?: components['schemas']['RecurringPeriod']
currentUsagePeriod?: components['schemas']['Period']
}
/** @enum {string} */
MeasureUsageFromEnum: 'CURRENT_PERIOD_START' | 'NOW'
/** Format: date-time */
MeasureUsageFromTime: string
/**
* @description The time from which usage is measured, defaults to the entitlement creation time.
* The provided value is truncated to the granularity of the underlying meter.
*/
MeasureUsageFrom:
| components['schemas']['MeasureUsageFromEnum']
| components['schemas']['MeasureUsageFromTime']
EntitlementMeteredCreateInputs: components['schemas']['EntitlementCreateSharedFields'] & {
/**
* @example metered
Expand All @@ -727,6 +738,7 @@ export interface components {
*/
isUnlimited?: boolean
usagePeriod: components['schemas']['RecurringPeriodCreateInput']
measureUsageFrom?: components['schemas']['MeasureUsageFrom']
/**
* Format: double
* @description You can grant usage automatically alongside the entitlement, the example scenario would be creating a starting balance. If an amount is specified here, a grant will be created alongside the entitlement with the specified amount.
Expand Down Expand Up @@ -761,6 +773,11 @@ export interface components {
*/
lastReset: string
currentUsagePeriod: components['schemas']['Period']
/**
* Format: date-time
* @description The time from which usage is measured. If not specified on creation, defaults to the entitlement creation time.
*/
measureUsageFrom: string
}
EntitlementStaticCreateInputs: components['schemas']['EntitlementCreateSharedFields'] & {
/**
Expand Down
23 changes: 23 additions & 0 deletions api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1980,6 +1980,21 @@ components:
$ref: "#/components/schemas/RecurringPeriod"
currentUsagePeriod:
$ref: "#/components/schemas/Period"
MeasureUsageFromEnum:
type: string
enum:
- CURRENT_PERIOD_START
- NOW
MeasureUsageFromTime:
type: string
format: date-time
MeasureUsageFrom:
description: |
The time from which usage is measured, defaults to the entitlement creation time.
The provided value is truncated to the granularity of the underlying meter.
oneOf:
- $ref: "#/components/schemas/MeasureUsageFromEnum"
- $ref: "#/components/schemas/MeasureUsageFromTime"
EntitlementMeteredCreateInputs:
allOf:
- $ref: "#/components/schemas/EntitlementCreateSharedFields"
Expand Down Expand Up @@ -2007,6 +2022,8 @@ components:
Deprecated, ignored by the backend. Please use isSoftLimit instead; this field will be removed in the future.
usagePeriod:
$ref: "#/components/schemas/RecurringPeriodCreateInput"
measureUsageFrom:
$ref: "#/components/schemas/MeasureUsageFrom"
issueAfterReset:
type: number
format: double
Expand Down Expand Up @@ -2049,6 +2066,7 @@ components:
required:
- lastReset
- currentUsagePeriod
- measureUsageFrom
properties:
lastReset:
description: |
Expand All @@ -2058,6 +2076,11 @@ components:
example: "2023-01-01T00:00:00Z"
currentUsagePeriod:
$ref: "#/components/schemas/Period"
measureUsageFrom:
type: string
format: date-time
description: |
The time from which usage is measured. If not specified on creation, defaults to the entitlement creation time.
EntitlementStaticCreateInputs:
allOf:
- $ref: "#/components/schemas/EntitlementCreateSharedFields"
Expand Down
39 changes: 37 additions & 2 deletions e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ func TestCredit(t *testing.T) {
var featureId *api.FeatureId
var featureKey string

const waitTime = time.Second * 4
const waitTime = time.Second * 10

t.Run("Create Feature", func(t *testing.T) {
randKey := ulid.Make().String()
Expand Down Expand Up @@ -559,18 +559,23 @@ func TestCredit(t *testing.T) {

t.Run("Create a Entitlement With Default Grants", func(t *testing.T) {
randSubject := ulid.Make().String()
measureUsageFrom := time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC)
muf := &api.MeasureUsageFrom{}
err := muf.FromMeasureUsageFromTime(measureUsageFrom)
require.NoError(t, err)
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",
},
MeasureUsageFrom: muf,
IssueAfterReset: convert.ToPointer(100.0),
IssueAfterResetPriority: convert.ToPointer(6),
}
body := &api.CreateEntitlementJSONRequestBody{}
err := body.FromEntitlementMeteredCreateInputs(meteredEntitlement)
err = body.FromEntitlementMeteredCreateInputs(meteredEntitlement)
require.NoError(t, err)
resp, err := client.CreateEntitlementWithResponse(context.Background(), randSubject, *body)

Expand All @@ -581,6 +586,7 @@ func TestCredit(t *testing.T) {
require.NoError(t, err)

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

// fetch grants for entitlement
grantListResp, err := client.ListEntitlementGrantsWithResponse(context.Background(), randSubject, *metered.Id, nil)
Expand All @@ -596,6 +602,35 @@ func TestCredit(t *testing.T) {
"issueAfterReset": "true",
}, *(*grantListResp.JSON200)[0].Metadata)
})
t.Run("Create a Entitlement With MeasureUsageFrom enum", func(t *testing.T) {
randSubject := ulid.Make().String()
periodAnchor := time.Now().Truncate(time.Minute).Add(-time.Hour).In(time.UTC)
muf := &api.MeasureUsageFrom{}
err := muf.FromMeasureUsageFromEnum(api.CURRENTPERIODSTART)
require.NoError(t, err)
meteredEntitlement := api.EntitlementMeteredCreateInputs{
Type: "metered",
FeatureId: featureId,
UsagePeriod: api.RecurringPeriodCreateInput{
Anchor: &periodAnchor,
Interval: "MONTH",
},
MeasureUsageFrom: muf,
}
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)
require.Equal(t, periodAnchor, metered.MeasureUsageFrom)
})

t.Run("Create Grant", func(t *testing.T) {
effectiveAt := time.Now().Truncate(time.Minute)
Expand Down
69 changes: 63 additions & 6 deletions internal/entitlement/entitlement.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package entitlement

import (
"fmt"
"slices"
"time"

"github.com/openmeterio/openmeter/pkg/clock"
"github.com/openmeterio/openmeter/pkg/models"
"github.com/openmeterio/openmeter/pkg/recurrence"
"github.com/openmeterio/openmeter/pkg/slicesx"
Expand All @@ -12,6 +15,60 @@ type TypedEntitlement interface {
GetType() EntitlementType
}

type MeasureUsageFromEnum string

const (
MeasureUsageFromCurrentPeriodStart MeasureUsageFromEnum = "CURRENT_PERIOD_START"
MeasureUsageFromNow MeasureUsageFromEnum = "NOW"
)

func (e MeasureUsageFromEnum) Values() []MeasureUsageFromEnum {
return []MeasureUsageFromEnum{MeasureUsageFromCurrentPeriodStart, MeasureUsageFromNow}
}

func (e MeasureUsageFromEnum) Validate() error {
if !slices.Contains(e.Values(), e) {
return fmt.Errorf("invalid value")
}
return nil
}

type MeasureUsageFromInput struct {
ts time.Time
}

func (m MeasureUsageFromInput) Get() time.Time {
return m.ts
}

func (m *MeasureUsageFromInput) FromTime(t time.Time) error {
if t.IsZero() {
return fmt.Errorf("time is zero")
}

m.ts = t
return nil
}

func (m *MeasureUsageFromInput) FromEnum(e MeasureUsageFromEnum, p UsagePeriod, t time.Time) error {
if err := e.Validate(); err != nil {
return err
}
switch e {
case MeasureUsageFromCurrentPeriodStart:
period, err := p.GetCurrentPeriodAt(clock.Now())
if err != nil {
return err
}
m.ts = period.From
case MeasureUsageFromNow:
m.ts = t
default:
return fmt.Errorf("unsupported enum value")
}
return nil
}

type CreateEntitlementInputs struct {
Namespace string `json:"namespace"`
FeatureID *string `json:"featureId"`
Expand All @@ -20,12 +77,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"`
IssueAfterResetPriority *uint8 `json:"issueAfterResetPriority,omitempty"`
IsSoftLimit *bool `json:"isSoftLimit,omitempty"`
Config []byte `json:"config,omitempty"`
UsagePeriod *UsagePeriod `json:"usagePeriod,omitempty"`
MeasureUsageFrom *MeasureUsageFromInput `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 Down
38 changes: 38 additions & 0 deletions internal/entitlement/entitlement_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,41 @@ func TestUsagePeriod(t *testing.T) {
assert.Equal(t, t1.AddDate(0, 0, 1), period.To)
})
}

func TestMeasureUsageFromInput(t *testing.T) {
t.Run("Should return time from input", func(t *testing.T) {
t1 := time.Now().Truncate(time.Minute)
m := &entitlement.MeasureUsageFromInput{}
err := m.FromTime(t1)
assert.NoError(t, err)
assert.Equal(t, t1, m.Get())
})

t.Run("Should return time from CURRENT_PERIOD_START enum", func(t *testing.T) {
t0 := time.Now().Truncate(time.Minute)
t1 := t0.Add(-time.Hour)
up := entitlement.UsagePeriod{
Interval: recurrence.RecurrencePeriodDaily,
Anchor: t1,
}

m := &entitlement.MeasureUsageFromInput{}
err := m.FromEnum(entitlement.MeasureUsageFromCurrentPeriodStart, up, t0)
assert.NoError(t, err)
assert.Equal(t, t1, m.Get())
})

t.Run("Should return time from CREATED_AT enum", func(t *testing.T) {
t0 := time.Now().Truncate(time.Minute)
t1 := t0.Add(-time.Hour)
up := entitlement.UsagePeriod{
Interval: recurrence.RecurrencePeriodDaily,
Anchor: t1,
}

m := &entitlement.MeasureUsageFromInput{}
err := m.FromEnum(entitlement.MeasureUsageFromNow, up, t0)
assert.NoError(t, err)
assert.Equal(t, t0, m.Get())
})
}
26 changes: 26 additions & 0 deletions internal/entitlement/httpdriver/entitlement.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,32 @@ func (h *entitlementHandler) CreateEntitlement() CreateEntitlementHandler {
if v.Metadata != nil {
request.Metadata = *v.Metadata
}
if v.MeasureUsageFrom != nil {
measureUsageFrom := &entitlement.MeasureUsageFromInput{}
apiTime, err := v.MeasureUsageFrom.AsMeasureUsageFromTime()
if err == nil {
err := measureUsageFrom.FromTime(apiTime)
if err != nil {
return request, err
}
} else {
apiEnum, err := v.MeasureUsageFrom.AsMeasureUsageFromEnum()
if err != nil {
return request, err
}

// sanity check
if request.UsagePeriod == nil {
return request, errors.New("usage period is required for enum measure usage from")
}

err = measureUsageFrom.FromEnum(entitlement.MeasureUsageFromEnum(apiEnum), *request.UsagePeriod, clock.Now())
if err != nil {
return request, err
}
}
request.MeasureUsageFrom = measureUsageFrom
}
case api.EntitlementStaticCreateInputs:
request = entitlement.CreateEntitlementInputs{
Namespace: ns,
Expand Down
1 change: 1 addition & 0 deletions internal/entitlement/httpdriver/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func (parser) ToMetered(e *entitlement.Entitlement) (*api.EntitlementMetered, er
return convert.ToPointer(int(p))
})
}),
MeasureUsageFrom: metered.MeasureUsageFrom,
Metadata: convert.MapToPointer(metered.Metadata),
SubjectKey: metered.SubjectKey,
Type: api.EntitlementMeteredType(metered.EntitlementType),
Expand Down
Loading

0 comments on commit c78b4b4

Please sign in to comment.