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
7 changes: 7 additions & 0 deletions openmeter/entitlement/snapshot/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/openmeterio/openmeter/openmeter/productcatalog/feature"
"github.com/openmeterio/openmeter/openmeter/subject"
"github.com/openmeterio/openmeter/openmeter/watermill/marshaler"
pkgmodels "github.com/openmeterio/openmeter/pkg/models"
"github.com/openmeterio/openmeter/pkg/timeutil"
)

Expand All @@ -39,6 +40,8 @@ func (o ValueOperationType) Validate() error {
return nil
}

var _ pkgmodels.CustomValidator[EntitlementValue] = (*EntitlementValue)(nil)

type EntitlementValue struct {
// Balance Only available for metered entitlements. Metered entitlements are built around a balance calculation where feature usage is deducted from the issued grants. Balance represents the remaining balance of the entitlement, it's value never turns negative.
Balance *float64 `json:"balance,omitempty"`
Expand All @@ -59,6 +62,10 @@ type EntitlementValue struct {
Usage *float64 `json:"usage,omitempty"`
}

func (e EntitlementValue) ValidateWith(validators ...pkgmodels.ValidatorFunc[EntitlementValue]) error {
return pkgmodels.Validate(e, validators...)
}

type SnapshotEvent struct {
Entitlement entitlement.Entitlement `json:"entitlement"`
Namespace models.NamespaceID `json:"namespace"`
Expand Down
54 changes: 28 additions & 26 deletions openmeter/notification/consumer/entitlementbalancethreshold.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,7 @@ import (
"github.com/openmeterio/openmeter/pkg/timeutil"
)

type EntitlementSnapshotHandlerState struct {
TotalGrants float64 `json:"totalGrants"`
}

var ErrNoBalanceAvailable = errors.New("no balance available")
var ErrNoBalanceAvailable = errors.New("no balance available for entitlement")

func (b *EntitlementSnapshotHandler) handleAsSnapshotEvent(ctx context.Context, event snapshot.SnapshotEvent) error {
// TODO[issue-1364]: this must be cached to prevent going to the DB for each balance.snapshot event
Expand Down Expand Up @@ -146,7 +142,7 @@ func (b *EntitlementSnapshotHandler) handleRule(ctx context.Context, balSnapshot

lastEventActualValue, err := getNumericThreshold(
lastEvent.Payload.BalanceThreshold.Threshold,
lastEvent.Payload.BalanceThreshold.Value)
snapshot.EntitlementValue(lastEvent.Payload.BalanceThreshold.Value))
if err != nil {
if errors.Is(err, ErrNoBalanceAvailable) {
// In case there are no grants, percentage all percentage rules would match, so let's instead
Expand Down Expand Up @@ -392,42 +388,48 @@ type numericThreshold struct {

const absoluteZero = 1e-9

func getNumericThreshold(threshold notification.BalanceThreshold, value api.EntitlementValue) (*numericThreshold, error) {
func getNumericThreshold(threshold notification.BalanceThreshold, value snapshot.EntitlementValue) (*numericThreshold, error) {
var (
balance = lo.FromPtr(value.Balance)
usage = lo.FromPtr(value.Usage)
overage = lo.FromPtr(value.Overage)
totalGrants = lo.FromPtr(value.TotalAvailableGrantAmount)
// Balance = TotalAvailableGrants - Usage
balance = lo.FromPtr(value.Balance)
// Usage = TotalAvailableGrants - Balance. It is only available if active grants are available in the current usage period.
usage = lo.FromPtr(value.Usage)
// Overage means the usage which is not covered by the grants.
overage = lo.FromPtr(value.Overage)
// TotalAvailableGrants means all the active grants available for in the current usage period.
totalAvailableGrants = lo.FromPtr(value.TotalAvailableGrantAmount)
// Total usage is the sum of the usage and overage
totalUsage = usage + overage
)

// If no grants are available, we cannot calculate the threshold value.
if totalGrants == 0 {
return nil, ErrNoBalanceAvailable
}

// Invalid entitlement value as there cannot be overage if the balance is not zero.
if balance > absoluteZero && overage > absoluteZero {
return nil, errors.New("balance and overage cannot be positive number at the same time")
}

switch threshold.Type {
// Deprecated: obsoleted by api.NotificationRuleBalanceThresholdValueTypeUsageValue
case api.NotificationRuleBalanceThresholdValueTypeUsageValue, api.NotificationRuleBalanceThresholdValueTypeNumber:
return &numericThreshold{
BalanceThreshold: threshold,
ThresholdValue: threshold.Value,
Active: threshold.Value < usage,
Active: threshold.Value < totalUsage,
}, nil
// Deprecated: obsoleted by api.NotificationRuleBalanceThresholdValueTypeUsagePercentage
case api.NotificationRuleBalanceThresholdValueTypeUsagePercentage, api.NotificationRuleBalanceThresholdValueTypePercent:
thresholdValue := totalGrants * (threshold.Value / 100)
// If no grants are available, we cannot calculate the threshold value.
if totalAvailableGrants <= absoluteZero {
return nil, ErrNoBalanceAvailable
}

thresholdValue := totalAvailableGrants * (threshold.Value / 100)

return &numericThreshold{
BalanceThreshold: threshold,
ThresholdValue: thresholdValue,
Active: thresholdValue < usage,
Active: thresholdValue < totalUsage,
}, nil
case api.NotificationRuleBalanceThresholdValueTypeBalanceValue:
// If no grants are available, we cannot calculate the threshold value.
if totalAvailableGrants <= absoluteZero {
return nil, ErrNoBalanceAvailable
}

active := threshold.Value > balance

if threshold.Value == 0 {
Expand All @@ -440,7 +442,7 @@ func getNumericThreshold(threshold notification.BalanceThreshold, value api.Enti
Active: active,
}, nil
default:
return nil, errors.New("unknown threshold type")
return nil, fmt.Errorf("unknown threshold type: %s", threshold.Type)
}
}

Expand Down Expand Up @@ -468,7 +470,7 @@ func getActiveThresholdsWithHighestPriority(thresholds []notification.BalanceThr
)

for _, threshold := range thresholds {
numThreshold, err := getNumericThreshold(threshold, api.EntitlementValue(value))
numThreshold, err := getNumericThreshold(threshold, value)
if err != nil {
if errors.Is(err, ErrNoBalanceAvailable) {
continue
Expand Down
Loading
Loading