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() } // GetIntentDeletedAt returns the effective intent deletion timestamp. // If an override is present, the override intent owns deletion; otherwise the base intent does. 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 { // State.FeatureID is the persisted resolved feature snapshot used by active // charges; created/deleted fallbacks resolve by key. 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 } // GetFeatureKeysOrIDs returns the unique state-aware feature references for the charges. // Each charge contributes the ref returned by GetFeatureKeyOrID, so created charges use keys, // deleted charges prefer IDs and fall back to keys, and all other states use IDs. 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"` } // AsOverridableIntent maps the intent's mutable fields as the base layer. 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...)) } // OverridableIntent stores the immutable intent plus the base and optional // override mutable layers. Direct layer access is error-prone because callers // must manually decide which layer is active; this API centralizes that choice // so reads and mutations use the correct override layer when present. 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...)) } // GetEffectiveIntent returns the customer-facing intent by combining the // immutable intent with the active mutable layer. // // WARNING: this clones and normalizes the intent and mutable fields. Prefer the // narrower effective getters when only a few fields are required. 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() } // GetEffectiveServicePeriod returns the service period from the active mutable // layer, preferring the override layer when it is present. func (i OverridableIntent) GetEffectiveServicePeriod() timeutil.ClosedPeriod { if i.overrideLayer != nil { return i.overrideLayer.ServicePeriod } return i.baseLayer.ServicePeriod } // GetEffectiveInvoiceAt returns the invoice-at timestamp from the active // mutable layer, preferring the override layer when it is present. func (i OverridableIntent) GetEffectiveInvoiceAt() time.Time { if i.overrideLayer != nil { return i.overrideLayer.InvoiceAt } return i.baseLayer.InvoiceAt } // GetFeatureKey returns the immutable usage-based feature key from // the base intent. Override layers cannot change usage attribution. func (i OverridableIntent) GetFeatureKey() string { return i.featureKey } // GetEffectivePrice returns a cloned price from the active mutable layer, // preferring the override layer when it is present. func (i OverridableIntent) GetEffectivePrice() productcatalog.Price { if i.overrideLayer != nil { return *i.overrideLayer.Price.Clone() } return *i.baseLayer.Price.Clone() } // GetEffectiveDiscounts returns cloned discounts from the active mutable layer, // preferring the override layer when it is present. func (i OverridableIntent) GetEffectiveDiscounts() billing.Discounts { if i.overrideLayer != nil { return i.overrideLayer.Discounts.Clone() } return i.baseLayer.Discounts.Clone() } // GetEffectiveUnitConfig returns the cloned unit_config from the active mutable // layer, preferring the override layer when it is present. Nil when the rate card // carries no unit_config. // // The override layer is a full snapshot of the effective fields (like Price/Discounts), // so a nil override value means the config was cleared, not inherited from the base. 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()) } // GetTaxConfig returns the immutable tax config from the base intent. // Override layers cannot change tax attribution. func (i OverridableIntent) GetTaxConfig() productcatalog.TaxCodeConfig { return i.intent.TaxConfig } // GetEffectiveMetaIntentMutableFields returns the shared meta mutable fields // from the active mutable layer, preferring the override layer when it is // present. 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) } // Mutate edits the requested intent mutable field layer. // // The callback always receives a non-nil pointer to a cloned mutable-field value. // The clone is written back only after it normalizes and validates, so validation // errors do not partially mutate the intent. 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 marks the usage-based base/original intent as deleted. // Adapters derive the effective charge DeletedAt from this value when no intent override is present. IntentDeletedAt *time.Time `json:"intentDeletedAt,omitempty"` InvoiceAt time.Time `json:"invoiceAt"` Price productcatalog.Price `json:"price"` Discounts billing.Discounts `json:"discounts"` // UnitConfig is the optional unit conversion snapshotted from the effective // rate card. Like Price it is a mutable rating input (set on create and // update) so re-rates read the config in effect for the charge. 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...)) }