| package usagebased |
|
|
| import ( |
| "errors" |
| "fmt" |
| "time" |
|
|
| "github.com/samber/lo" |
|
|
| "github.com/openmeterio/openmeter/openmeter/billing" |
| "github.com/openmeterio/openmeter/openmeter/billing/charges/meta" |
| "github.com/openmeterio/openmeter/openmeter/billing/models/totals" |
| "github.com/openmeterio/openmeter/openmeter/customer" |
| "github.com/openmeterio/openmeter/openmeter/productcatalog" |
| "github.com/openmeterio/openmeter/openmeter/productcatalog/feature" |
| "github.com/openmeterio/openmeter/pkg/currencyx" |
| "github.com/openmeterio/openmeter/pkg/models" |
| "github.com/openmeterio/openmeter/pkg/ref" |
| "github.com/openmeterio/openmeter/pkg/timeutil" |
| ) |
|
|
| var _ meta.ChargeAccessor = (*ChargeBase)(nil) |
|
|
| type ChargeBase struct { |
| meta.ManagedResource |
|
|
| Intent OverridableIntent `json:"intent"` |
| Status Status `json:"status"` |
|
|
| State State `json:"state"` |
| } |
|
|
| func (c ChargeBase) Validate() error { |
| var errs []error |
|
|
| if err := c.ManagedResource.Validate(); err != nil { |
| errs = append(errs, fmt.Errorf("managed resource: %w", err)) |
| } |
|
|
| if err := c.Intent.Validate(); err != nil { |
| errs = append(errs, fmt.Errorf("intent: %w", err)) |
| } |
|
|
| if err := c.Status.Validate(); err != nil { |
| errs = append(errs, fmt.Errorf("status: %w", err)) |
| } |
|
|
| if err := c.State.Validate(); err != nil { |
| errs = append(errs, fmt.Errorf("state: %w", err)) |
| } |
|
|
| return models.NewNillableGenericValidationError(errors.Join(errs...)) |
| } |
|
|
| func (c ChargeBase) GetChargeID() meta.ChargeID { |
| return meta.ChargeID{ |
| Namespace: c.Namespace, |
| ID: c.ID, |
| } |
| } |
|
|
| func (c ChargeBase) GetCustomerID() customer.CustomerID { |
| return customer.CustomerID{ |
| Namespace: c.Namespace, |
| ID: c.Intent.GetCustomerID(), |
| } |
| } |
|
|
| func (c ChargeBase) GetCurrency() currencyx.Code { |
| return c.Intent.GetCurrency() |
| } |
|
|
| |
| |
| func (c ChargeBase) GetIntentDeletedAt() *time.Time { |
| return c.Intent.GetDeletedAt() |
| } |
|
|
| func (c ChargeBase) ErrorAttributes() models.Attributes { |
| return models.Attributes{ |
| "charge_id": c.ID, |
| "namespace": c.Namespace, |
| "charge_type": string(meta.ChargeTypeUsageBased), |
| } |
| } |
|
|
| var _ meta.ChargeAccessor = (*Charge)(nil) |
|
|
| type Charge struct { |
| ChargeBase |
|
|
| Realizations RealizationRuns `json:"realizations"` |
| Expands Expands `json:"expands"` |
| } |
|
|
| type Charges []Charge |
|
|
| func (c Charge) GetStatus() Status { |
| return c.Status |
| } |
|
|
| func (c Charge) WithStatus(status Status) Charge { |
| c.Status = status |
| return c |
| } |
|
|
| func (c Charge) GetBase() ChargeBase { |
| return c.ChargeBase |
| } |
|
|
| func (c Charge) WithBase(base ChargeBase) Charge { |
| c.ChargeBase = base |
| return c |
| } |
|
|
| func (c Charge) Validate() error { |
| var errs []error |
|
|
| if err := c.ChargeBase.Validate(); err != nil { |
| errs = append(errs, fmt.Errorf("charge base: %w", err)) |
| } |
|
|
| if err := c.Expands.Validate(); err != nil { |
| errs = append(errs, fmt.Errorf("expands: %w", err)) |
| } |
|
|
| return models.NewNillableGenericValidationError(errors.Join(errs...)) |
| } |
|
|
| func (c Charge) GetCurrentRealizationRun() (RealizationRun, error) { |
| if c.State.CurrentRealizationRunID == nil { |
| return RealizationRun{}, fmt.Errorf("no current realization run") |
| } |
|
|
| return c.Realizations.GetByID(*c.State.CurrentRealizationRunID) |
| } |
|
|
| func (c Charge) GetCustomerID() customer.CustomerID { |
| return customer.CustomerID{ |
| Namespace: c.Namespace, |
| ID: c.Intent.GetCustomerID(), |
| } |
| } |
|
|
| func (c Charge) GetFeatureKeyOrID() ref.IDOrKey { |
| |
| |
| switch c.Status { |
| case StatusCreated: |
| return ref.IDOrKey{ |
| Key: c.Intent.GetBaseIntent().FeatureKey, |
| } |
| case StatusDeleted: |
| if c.State.FeatureID != "" { |
| return ref.IDOrKey{ |
| ID: c.State.FeatureID, |
| } |
| } |
|
|
| return ref.IDOrKey{ |
| Key: c.Intent.GetBaseIntent().FeatureKey, |
| } |
| default: |
| return ref.IDOrKey{ |
| ID: c.State.FeatureID, |
| } |
| } |
| } |
|
|
| func (c Charge) ResolveFeatureMeter(featureMeters feature.FeatureMeters) (feature.FeatureMeter, error) { |
| const requireMeter = true |
|
|
| featureRef := c.GetFeatureKeyOrID() |
| if featureRef.ID != "" { |
| return featureMeters.GetByID(featureRef.ID, requireMeter) |
| } |
|
|
| featureMeter, err := featureMeters.Get(featureRef.Key, requireMeter) |
| if err != nil { |
| return feature.FeatureMeter{}, fmt.Errorf("get feature meter: %w", err) |
| } |
|
|
| return featureMeter, nil |
| } |
|
|
| |
| |
| |
| func (c Charges) GetFeatureKeysOrIDs() []ref.IDOrKey { |
| return lo.Uniq(lo.Map(c, func(charge Charge, _ int) ref.IDOrKey { |
| return charge.GetFeatureKeyOrID() |
| })) |
| } |
|
|
| type Intent struct { |
| meta.Intent |
| IntentMutableFields `json:"intentMutableFields"` |
| SettlementMode productcatalog.SettlementMode `json:"settlementMode"` |
| FeatureKey string `json:"featureKey"` |
| } |
|
|
| |
| func (i Intent) AsOverridableIntent() OverridableIntent { |
| return OverridableIntent{ |
| intent: i.Intent, |
| baseLayer: i.IntentMutableFields, |
| settlementMode: i.SettlementMode, |
| featureKey: i.FeatureKey, |
| } |
| } |
|
|
| func (i Intent) Normalized() Intent { |
| i.IntentMutableFields = i.IntentMutableFields.Normalized() |
|
|
| return i |
| } |
|
|
| func (i Intent) Validate() error { |
| var errs []error |
|
|
| if err := i.Intent.Validate(); err != nil { |
| errs = append(errs, err) |
| } |
|
|
| if err := i.IntentMutableFields.Validate(); err != nil { |
| errs = append(errs, err) |
| } |
|
|
| if err := i.SettlementMode.Validate(); err != nil { |
| errs = append(errs, fmt.Errorf("settlement mode: %w", err)) |
| } |
|
|
| if i.FeatureKey == "" { |
| errs = append(errs, fmt.Errorf("feature key is required")) |
| } |
|
|
| return models.NewNillableGenericValidationError(errors.Join(errs...)) |
| } |
|
|
| |
| |
| |
| |
| type OverridableIntent struct { |
| intent meta.Intent |
|
|
| baseLayer IntentMutableFields |
| overrideLayer *IntentMutableFields |
|
|
| settlementMode productcatalog.SettlementMode |
| featureKey string |
| } |
|
|
| func NewOverridableIntent(baseIntent Intent, overrideLayer *IntentMutableFields) OverridableIntent { |
| return OverridableIntent{ |
| intent: baseIntent.Intent, |
| baseLayer: baseIntent.IntentMutableFields, |
| overrideLayer: overrideLayer, |
| settlementMode: baseIntent.SettlementMode, |
| featureKey: baseIntent.FeatureKey, |
| } |
| } |
|
|
| func (i OverridableIntent) Normalized() OverridableIntent { |
| i.baseLayer = i.baseLayer.Normalized() |
| if i.overrideLayer != nil { |
| overrideLayer := i.overrideLayer.Normalized() |
| i.overrideLayer = &overrideLayer |
| } |
|
|
| return i |
| } |
|
|
| func (i OverridableIntent) GetCustomerID() string { |
| return i.intent.CustomerID |
| } |
|
|
| func (i OverridableIntent) GetCurrency() currencyx.Code { |
| return i.intent.Currency |
| } |
|
|
| func (i OverridableIntent) GetSettlementMode() productcatalog.SettlementMode { |
| return i.settlementMode |
| } |
|
|
| func (i OverridableIntent) GetUniqueReferenceID() *string { |
| return i.intent.UniqueReferenceID |
| } |
|
|
| func (i OverridableIntent) GetSubscription() *meta.SubscriptionReference { |
| if i.intent.Subscription == nil { |
| return nil |
| } |
|
|
| subscription := *i.intent.Subscription |
|
|
| return &subscription |
| } |
|
|
| func (i OverridableIntent) Validate() error { |
| var errs []error |
|
|
| if err := i.intent.Validate(); err != nil { |
| errs = append(errs, fmt.Errorf("intent: %w", err)) |
| } |
|
|
| if err := i.baseLayer.Validate(); err != nil { |
| errs = append(errs, fmt.Errorf("base layer: %w", err)) |
| } |
|
|
| if i.overrideLayer != nil { |
| if err := i.overrideLayer.Validate(); err != nil { |
| errs = append(errs, fmt.Errorf("override layer: %w", err)) |
| } |
| } |
|
|
| if err := i.settlementMode.Validate(); err != nil { |
| errs = append(errs, fmt.Errorf("settlement mode: %w", err)) |
| } |
|
|
| return models.NewNillableGenericValidationError(errors.Join(errs...)) |
| } |
|
|
| |
| |
| |
| |
| |
| func (i OverridableIntent) GetEffectiveIntent() Intent { |
| intent := Intent{ |
| Intent: i.intent.Clone(), |
| IntentMutableFields: i.baseLayer.Clone(), |
| SettlementMode: i.settlementMode, |
| FeatureKey: i.featureKey, |
| } |
|
|
| if i.overrideLayer != nil { |
| intent.IntentMutableFields = i.overrideLayer.Clone() |
| } |
|
|
| return intent.Normalized() |
| } |
|
|
| |
| |
| func (i OverridableIntent) GetEffectiveServicePeriod() timeutil.ClosedPeriod { |
| if i.overrideLayer != nil { |
| return i.overrideLayer.ServicePeriod |
| } |
|
|
| return i.baseLayer.ServicePeriod |
| } |
|
|
| |
| |
| func (i OverridableIntent) GetEffectiveInvoiceAt() time.Time { |
| if i.overrideLayer != nil { |
| return i.overrideLayer.InvoiceAt |
| } |
|
|
| return i.baseLayer.InvoiceAt |
| } |
|
|
| |
| |
| func (i OverridableIntent) GetFeatureKey() string { |
| return i.featureKey |
| } |
|
|
| |
| |
| func (i OverridableIntent) GetEffectivePrice() productcatalog.Price { |
| if i.overrideLayer != nil { |
| return *i.overrideLayer.Price.Clone() |
| } |
|
|
| return *i.baseLayer.Price.Clone() |
| } |
|
|
| |
| |
| func (i OverridableIntent) GetEffectiveDiscounts() billing.Discounts { |
| if i.overrideLayer != nil { |
| return i.overrideLayer.Discounts.Clone() |
| } |
|
|
| return i.baseLayer.Discounts.Clone() |
| } |
|
|
| |
| |
| |
| |
| |
| |
| func (i OverridableIntent) GetEffectiveUnitConfig() *productcatalog.UnitConfig { |
| unitConfig := i.baseLayer.UnitConfig |
| if i.overrideLayer != nil { |
| unitConfig = i.overrideLayer.UnitConfig |
| } |
|
|
| if unitConfig == nil { |
| return nil |
| } |
|
|
| return lo.ToPtr(unitConfig.Clone()) |
| } |
|
|
| |
| |
| func (i OverridableIntent) GetTaxConfig() productcatalog.TaxCodeConfig { |
| return i.intent.TaxConfig |
| } |
|
|
| |
| |
| |
| func (i OverridableIntent) GetEffectiveMetaIntentMutableFields() meta.IntentMutableFields { |
| if i.overrideLayer != nil { |
| return i.overrideLayer.IntentMutableFields |
| } |
|
|
| return i.baseLayer.IntentMutableFields |
| } |
|
|
| func (i OverridableIntent) GetBaseManagedBy() billing.InvoiceLineManagedBy { |
| return i.intent.ManagedBy |
| } |
|
|
| func (i OverridableIntent) GetBaseIntent() Intent { |
| return Intent{ |
| Intent: i.intent.Clone(), |
| IntentMutableFields: i.baseLayer.Clone(), |
| SettlementMode: i.settlementMode, |
| FeatureKey: i.featureKey, |
| } |
| } |
|
|
| func (i OverridableIntent) GetIntentForTarget(target meta.ChangeTarget) (Intent, error) { |
| out := Intent{ |
| Intent: i.intent.Clone(), |
| SettlementMode: i.settlementMode, |
| FeatureKey: i.featureKey, |
| } |
|
|
| switch target { |
| case meta.ChangeTargetBase: |
| out.IntentMutableFields = i.baseLayer.Clone() |
| case meta.ChangeTargetOverride: |
| if i.overrideLayer == nil { |
| return Intent{}, fmt.Errorf("override layer not present for charge") |
| } |
|
|
| out.IntentMutableFields = i.overrideLayer.Clone() |
| default: |
| return Intent{}, fmt.Errorf("invalid change target: %s", target) |
| } |
|
|
| return out, nil |
| } |
|
|
| func (i OverridableIntent) GetOverrideLayerMutableFields() *IntentMutableFields { |
| if i.overrideLayer == nil { |
| return nil |
| } |
|
|
| return lo.ToPtr(i.overrideLayer.Clone()) |
| } |
|
|
| func (i OverridableIntent) HasOverrideLayer() bool { |
| return i.overrideLayer != nil |
| } |
|
|
| func (i OverridableIntent) GetDeletedAt() *time.Time { |
| if i.overrideLayer != nil { |
| return i.overrideLayer.IntentDeletedAt |
| } |
|
|
| return i.baseLayer.IntentDeletedAt |
| } |
|
|
| func (i *OverridableIntent) MutateEffective(editFn func(*IntentMutableFields) error) error { |
| target := meta.ChangeTargetBase |
| if i.overrideLayer != nil { |
| target = meta.ChangeTargetOverride |
| } |
|
|
| return i.Mutate(target, editFn) |
| } |
|
|
| |
| |
| |
| |
| |
| func (i *OverridableIntent) Mutate(target meta.ChangeTarget, editFn func(*IntentMutableFields) error) error { |
| var targetFields IntentMutableFields |
| switch target { |
| case meta.ChangeTargetBase: |
| targetFields = i.baseLayer.Clone() |
| case meta.ChangeTargetOverride: |
| if i.overrideLayer == nil { |
| return fmt.Errorf("override layer not present for charge") |
| } |
|
|
| targetFields = i.overrideLayer.Clone() |
| } |
|
|
| if err := editFn(&targetFields); err != nil { |
| return err |
| } |
|
|
| normalizedFields := targetFields.Normalized() |
|
|
| if err := normalizedFields.Validate(); err != nil { |
| return fmt.Errorf("validating intent: %w", err) |
| } |
|
|
| switch target { |
| case meta.ChangeTargetBase: |
| i.baseLayer = normalizedFields |
| case meta.ChangeTargetOverride: |
| if i.overrideLayer == nil { |
| return fmt.Errorf("override layer not present for charge") |
| } |
|
|
| i.overrideLayer = &normalizedFields |
| default: |
| return fmt.Errorf("invalid change target: %s", target) |
| } |
|
|
| return nil |
| } |
|
|
| type IntentMutableFields struct { |
| meta.IntentMutableFields |
|
|
| |
| |
| IntentDeletedAt *time.Time `json:"intentDeletedAt,omitempty"` |
|
|
| InvoiceAt time.Time `json:"invoiceAt"` |
|
|
| Price productcatalog.Price `json:"price"` |
|
|
| Discounts billing.Discounts `json:"discounts"` |
|
|
| |
| |
| |
| UnitConfig *productcatalog.UnitConfig `json:"unitConfig,omitempty"` |
| } |
|
|
| func (f IntentMutableFields) Normalized() IntentMutableFields { |
| f.IntentMutableFields = f.IntentMutableFields.Normalized() |
| f.InvoiceAt = meta.NormalizeTimestamp(f.InvoiceAt) |
|
|
| return f |
| } |
|
|
| func (f IntentMutableFields) Clone() IntentMutableFields { |
| out := f |
| out.IntentMutableFields = f.IntentMutableFields.Clone() |
| out.Price = *f.Price.Clone() |
| out.Discounts = f.Discounts.Clone() |
|
|
| if f.UnitConfig != nil { |
| out.UnitConfig = lo.ToPtr(f.UnitConfig.Clone()) |
| } |
|
|
| return out |
| } |
|
|
| func (f IntentMutableFields) Validate() error { |
| var errs []error |
|
|
| if err := f.IntentMutableFields.Validate(); err != nil { |
| errs = append(errs, fmt.Errorf("intent mutable fields: %w", err)) |
| } |
|
|
| if err := f.Discounts.Validate(); err != nil { |
| errs = append(errs, fmt.Errorf("discounts: %w", err)) |
| } |
|
|
| if f.InvoiceAt.IsZero() { |
| errs = append(errs, fmt.Errorf("invoice at is required")) |
| } |
|
|
| if err := f.Price.Validate(); err != nil { |
| errs = append(errs, fmt.Errorf("price: %w", err)) |
| } |
|
|
| if err := f.UnitConfig.Validate(); err != nil { |
| errs = append(errs, fmt.Errorf("unit config: %w", err)) |
| } |
|
|
| return models.NewNillableGenericValidationError(errors.Join(errs...)) |
| } |
|
|
| type State struct { |
| CurrentRealizationRunID *string `json:"currentRealizationRunId"` |
| AdvanceAfter *time.Time `json:"advanceAfter"` |
| FeatureID string `json:"featureId"` |
| RatingEngine RatingEngine `json:"ratingEngine"` |
| } |
|
|
| func (s State) Normalized() State { |
| s.AdvanceAfter = meta.NormalizeOptionalTimestamp(s.AdvanceAfter) |
|
|
| return s |
| } |
|
|
| func (s State) Validate() error { |
| var errs []error |
|
|
| if s.CurrentRealizationRunID != nil && *s.CurrentRealizationRunID == "" { |
| errs = append(errs, fmt.Errorf("current realization run ID must be non-empty")) |
| } |
|
|
| if s.FeatureID == "" { |
| errs = append(errs, fmt.Errorf("feature id must be set")) |
| } |
|
|
| if err := s.RatingEngine.Validate(); err != nil { |
| errs = append(errs, fmt.Errorf("rating engine: %w", err)) |
| } |
|
|
| return models.NewNillableGenericValidationError(errors.Join(errs...)) |
| } |
|
|
| type Expands struct { |
| RealtimeUsage *totals.Totals `json:"realtimeUsage,omitempty"` |
| } |
|
|
| func (e Expands) Validate() error { |
| var errs []error |
|
|
| if e.RealtimeUsage != nil { |
| if err := e.RealtimeUsage.Validate(); err != nil { |
| errs = append(errs, fmt.Errorf("realtime usage: %w", err)) |
| } |
| } |
|
|
| return models.NewNillableGenericValidationError(errors.Join(errs...)) |
| } |
|
|