Skip to content
Closed
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
2,690 changes: 1,500 additions & 1,190 deletions api/api.gen.go

Large diffs are not rendered by default.

2,630 changes: 1,470 additions & 1,160 deletions api/client/go/client.gen.go

Large diffs are not rendered by default.

313 changes: 300 additions & 13 deletions api/client/javascript/src/client/schemas.ts

Large diffs are not rendered by default.

289 changes: 267 additions & 22 deletions api/client/javascript/src/zod/index.ts

Large diffs are not rendered by default.

355 changes: 341 additions & 14 deletions api/openapi.cloud.yaml

Large diffs are not rendered by default.

355 changes: 341 additions & 14 deletions api/openapi.yaml

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions api/spec/src/entitlements/v2/customer.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ interface CustomerEntitlements {
@operationId("createCustomerEntitlementV2")
post(
@path customerIdOrKey: ULIDOrExternalKey,
@body entitlement: OpenMeter.Entitlements.EntitlementCreateInputs,
@body entitlement: EntitlementV2CreateInputs,
): {
@statusCode _: 201;
@body body: EntitlementV2;
Expand Down Expand Up @@ -88,7 +88,7 @@ interface CustomerEntitlements {
override(
@path customerIdOrKey: ULIDOrExternalKey,
@path entitlementIdOrFeatureKey: ULIDOrExternalKey,
@body entitlement: EntitlementCreateInputs,
@body entitlement: EntitlementV2CreateInputs,
):
| {
@statusCode _: 201;
Expand Down Expand Up @@ -117,7 +117,7 @@ interface CustomerEntitlement {
...OpenMeter.QueryPagination,
...OpenMeter.QueryLimitOffset,
...OpenMeter.QueryOrdering<GrantOrderBy>,
): OpenMeter.PaginatedResponse<Grant> | OpenMeter.CommonErrors;
): OpenMeter.PaginatedResponse<GrantV2> | OpenMeter.CommonErrors;

/**
* Grants define a behavior of granting usage for a metered entitlement. They can have complicated recurrence and rollover rules, thanks to which you can define a wide range of access patterns with a single grant, in most cases you don't have to periodically create new grants. You can only issue grants for active metered entitlements.
Expand All @@ -139,10 +139,10 @@ interface CustomerEntitlement {
createCustomerEntitlementGrant(
@path customerIdOrKey: ULIDOrExternalKey,
@path entitlementIdOrFeatureKey: ULIDOrKey,
@body grant: GrantCreateInput,
@body grant: GrantCreateInputV2,
): {
@statusCode _: 201;
@body body: Grant;
@body body: GrantV2;
} | OpenMeter.CommonErrors | OpenMeter.ConflictError;

/**
Expand Down
78 changes: 77 additions & 1 deletion api/spec/src/entitlements/v2/entitlements.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,17 @@ union EntitlementV2 {
boolean: EntitlementBooleanV2,
}

/**
* Create inputs for entitlement
*/
@discriminated(#{ envelope: "none", discriminatorPropertyName: "type" })
@friendlyName("EntitlementV2CreateInputs")
union EntitlementV2CreateInputs {
metered: EntitlementMeteredV2CreateInputs,
static: EntitlementStaticCreateInputs,
boolean: EntitlementBooleanCreateInputs,
}

/**
* Customer fields for entitlements
*/
Expand All @@ -105,6 +116,70 @@ model EntitlementCustomerFields {
customerId: ULID;
}

/**
* Issue after reset
*/
@friendlyName("IssueAfterReset")
model IssueAfterReset {
/**
* The initial grant amount
*/
@summary("Initial grant amount")
@minValue(0)
amount: float64;

/**
* The priority of the issue after reset
*/
@summary("Issue grant after reset priority")
@minValue(1)
@maxValue(255)
priority?: uint8 = 1;
}

/**
* Create inputs for metered entitlement
*/
@friendlyName("EntitlementMeteredV2CreateInputs")
model EntitlementMeteredV2CreateInputs {
...OmitProperties<
OpenMeter.Entitlements.EntitlementMeteredCreateInputs,
"isUnlimited" | "issueAfterReset" | "issueAfterResetPriority"
>;

/**
* 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.
* That grant will have it's rollover settings configured in a way that after each reset operation, the balance will return the original amount specified here.
* Manually creating such a grant would mean having the "amount", "minRolloverAmount", and "maxRolloverAmount" fields all be the same.
*/
#deprecated "Use issue.Amount instead, will be removed in next major version."
@minValue(0)
@summary("Initial grant amount")
issueAfterReset?: float64;

/**
* Defines the grant priority for the default grant.
*/
#deprecated "Use issue.Priority instead, will be removed in next major version."
@minValue(1)
@maxValue(255)
@summary("Issue grant after reset priority")
issueAfterResetPriority?: uint8 = 1;

/**
* Issue after reset
*/
@summary("Issue after reset")
issue?: IssueAfterReset;

/**
* Grants
*/
@summary("Grants")
grants?: GrantCreateInputV2[];
}

/**
* Metered entitlements are useful for many different use cases, from setting up usage based access to implementing complex credit systems.
* Access is determined based on feature usage using a balance calculation (the "usage allowance" provided by the issued grants is "burnt down" by the usage).
Expand All @@ -113,7 +188,7 @@ model EntitlementCustomerFields {
model EntitlementMeteredV2 {
type: EntitlementType.metered;
...OmitProperties<
EntitlementMeteredCreateInputs,
EntitlementMeteredV2CreateInputs,

| "type"
| "measureUsageFrom"
Expand All @@ -122,6 +197,7 @@ model EntitlementMeteredV2 {
| "featureKey"
| "featureId"
| "currentUsagePeriod"
| "grants"
>;
...OmitProperties<
EntitlementSharedFields,
Expand Down
90 changes: 90 additions & 0 deletions api/spec/src/entitlements/v2/grant.tsp
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import "@typespec/http";
import "@typespec/rest";
import "@typespec/openapi3";
import "..";
import "../..";

namespace OpenMeter.Entitlements.V2;

/**
* The grant.
*/
@friendlyName("EntitlementGrantV2")
model GrantV2 {
...ResourceTimestamps;
...OmitProperties<GrantCreateInputV2, "recurrence">;

/**
* Readonly unique ULID identifier.
*/
@example("01ARZ3NDEKTSV4RRFFQ69G5FAV")
@visibility(Lifecycle.Read)
id: ULID;

/**
* The unique entitlement ULID that the grant is associated with.
*/
@example("01ARZ3NDEKTSV4RRFFQ69G5FAV")
@visibility(Lifecycle.Read)
entitlementId: string;

/**
* The next time the grant will recurr.
*/
@example(DateTime.fromISO("2023-01-01T01:01:01.001Z"))
nextRecurrence?: DateTime;

/**
* The time the grant expires.
*/
@example(DateTime.fromISO("2023-01-01T01:01:01.001Z"))
@visibility(Lifecycle.Read)
expiresAt?: DateTime;

/**
* The time the grant was voided.
*/
@example(DateTime.fromISO("2023-01-01T01:01:01.001Z"))
voidedAt?: DateTime;

/**
* The recurrence period of the grant.
*/
recurrence?: RecurringPeriod;
}

/**
* The grant creation input.
*/
@friendlyName("EntitlementGrantCreateInputV2")
model GrantCreateInputV2 {
...OmitProperties<
OpenMeter.Entitlements.GrantCreateInput,
"maxRolloverAmount" | "metadata" | "expiration"
>;

/**
* Grants are rolled over at reset, after which they can have a different balance compared to what they had before the reset. The default value equals grant amount.
* Balance after the reset is calculated as: Balance_After_Reset = MIN(MaxRolloverAmount, MAX(Balance_Before_Reset, MinRolloverAmount))
*/
@example(100.0)
maxRolloverAmount?: float64;

/**
* The grant expiration definition. If no expiration is provided, the grant can be active indefinitely.
*/
expiration?: ExpirationPeriod;

/**
* The grant metadata.
*/
#deprecated "Use annotations instead, will be removed in next major version."
@example(#{ stripePaymentId: "pi_4OrAkhLvyihio9p51h9iiFnB" })
metadata?: Metadata;

/**
* The grant metadata.
*/
@example(#{ stripePaymentId: "pi_4OrAkhLvyihio9p51h9iiFnB" })
annotations?: Annotations;
}
2 changes: 1 addition & 1 deletion api/spec/src/entitlements/v2/grants.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,5 @@ interface Grants {
...OpenMeter.QueryPagination,
...OpenMeter.QueryLimitOffset,
...OpenMeter.QueryOrdering<GrantOrderBy>,
): OpenMeter.PaginatedResponse<Grant> | OpenMeter.CommonErrors;
): OpenMeter.PaginatedResponse<GrantV2> | OpenMeter.CommonErrors;
}
1 change: 1 addition & 0 deletions api/spec/src/entitlements/v2/main.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import "..";
import "./entitlements.tsp";
import "./customer.tsp";
import "./grants.tsp";
import "./grant.tsp";

using TypeSpec.Http;
using TypeSpec.Rest;
Expand Down
99 changes: 93 additions & 6 deletions e2e/entitlement_parity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func TestEntitlementParitySuite(t *testing.T) {

// Customer-based create (v2)
{
metered := api.EntitlementMeteredCreateInputs{
metered := api.EntitlementMeteredV2CreateInputs{
Type: "metered",
FeatureId: &feature2ID,
UsagePeriod: api.RecurringPeriodCreateInput{
Expand All @@ -111,7 +111,7 @@ func TestEntitlementParitySuite(t *testing.T) {
},
}
var body api.CreateCustomerEntitlementV2JSONRequestBody
require.NoError(t, body.FromEntitlementMeteredCreateInputs(metered))
require.NoError(t, body.FromEntitlementMeteredV2CreateInputs(metered))

resp, err := client.CreateCustomerEntitlementV2WithResponse(ctx, customerID, body)
require.NoError(t, err)
Expand Down Expand Up @@ -194,7 +194,7 @@ func TestEntitlementParitySuite(t *testing.T) {
resp, err := client.CreateCustomerEntitlementGrantV2WithResponse(ctx, customerID, customerEntitlementFeatureKey, api.CreateCustomerEntitlementGrantV2JSONRequestBody{
Amount: grantAmount,
EffectiveAt: effectiveAt,
Expiration: api.ExpirationPeriod{Duration: "MONTH", Count: 1},
Expiration: &api.ExpirationPeriod{Duration: "MONTH", Count: 1},
})
require.NoError(t, err)
require.Equal(t, http.StatusCreated, resp.StatusCode(), "Invalid status code [response_body=%s]", string(resp.Body))
Expand All @@ -205,7 +205,7 @@ func TestEntitlementParitySuite(t *testing.T) {
resp, err := client.CreateCustomerEntitlementGrantV2WithResponse(ctx, customerID, feature1Key, api.CreateCustomerEntitlementGrantV2JSONRequestBody{
Amount: grantAmount,
EffectiveAt: effectiveAt,
Expiration: api.ExpirationPeriod{Duration: "MONTH", Count: 1},
Expiration: &api.ExpirationPeriod{Duration: "MONTH", Count: 1},
})
require.NoError(t, err)
require.Equal(t, http.StatusCreated, resp.StatusCode(), "Invalid status code [response_body=%s]", string(resp.Body))
Expand Down Expand Up @@ -233,7 +233,7 @@ func TestEntitlementParitySuite(t *testing.T) {
}

// v2 list grants for subject entitlement (by feature key)
var v2GrantsForV1Entitlement api.GrantPaginatedResponse
var v2GrantsForV1Entitlement api.GrantV2PaginatedResponse
{
resp, err := client.ListCustomerEntitlementGrantsV2WithResponse(ctx, customerID, feature1Key, &api.ListCustomerEntitlementGrantsV2Params{})
require.NoError(t, err)
Expand All @@ -243,7 +243,7 @@ func TestEntitlementParitySuite(t *testing.T) {
}

// v2 list grants for v2 entitlement (by feature key)
var v2Grants api.GrantPaginatedResponse
var v2Grants api.GrantV2PaginatedResponse
{
resp, err := client.ListCustomerEntitlementGrantsV2WithResponse(ctx, customerID, customerEntitlementFeatureKey, &api.ListCustomerEntitlementGrantsV2Params{})
require.NoError(t, err)
Expand Down Expand Up @@ -369,3 +369,90 @@ func TestEntitlementParitySuite(t *testing.T) {
})
})
}

func TestEntitlementDifferences(t *testing.T) {
client := initClient(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

meterSlug := "entitlement_parity_meter"

// Test data
customerKey := fmt.Sprintf("parity_cust_%d_2", time.Now().Unix())
subjectKey := customerKey + "-subject"

// Create customer and subject mapping
cust := CreateCustomerWithSubject(t, client, customerKey, subjectKey)
customerID := cust.Id

// Create a feature to use across both flows
var featureID string
var feature1Key string
{
randKey := fmt.Sprintf("entitlement_parity_feature_1_%d_2", time.Now().Unix())
resp, err := client.CreateFeatureWithResponse(ctx, api.CreateFeatureJSONRequestBody{
Name: "Entitlement Parity Feature",
MeterSlug: convert.ToPointer(meterSlug),
Key: randKey,
})
require.NoError(t, err)
require.Equal(t, http.StatusCreated, resp.StatusCode(), "Invalid status code [response_body=%s]", string(resp.Body))
featureID = resp.JSON201.Id
feature1Key = randKey
}

// Usage period for requests
month := &api.RecurringPeriodInterval{}
require.NoError(t, month.FromRecurringPeriodIntervalEnum(api.RecurringPeriodIntervalEnumMONTH))
anchor := convert.ToPointer(time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC))

t.Run("New API should be able to create grants without expiration", func(t *testing.T) {
t.Run("Should create entitlement through V2 API with default grants", func(t *testing.T) {
metered := api.EntitlementMeteredV2CreateInputs{
Type: "metered",
FeatureId: &featureID,
UsagePeriod: api.RecurringPeriodCreateInput{
Anchor: anchor,
Interval: *month,
},
Grants: &[]api.EntitlementGrantCreateInputV2{
{
Amount: 100,
EffectiveAt: time.Now().Truncate(time.Minute).Add(time.Minute),
Expiration: nil,
},
},
}
var body api.CreateCustomerEntitlementV2JSONRequestBody
require.NoError(t, body.FromEntitlementMeteredV2CreateInputs(metered))

// Let's create an entitlement with a grant
entRes, err := client.CreateCustomerEntitlementV2WithResponse(ctx, customerID, body)
require.NoError(t, err)
require.Equal(t, http.StatusCreated, entRes.StatusCode(), "Invalid status code [response_body=%s]", string(entRes.Body))

v2EntGrants, err := client.ListCustomerEntitlementGrantsV2WithResponse(ctx, customerID, feature1Key, &api.ListCustomerEntitlementGrantsV2Params{})
require.NoError(t, err)
require.Equal(t, http.StatusOK, v2EntGrants.StatusCode(), "Invalid status code [response_body=%s]", string(v2EntGrants.Body))

require.NotNil(t, v2EntGrants.JSON200)
require.GreaterOrEqual(t, len(v2EntGrants.JSON200.Items), 1, "Invalid number of grants [response_body=%s]", string(v2EntGrants.Body))

for _, grant := range v2EntGrants.JSON200.Items {
require.Nil(t, grant.Expiration)
}
})

t.Run("Old API should still be able to list grants without expiration with filled-in dummy values", func(t *testing.T) {
v1EntGrants, err := client.ListEntitlementGrantsWithResponse(ctx, subjectKey, feature1Key, &api.ListEntitlementGrantsParams{})
require.NoError(t, err)
require.Equal(t, http.StatusOK, v1EntGrants.StatusCode(), "Invalid status code [response_body=%s]", string(v1EntGrants.Body))
require.NotNil(t, v1EntGrants.JSON200)

require.GreaterOrEqual(t, len(lo.FromPtr(v1EntGrants.JSON200)), 1, "Invalid number of grants [response_body=%s]", string(v1EntGrants.Body))
for _, grant := range lo.FromPtr(v1EntGrants.JSON200) {
require.NotNil(t, grant.Expiration)
}
})
})
}
Loading