diff --git a/openmeter/billing/charges/creditpurchase/adapter/charge.go b/openmeter/billing/charges/creditpurchase/adapter/charge.go index b7ffc62022..80a7cbc499 100644 --- a/openmeter/billing/charges/creditpurchase/adapter/charge.go +++ b/openmeter/billing/charges/creditpurchase/adapter/charge.go @@ -67,6 +67,8 @@ func (a *adapter) CreateCharge(ctx context.Context, in creditpurchase.CreateChar create := tx.db.ChargeCreditPurchase.Create(). SetNamespace(in.Namespace). SetCreditAmount(in.Intent.CreditAmount). + SetNillableEffectiveAt(meta.NormalizeOptionalTimestamp(in.Intent.EffectiveAt)). + SetNillablePriority(in.Intent.Priority). SetSettlement(in.Intent.Settlement) create, err := chargemeta.Create(create, chargemeta.CreateInput{ diff --git a/openmeter/billing/charges/creditpurchase/adapter/mapper.go b/openmeter/billing/charges/creditpurchase/adapter/mapper.go index da119ec389..c22f9b9d5d 100644 --- a/openmeter/billing/charges/creditpurchase/adapter/mapper.go +++ b/openmeter/billing/charges/creditpurchase/adapter/mapper.go @@ -12,6 +12,7 @@ import ( "github.com/openmeterio/openmeter/openmeter/billing/charges/models/ledgertransaction" "github.com/openmeterio/openmeter/openmeter/billing/charges/models/payment" entdb "github.com/openmeterio/openmeter/openmeter/ent/db" + "github.com/openmeterio/openmeter/pkg/convert" ) func MapCreditPurchaseChargeFromDB(dbEntity *entdb.ChargeCreditPurchase, expands meta.Expands) (creditpurchase.Charge, error) { @@ -55,6 +56,8 @@ func MapCreditPurchaseChargeFromDB(dbEntity *entdb.ChargeCreditPurchase, expands Intent: creditpurchase.Intent{ Intent: mappedMeta.Intent, CreditAmount: dbEntity.CreditAmount, + EffectiveAt: convert.SafeToUTC(dbEntity.EffectiveAt), + Priority: dbEntity.Priority, Settlement: dbEntity.Settlement, }, State: creditpurchase.State{ diff --git a/openmeter/billing/charges/creditpurchase/chargecreditpurchase.go b/openmeter/billing/charges/creditpurchase/chargecreditpurchase.go index 4d972c1bea..5e03fb22ff 100644 --- a/openmeter/billing/charges/creditpurchase/chargecreditpurchase.go +++ b/openmeter/billing/charges/creditpurchase/chargecreditpurchase.go @@ -3,12 +3,15 @@ package creditpurchase import ( "errors" "fmt" + "time" "github.com/alpacahq/alpacadecimal" + "github.com/samber/lo" "github.com/openmeterio/openmeter/openmeter/billing/charges/meta" "github.com/openmeterio/openmeter/openmeter/billing/charges/models/ledgertransaction" "github.com/openmeterio/openmeter/openmeter/billing/charges/models/payment" + "github.com/openmeterio/openmeter/pkg/clock" "github.com/openmeterio/openmeter/pkg/models" ) @@ -64,6 +67,11 @@ type Intent struct { meta.Intent CreditAmount alpacadecimal.Decimal `json:"amount"` + // EffectiveAt is the time at which the credit purchase is effective. + // Warning/TODO[later]: Currently this is not supported in credit purchase handler and the charge will be created + // with booked_at set to CreatedAt. + EffectiveAt *time.Time `json:"effectiveAt"` + Priority *int `json:"priority"` // Settlement intent Settlement Settlement `json:"settlement"` @@ -71,6 +79,7 @@ type Intent struct { func (i Intent) Normalized() Intent { i.Intent = i.Intent.Normalized() + i.EffectiveAt = meta.NormalizeOptionalTimestamp(i.EffectiveAt) calc, err := i.Currency.Calculator() if err == nil { @@ -80,6 +89,10 @@ func (i Intent) Normalized() Intent { return i } +func (i Intent) CalculateEffectiveAt() time.Time { + return lo.FromPtrOr(i.EffectiveAt, clock.Now().UTC()) +} + func (i Intent) Validate() error { var errs []error @@ -95,7 +108,11 @@ func (i Intent) Validate() error { errs = append(errs, fmt.Errorf("settlement: %w", err)) } - return errors.Join(errs...) + if i.EffectiveAt != nil { + return errors.New("effective at is not yet supported") + } + + return models.NewNillableGenericValidationError(errors.Join(errs...)) } type State struct { diff --git a/openmeter/ent/db/chargecreditpurchase.go b/openmeter/ent/db/chargecreditpurchase.go index ed8353efca..5809f17360 100644 --- a/openmeter/ent/db/chargecreditpurchase.go +++ b/openmeter/ent/db/chargecreditpurchase.go @@ -79,6 +79,10 @@ type ChargeCreditPurchase struct { Description *string `json:"description,omitempty"` // CreditAmount holds the value of the "credit_amount" field. CreditAmount alpacadecimal.Decimal `json:"credit_amount,omitempty"` + // EffectiveAt holds the value of the "effective_at" field. + EffectiveAt *time.Time `json:"effective_at,omitempty"` + // Priority holds the value of the "priority" field. + Priority *int `json:"priority,omitempty"` // Settlement holds the value of the "settlement" field. Settlement creditpurchase.Settlement `json:"settlement,omitempty"` // CreditGrantTransactionGroupID holds the value of the "credit_grant_transaction_group_id" field. @@ -198,9 +202,11 @@ func (*ChargeCreditPurchase) scanValues(columns []string) ([]any, error) { values[i] = new([]byte) case chargecreditpurchase.FieldCreditAmount: values[i] = new(alpacadecimal.Decimal) + case chargecreditpurchase.FieldPriority: + values[i] = new(sql.NullInt64) case chargecreditpurchase.FieldID, chargecreditpurchase.FieldCustomerID, chargecreditpurchase.FieldStatus, chargecreditpurchase.FieldUniqueReferenceID, chargecreditpurchase.FieldCurrency, chargecreditpurchase.FieldManagedBy, chargecreditpurchase.FieldSubscriptionID, chargecreditpurchase.FieldSubscriptionPhaseID, chargecreditpurchase.FieldSubscriptionItemID, chargecreditpurchase.FieldNamespace, chargecreditpurchase.FieldName, chargecreditpurchase.FieldDescription, chargecreditpurchase.FieldCreditGrantTransactionGroupID: values[i] = new(sql.NullString) - case chargecreditpurchase.FieldServicePeriodFrom, chargecreditpurchase.FieldServicePeriodTo, chargecreditpurchase.FieldBillingPeriodFrom, chargecreditpurchase.FieldBillingPeriodTo, chargecreditpurchase.FieldFullServicePeriodFrom, chargecreditpurchase.FieldFullServicePeriodTo, chargecreditpurchase.FieldAdvanceAfter, chargecreditpurchase.FieldCreatedAt, chargecreditpurchase.FieldUpdatedAt, chargecreditpurchase.FieldDeletedAt, chargecreditpurchase.FieldCreditGrantedAt: + case chargecreditpurchase.FieldServicePeriodFrom, chargecreditpurchase.FieldServicePeriodTo, chargecreditpurchase.FieldBillingPeriodFrom, chargecreditpurchase.FieldBillingPeriodTo, chargecreditpurchase.FieldFullServicePeriodFrom, chargecreditpurchase.FieldFullServicePeriodTo, chargecreditpurchase.FieldAdvanceAfter, chargecreditpurchase.FieldCreatedAt, chargecreditpurchase.FieldUpdatedAt, chargecreditpurchase.FieldDeletedAt, chargecreditpurchase.FieldEffectiveAt, chargecreditpurchase.FieldCreditGrantedAt: values[i] = new(sql.NullTime) case chargecreditpurchase.FieldSettlement: values[i] = chargecreditpurchase.ValueScanner.Settlement.ScanValue() @@ -380,6 +386,20 @@ func (_m *ChargeCreditPurchase) assignValues(columns []string, values []any) err } else if value != nil { _m.CreditAmount = *value } + case chargecreditpurchase.FieldEffectiveAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field effective_at", values[i]) + } else if value.Valid { + _m.EffectiveAt = new(time.Time) + *_m.EffectiveAt = value.Time + } + case chargecreditpurchase.FieldPriority: + if value, ok := values[i].(*sql.NullInt64); !ok { + return fmt.Errorf("unexpected type %T for field priority", values[i]) + } else if value.Valid { + _m.Priority = new(int) + *_m.Priority = int(value.Int64) + } case chargecreditpurchase.FieldSettlement: if value, err := chargecreditpurchase.ValueScanner.Settlement.FromValue(values[i]); err != nil { return err @@ -557,6 +577,16 @@ func (_m *ChargeCreditPurchase) String() string { builder.WriteString("credit_amount=") builder.WriteString(fmt.Sprintf("%v", _m.CreditAmount)) builder.WriteString(", ") + if v := _m.EffectiveAt; v != nil { + builder.WriteString("effective_at=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteString(", ") + if v := _m.Priority; v != nil { + builder.WriteString("priority=") + builder.WriteString(fmt.Sprintf("%v", *v)) + } + builder.WriteString(", ") builder.WriteString("settlement=") builder.WriteString(fmt.Sprintf("%v", _m.Settlement)) builder.WriteString(", ") diff --git a/openmeter/ent/db/chargecreditpurchase/chargecreditpurchase.go b/openmeter/ent/db/chargecreditpurchase/chargecreditpurchase.go index 45045b6fe3..271afa2c43 100644 --- a/openmeter/ent/db/chargecreditpurchase/chargecreditpurchase.go +++ b/openmeter/ent/db/chargecreditpurchase/chargecreditpurchase.go @@ -67,6 +67,10 @@ const ( FieldDescription = "description" // FieldCreditAmount holds the string denoting the credit_amount field in the database. FieldCreditAmount = "credit_amount" + // FieldEffectiveAt holds the string denoting the effective_at field in the database. + FieldEffectiveAt = "effective_at" + // FieldPriority holds the string denoting the priority field in the database. + FieldPriority = "priority" // FieldSettlement holds the string denoting the settlement field in the database. FieldSettlement = "settlement" // FieldCreditGrantTransactionGroupID holds the string denoting the credit_grant_transaction_group_id field in the database. @@ -167,6 +171,8 @@ var Columns = []string{ FieldName, FieldDescription, FieldCreditAmount, + FieldEffectiveAt, + FieldPriority, FieldSettlement, FieldCreditGrantTransactionGroupID, FieldCreditGrantedAt, @@ -343,6 +349,16 @@ func ByCreditAmount(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldCreditAmount, opts...).ToFunc() } +// ByEffectiveAt orders the results by the effective_at field. +func ByEffectiveAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldEffectiveAt, opts...).ToFunc() +} + +// ByPriority orders the results by the priority field. +func ByPriority(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldPriority, opts...).ToFunc() +} + // BySettlement orders the results by the settlement field. func BySettlement(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldSettlement, opts...).ToFunc() diff --git a/openmeter/ent/db/chargecreditpurchase/where.go b/openmeter/ent/db/chargecreditpurchase/where.go index 296b9e77fb..1388008314 100644 --- a/openmeter/ent/db/chargecreditpurchase/where.go +++ b/openmeter/ent/db/chargecreditpurchase/where.go @@ -170,6 +170,16 @@ func CreditAmount(v alpacadecimal.Decimal) predicate.ChargeCreditPurchase { return predicate.ChargeCreditPurchase(sql.FieldEQ(FieldCreditAmount, v)) } +// EffectiveAt applies equality check predicate on the "effective_at" field. It's identical to EffectiveAtEQ. +func EffectiveAt(v time.Time) predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldEQ(FieldEffectiveAt, v)) +} + +// Priority applies equality check predicate on the "priority" field. It's identical to PriorityEQ. +func Priority(v int) predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldEQ(FieldPriority, v)) +} + // CreditGrantTransactionGroupID applies equality check predicate on the "credit_grant_transaction_group_id" field. It's identical to CreditGrantTransactionGroupIDEQ. func CreditGrantTransactionGroupID(v string) predicate.ChargeCreditPurchase { return predicate.ChargeCreditPurchase(sql.FieldEQ(FieldCreditGrantTransactionGroupID, v)) @@ -1374,6 +1384,106 @@ func CreditAmountLTE(v alpacadecimal.Decimal) predicate.ChargeCreditPurchase { return predicate.ChargeCreditPurchase(sql.FieldLTE(FieldCreditAmount, v)) } +// EffectiveAtEQ applies the EQ predicate on the "effective_at" field. +func EffectiveAtEQ(v time.Time) predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldEQ(FieldEffectiveAt, v)) +} + +// EffectiveAtNEQ applies the NEQ predicate on the "effective_at" field. +func EffectiveAtNEQ(v time.Time) predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldNEQ(FieldEffectiveAt, v)) +} + +// EffectiveAtIn applies the In predicate on the "effective_at" field. +func EffectiveAtIn(vs ...time.Time) predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldIn(FieldEffectiveAt, vs...)) +} + +// EffectiveAtNotIn applies the NotIn predicate on the "effective_at" field. +func EffectiveAtNotIn(vs ...time.Time) predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldNotIn(FieldEffectiveAt, vs...)) +} + +// EffectiveAtGT applies the GT predicate on the "effective_at" field. +func EffectiveAtGT(v time.Time) predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldGT(FieldEffectiveAt, v)) +} + +// EffectiveAtGTE applies the GTE predicate on the "effective_at" field. +func EffectiveAtGTE(v time.Time) predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldGTE(FieldEffectiveAt, v)) +} + +// EffectiveAtLT applies the LT predicate on the "effective_at" field. +func EffectiveAtLT(v time.Time) predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldLT(FieldEffectiveAt, v)) +} + +// EffectiveAtLTE applies the LTE predicate on the "effective_at" field. +func EffectiveAtLTE(v time.Time) predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldLTE(FieldEffectiveAt, v)) +} + +// EffectiveAtIsNil applies the IsNil predicate on the "effective_at" field. +func EffectiveAtIsNil() predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldIsNull(FieldEffectiveAt)) +} + +// EffectiveAtNotNil applies the NotNil predicate on the "effective_at" field. +func EffectiveAtNotNil() predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldNotNull(FieldEffectiveAt)) +} + +// PriorityEQ applies the EQ predicate on the "priority" field. +func PriorityEQ(v int) predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldEQ(FieldPriority, v)) +} + +// PriorityNEQ applies the NEQ predicate on the "priority" field. +func PriorityNEQ(v int) predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldNEQ(FieldPriority, v)) +} + +// PriorityIn applies the In predicate on the "priority" field. +func PriorityIn(vs ...int) predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldIn(FieldPriority, vs...)) +} + +// PriorityNotIn applies the NotIn predicate on the "priority" field. +func PriorityNotIn(vs ...int) predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldNotIn(FieldPriority, vs...)) +} + +// PriorityGT applies the GT predicate on the "priority" field. +func PriorityGT(v int) predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldGT(FieldPriority, v)) +} + +// PriorityGTE applies the GTE predicate on the "priority" field. +func PriorityGTE(v int) predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldGTE(FieldPriority, v)) +} + +// PriorityLT applies the LT predicate on the "priority" field. +func PriorityLT(v int) predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldLT(FieldPriority, v)) +} + +// PriorityLTE applies the LTE predicate on the "priority" field. +func PriorityLTE(v int) predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldLTE(FieldPriority, v)) +} + +// PriorityIsNil applies the IsNil predicate on the "priority" field. +func PriorityIsNil() predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldIsNull(FieldPriority)) +} + +// PriorityNotNil applies the NotNil predicate on the "priority" field. +func PriorityNotNil() predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldNotNull(FieldPriority)) +} + // CreditGrantTransactionGroupIDEQ applies the EQ predicate on the "credit_grant_transaction_group_id" field. func CreditGrantTransactionGroupIDEQ(v string) predicate.ChargeCreditPurchase { return predicate.ChargeCreditPurchase(sql.FieldEQ(FieldCreditGrantTransactionGroupID, v)) diff --git a/openmeter/ent/db/chargecreditpurchase_create.go b/openmeter/ent/db/chargecreditpurchase_create.go index 42027e26cf..aa53faba99 100644 --- a/openmeter/ent/db/chargecreditpurchase_create.go +++ b/openmeter/ent/db/chargecreditpurchase_create.go @@ -252,6 +252,34 @@ func (_c *ChargeCreditPurchaseCreate) SetCreditAmount(v alpacadecimal.Decimal) * return _c } +// SetEffectiveAt sets the "effective_at" field. +func (_c *ChargeCreditPurchaseCreate) SetEffectiveAt(v time.Time) *ChargeCreditPurchaseCreate { + _c.mutation.SetEffectiveAt(v) + return _c +} + +// SetNillableEffectiveAt sets the "effective_at" field if the given value is not nil. +func (_c *ChargeCreditPurchaseCreate) SetNillableEffectiveAt(v *time.Time) *ChargeCreditPurchaseCreate { + if v != nil { + _c.SetEffectiveAt(*v) + } + return _c +} + +// SetPriority sets the "priority" field. +func (_c *ChargeCreditPurchaseCreate) SetPriority(v int) *ChargeCreditPurchaseCreate { + _c.mutation.SetPriority(v) + return _c +} + +// SetNillablePriority sets the "priority" field if the given value is not nil. +func (_c *ChargeCreditPurchaseCreate) SetNillablePriority(v *int) *ChargeCreditPurchaseCreate { + if v != nil { + _c.SetPriority(*v) + } + return _c +} + // SetSettlement sets the "settlement" field. func (_c *ChargeCreditPurchaseCreate) SetSettlement(v creditpurchase.Settlement) *ChargeCreditPurchaseCreate { _c.mutation.SetSettlement(v) @@ -633,6 +661,14 @@ func (_c *ChargeCreditPurchaseCreate) createSpec() (*ChargeCreditPurchase, *sqlg _spec.SetField(chargecreditpurchase.FieldCreditAmount, field.TypeOther, value) _node.CreditAmount = value } + if value, ok := _c.mutation.EffectiveAt(); ok { + _spec.SetField(chargecreditpurchase.FieldEffectiveAt, field.TypeTime, value) + _node.EffectiveAt = &value + } + if value, ok := _c.mutation.Priority(); ok { + _spec.SetField(chargecreditpurchase.FieldPriority, field.TypeInt, value) + _node.Priority = &value + } if value, ok := _c.mutation.Settlement(); ok { vv, err := chargecreditpurchase.ValueScanner.Settlement.Value(value) if err != nil { @@ -1128,6 +1164,12 @@ func (u *ChargeCreditPurchaseUpsertOne) UpdateNewValues() *ChargeCreditPurchaseU if _, exists := u.create.mutation.CreatedAt(); exists { s.SetIgnore(chargecreditpurchase.FieldCreatedAt) } + if _, exists := u.create.mutation.EffectiveAt(); exists { + s.SetIgnore(chargecreditpurchase.FieldEffectiveAt) + } + if _, exists := u.create.mutation.Priority(); exists { + s.SetIgnore(chargecreditpurchase.FieldPriority) + } })) return u } @@ -1684,6 +1726,12 @@ func (u *ChargeCreditPurchaseUpsertBulk) UpdateNewValues() *ChargeCreditPurchase if _, exists := b.mutation.CreatedAt(); exists { s.SetIgnore(chargecreditpurchase.FieldCreatedAt) } + if _, exists := b.mutation.EffectiveAt(); exists { + s.SetIgnore(chargecreditpurchase.FieldEffectiveAt) + } + if _, exists := b.mutation.Priority(); exists { + s.SetIgnore(chargecreditpurchase.FieldPriority) + } } })) return u diff --git a/openmeter/ent/db/chargecreditpurchase_update.go b/openmeter/ent/db/chargecreditpurchase_update.go index e4ff30366d..6dd3d8c9f1 100644 --- a/openmeter/ent/db/chargecreditpurchase_update.go +++ b/openmeter/ent/db/chargecreditpurchase_update.go @@ -516,6 +516,12 @@ func (_u *ChargeCreditPurchaseUpdate) sqlSave(ctx context.Context) (_node int, e if value, ok := _u.mutation.CreditAmount(); ok { _spec.SetField(chargecreditpurchase.FieldCreditAmount, field.TypeOther, value) } + if _u.mutation.EffectiveAtCleared() { + _spec.ClearField(chargecreditpurchase.FieldEffectiveAt, field.TypeTime) + } + if _u.mutation.PriorityCleared() { + _spec.ClearField(chargecreditpurchase.FieldPriority, field.TypeInt) + } if value, ok := _u.mutation.Settlement(); ok { vv, err := chargecreditpurchase.ValueScanner.Settlement.Value(value) if err != nil { @@ -1124,6 +1130,12 @@ func (_u *ChargeCreditPurchaseUpdateOne) sqlSave(ctx context.Context) (_node *Ch if value, ok := _u.mutation.CreditAmount(); ok { _spec.SetField(chargecreditpurchase.FieldCreditAmount, field.TypeOther, value) } + if _u.mutation.EffectiveAtCleared() { + _spec.ClearField(chargecreditpurchase.FieldEffectiveAt, field.TypeTime) + } + if _u.mutation.PriorityCleared() { + _spec.ClearField(chargecreditpurchase.FieldPriority, field.TypeInt) + } if value, ok := _u.mutation.Settlement(); ok { vv, err := chargecreditpurchase.ValueScanner.Settlement.Value(value) if err != nil { diff --git a/openmeter/ent/db/migrate/schema.go b/openmeter/ent/db/migrate/schema.go index dfaa6660d1..3dbfba8342 100644 --- a/openmeter/ent/db/migrate/schema.go +++ b/openmeter/ent/db/migrate/schema.go @@ -1656,6 +1656,8 @@ var ( {Name: "name", Type: field.TypeString}, {Name: "description", Type: field.TypeString, Nullable: true}, {Name: "credit_amount", Type: field.TypeOther, SchemaType: map[string]string{"postgres": "numeric"}}, + {Name: "effective_at", Type: field.TypeTime, Nullable: true}, + {Name: "priority", Type: field.TypeInt, Nullable: true}, {Name: "settlement", Type: field.TypeString, SchemaType: map[string]string{"postgres": "jsonb"}}, {Name: "credit_grant_transaction_group_id", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "char(26)"}}, {Name: "credit_granted_at", Type: field.TypeTime, Nullable: true}, @@ -1672,25 +1674,25 @@ var ( ForeignKeys: []*schema.ForeignKey{ { Symbol: "charge_credit_purchases_customers_charges_credit_purchase", - Columns: []*schema.Column{ChargeCreditPurchasesColumns[24]}, + Columns: []*schema.Column{ChargeCreditPurchasesColumns[26]}, RefColumns: []*schema.Column{CustomersColumns[0]}, OnDelete: schema.NoAction, }, { Symbol: "charge_credit_purchases_subscriptions_charges_credit_purchase", - Columns: []*schema.Column{ChargeCreditPurchasesColumns[25]}, + Columns: []*schema.Column{ChargeCreditPurchasesColumns[27]}, RefColumns: []*schema.Column{SubscriptionsColumns[0]}, OnDelete: schema.SetNull, }, { Symbol: "charge_credit_purchases_subscription_items_charges_credit_purchase", - Columns: []*schema.Column{ChargeCreditPurchasesColumns[26]}, + Columns: []*schema.Column{ChargeCreditPurchasesColumns[28]}, RefColumns: []*schema.Column{SubscriptionItemsColumns[0]}, OnDelete: schema.SetNull, }, { Symbol: "charge_credit_purchases_subscription_phases_charges_credit_purchase", - Columns: []*schema.Column{ChargeCreditPurchasesColumns[27]}, + Columns: []*schema.Column{ChargeCreditPurchasesColumns[29]}, RefColumns: []*schema.Column{SubscriptionPhasesColumns[0]}, OnDelete: schema.SetNull, }, @@ -1699,7 +1701,7 @@ var ( { Name: "chargecreditpurchase_namespace_customer_id_unique_reference_id", Unique: true, - Columns: []*schema.Column{ChargeCreditPurchasesColumns[13], ChargeCreditPurchasesColumns[24], ChargeCreditPurchasesColumns[8]}, + Columns: []*schema.Column{ChargeCreditPurchasesColumns[13], ChargeCreditPurchasesColumns[26], ChargeCreditPurchasesColumns[8]}, Annotation: &entsql.IndexAnnotation{ Where: "unique_reference_id IS NOT NULL AND deleted_at IS NULL", }, diff --git a/openmeter/ent/db/mutation.go b/openmeter/ent/db/mutation.go index bcd42a06ec..7b12d63f83 100644 --- a/openmeter/ent/db/mutation.go +++ b/openmeter/ent/db/mutation.go @@ -36506,6 +36506,9 @@ type ChargeCreditPurchaseMutation struct { name *string description *string credit_amount *alpacadecimal.Decimal + effective_at *time.Time + priority *int + addpriority *int settlement *creditpurchase.Settlement credit_grant_transaction_group_id *string credit_granted_at *time.Time @@ -37614,6 +37617,125 @@ func (m *ChargeCreditPurchaseMutation) ResetCreditAmount() { m.credit_amount = nil } +// SetEffectiveAt sets the "effective_at" field. +func (m *ChargeCreditPurchaseMutation) SetEffectiveAt(t time.Time) { + m.effective_at = &t +} + +// EffectiveAt returns the value of the "effective_at" field in the mutation. +func (m *ChargeCreditPurchaseMutation) EffectiveAt() (r time.Time, exists bool) { + v := m.effective_at + if v == nil { + return + } + return *v, true +} + +// OldEffectiveAt returns the old "effective_at" field's value of the ChargeCreditPurchase entity. +// If the ChargeCreditPurchase object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ChargeCreditPurchaseMutation) OldEffectiveAt(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldEffectiveAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldEffectiveAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldEffectiveAt: %w", err) + } + return oldValue.EffectiveAt, nil +} + +// ClearEffectiveAt clears the value of the "effective_at" field. +func (m *ChargeCreditPurchaseMutation) ClearEffectiveAt() { + m.effective_at = nil + m.clearedFields[chargecreditpurchase.FieldEffectiveAt] = struct{}{} +} + +// EffectiveAtCleared returns if the "effective_at" field was cleared in this mutation. +func (m *ChargeCreditPurchaseMutation) EffectiveAtCleared() bool { + _, ok := m.clearedFields[chargecreditpurchase.FieldEffectiveAt] + return ok +} + +// ResetEffectiveAt resets all changes to the "effective_at" field. +func (m *ChargeCreditPurchaseMutation) ResetEffectiveAt() { + m.effective_at = nil + delete(m.clearedFields, chargecreditpurchase.FieldEffectiveAt) +} + +// SetPriority sets the "priority" field. +func (m *ChargeCreditPurchaseMutation) SetPriority(i int) { + m.priority = &i + m.addpriority = nil +} + +// Priority returns the value of the "priority" field in the mutation. +func (m *ChargeCreditPurchaseMutation) Priority() (r int, exists bool) { + v := m.priority + if v == nil { + return + } + return *v, true +} + +// OldPriority returns the old "priority" field's value of the ChargeCreditPurchase entity. +// If the ChargeCreditPurchase object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ChargeCreditPurchaseMutation) OldPriority(ctx context.Context) (v *int, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldPriority is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldPriority requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldPriority: %w", err) + } + return oldValue.Priority, nil +} + +// AddPriority adds i to the "priority" field. +func (m *ChargeCreditPurchaseMutation) AddPriority(i int) { + if m.addpriority != nil { + *m.addpriority += i + } else { + m.addpriority = &i + } +} + +// AddedPriority returns the value that was added to the "priority" field in this mutation. +func (m *ChargeCreditPurchaseMutation) AddedPriority() (r int, exists bool) { + v := m.addpriority + if v == nil { + return + } + return *v, true +} + +// ClearPriority clears the value of the "priority" field. +func (m *ChargeCreditPurchaseMutation) ClearPriority() { + m.priority = nil + m.addpriority = nil + m.clearedFields[chargecreditpurchase.FieldPriority] = struct{}{} +} + +// PriorityCleared returns if the "priority" field was cleared in this mutation. +func (m *ChargeCreditPurchaseMutation) PriorityCleared() bool { + _, ok := m.clearedFields[chargecreditpurchase.FieldPriority] + return ok +} + +// ResetPriority resets all changes to the "priority" field. +func (m *ChargeCreditPurchaseMutation) ResetPriority() { + m.priority = nil + m.addpriority = nil + delete(m.clearedFields, chargecreditpurchase.FieldPriority) +} + // SetSettlement sets the "settlement" field. func (m *ChargeCreditPurchaseMutation) SetSettlement(c creditpurchase.Settlement) { m.settlement = &c @@ -38007,7 +38129,7 @@ func (m *ChargeCreditPurchaseMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *ChargeCreditPurchaseMutation) Fields() []string { - fields := make([]string, 0, 27) + fields := make([]string, 0, 29) if m.customer != nil { fields = append(fields, chargecreditpurchase.FieldCustomerID) } @@ -38080,6 +38202,12 @@ func (m *ChargeCreditPurchaseMutation) Fields() []string { if m.credit_amount != nil { fields = append(fields, chargecreditpurchase.FieldCreditAmount) } + if m.effective_at != nil { + fields = append(fields, chargecreditpurchase.FieldEffectiveAt) + } + if m.priority != nil { + fields = append(fields, chargecreditpurchase.FieldPriority) + } if m.settlement != nil { fields = append(fields, chargecreditpurchase.FieldSettlement) } @@ -38145,6 +38273,10 @@ func (m *ChargeCreditPurchaseMutation) Field(name string) (ent.Value, bool) { return m.Description() case chargecreditpurchase.FieldCreditAmount: return m.CreditAmount() + case chargecreditpurchase.FieldEffectiveAt: + return m.EffectiveAt() + case chargecreditpurchase.FieldPriority: + return m.Priority() case chargecreditpurchase.FieldSettlement: return m.Settlement() case chargecreditpurchase.FieldCreditGrantTransactionGroupID: @@ -38208,6 +38340,10 @@ func (m *ChargeCreditPurchaseMutation) OldField(ctx context.Context, name string return m.OldDescription(ctx) case chargecreditpurchase.FieldCreditAmount: return m.OldCreditAmount(ctx) + case chargecreditpurchase.FieldEffectiveAt: + return m.OldEffectiveAt(ctx) + case chargecreditpurchase.FieldPriority: + return m.OldPriority(ctx) case chargecreditpurchase.FieldSettlement: return m.OldSettlement(ctx) case chargecreditpurchase.FieldCreditGrantTransactionGroupID: @@ -38391,6 +38527,20 @@ func (m *ChargeCreditPurchaseMutation) SetField(name string, value ent.Value) er } m.SetCreditAmount(v) return nil + case chargecreditpurchase.FieldEffectiveAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetEffectiveAt(v) + return nil + case chargecreditpurchase.FieldPriority: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetPriority(v) + return nil case chargecreditpurchase.FieldSettlement: v, ok := value.(creditpurchase.Settlement) if !ok { @@ -38419,13 +38569,21 @@ func (m *ChargeCreditPurchaseMutation) SetField(name string, value ent.Value) er // AddedFields returns all numeric fields that were incremented/decremented during // this mutation. func (m *ChargeCreditPurchaseMutation) AddedFields() []string { - return nil + var fields []string + if m.addpriority != nil { + fields = append(fields, chargecreditpurchase.FieldPriority) + } + return fields } // AddedField returns the numeric value that was incremented/decremented on a field // with the given name. The second boolean return value indicates that this field // was not set, or was not defined in the schema. func (m *ChargeCreditPurchaseMutation) AddedField(name string) (ent.Value, bool) { + switch name { + case chargecreditpurchase.FieldPriority: + return m.AddedPriority() + } return nil, false } @@ -38434,6 +38592,13 @@ func (m *ChargeCreditPurchaseMutation) AddedField(name string) (ent.Value, bool) // type. func (m *ChargeCreditPurchaseMutation) AddField(name string, value ent.Value) error { switch name { + case chargecreditpurchase.FieldPriority: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddPriority(v) + return nil } return fmt.Errorf("unknown ChargeCreditPurchase numeric field %s", name) } @@ -38469,6 +38634,12 @@ func (m *ChargeCreditPurchaseMutation) ClearedFields() []string { if m.FieldCleared(chargecreditpurchase.FieldDescription) { fields = append(fields, chargecreditpurchase.FieldDescription) } + if m.FieldCleared(chargecreditpurchase.FieldEffectiveAt) { + fields = append(fields, chargecreditpurchase.FieldEffectiveAt) + } + if m.FieldCleared(chargecreditpurchase.FieldPriority) { + fields = append(fields, chargecreditpurchase.FieldPriority) + } if m.FieldCleared(chargecreditpurchase.FieldCreditGrantTransactionGroupID) { fields = append(fields, chargecreditpurchase.FieldCreditGrantTransactionGroupID) } @@ -38516,6 +38687,12 @@ func (m *ChargeCreditPurchaseMutation) ClearField(name string) error { case chargecreditpurchase.FieldDescription: m.ClearDescription() return nil + case chargecreditpurchase.FieldEffectiveAt: + m.ClearEffectiveAt() + return nil + case chargecreditpurchase.FieldPriority: + m.ClearPriority() + return nil case chargecreditpurchase.FieldCreditGrantTransactionGroupID: m.ClearCreditGrantTransactionGroupID() return nil @@ -38602,6 +38779,12 @@ func (m *ChargeCreditPurchaseMutation) ResetField(name string) error { case chargecreditpurchase.FieldCreditAmount: m.ResetCreditAmount() return nil + case chargecreditpurchase.FieldEffectiveAt: + m.ResetEffectiveAt() + return nil + case chargecreditpurchase.FieldPriority: + m.ResetPriority() + return nil case chargecreditpurchase.FieldSettlement: m.ResetSettlement() return nil diff --git a/openmeter/ent/db/runtime.go b/openmeter/ent/db/runtime.go index 95f10f47bb..63204c5d22 100644 --- a/openmeter/ent/db/runtime.go +++ b/openmeter/ent/db/runtime.go @@ -961,10 +961,10 @@ func init() { // chargecreditpurchase.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field. chargecreditpurchase.UpdateDefaultUpdatedAt = chargecreditpurchaseDescUpdatedAt.UpdateDefault.(func() time.Time) // chargecreditpurchaseDescSettlement is the schema descriptor for settlement field. - chargecreditpurchaseDescSettlement := chargecreditpurchaseFields[1].Descriptor() + chargecreditpurchaseDescSettlement := chargecreditpurchaseFields[3].Descriptor() chargecreditpurchase.ValueScanner.Settlement = chargecreditpurchaseDescSettlement.ValueScanner.(field.TypeValueScanner[creditpurchase.Settlement]) // chargecreditpurchaseDescCreditGrantTransactionGroupID is the schema descriptor for credit_grant_transaction_group_id field. - chargecreditpurchaseDescCreditGrantTransactionGroupID := chargecreditpurchaseFields[2].Descriptor() + chargecreditpurchaseDescCreditGrantTransactionGroupID := chargecreditpurchaseFields[4].Descriptor() // chargecreditpurchase.CreditGrantTransactionGroupIDValidator is a validator for the "credit_grant_transaction_group_id" field. It is called by the builders before save. chargecreditpurchase.CreditGrantTransactionGroupIDValidator = chargecreditpurchaseDescCreditGrantTransactionGroupID.Validators[0].(func(string) error) // chargecreditpurchaseDescID is the schema descriptor for id field. diff --git a/openmeter/ent/schema/chargescreditpurchase.go b/openmeter/ent/schema/chargescreditpurchase.go index a0c5193c6b..b6773cbc67 100644 --- a/openmeter/ent/schema/chargescreditpurchase.go +++ b/openmeter/ent/schema/chargescreditpurchase.go @@ -34,6 +34,14 @@ func (ChargeCreditPurchase) Fields() []ent.Field { SchemaType(map[string]string{ dialect.Postgres: "numeric", }), + field.Time("effective_at"). + Optional(). + Nillable(). + Immutable(), + field.Int("priority"). + Optional(). + Nillable(). + Immutable(), field.String("settlement"). GoType(creditpurchase.Settlement{}). diff --git a/openmeter/ledger/chargeadapter/creditpurchase.go b/openmeter/ledger/chargeadapter/creditpurchase.go index f31edec9a0..5de0ab3703 100644 --- a/openmeter/ledger/chargeadapter/creditpurchase.go +++ b/openmeter/ledger/chargeadapter/creditpurchase.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/alpacahq/alpacadecimal" + "github.com/samber/lo" chargecreditpurchase "github.com/openmeterio/openmeter/openmeter/billing/charges/creditpurchase" "github.com/openmeterio/openmeter/openmeter/billing/charges/models/ledgertransaction" @@ -77,10 +78,11 @@ func (h *creditPurchaseHandler) OnPromotionalCreditPurchase(ctx context.Context, Namespace: charge.Namespace, }, transactions.IssueCustomerReceivableTemplate{ - At: charge.CreatedAt, - Amount: charge.Intent.CreditAmount, - Currency: charge.Intent.Currency, - CostBasis: &costBasis, + At: charge.CreatedAt, + Amount: charge.Intent.CreditAmount, + Currency: charge.Intent.Currency, + CostBasis: &costBasis, + CreditPriority: lo.ToPtr(h.creditPurchasePriority(charge)), }, ) if err != nil { @@ -172,10 +174,11 @@ func (h *creditPurchaseHandler) OnCreditPurchaseInitiated(ctx context.Context, c if issuableAmount.IsPositive() { templates = append(templates, transactions.IssueCustomerReceivableTemplate{ - At: charge.CreatedAt, - Amount: issuableAmount, - Currency: charge.Intent.Currency, - CostBasis: &externalSettlement.CostBasis, + At: charge.CreatedAt, + Amount: issuableAmount, + Currency: charge.Intent.Currency, + CostBasis: &externalSettlement.CostBasis, + CreditPriority: lo.ToPtr(h.creditPurchasePriority(charge)), }) } @@ -312,6 +315,14 @@ func (h *creditPurchaseHandler) OnCreditPurchasePaymentSettled(ctx context.Conte }, nil } +func (h *creditPurchaseHandler) creditPurchasePriority(charge chargecreditpurchase.Charge) int { + if charge.Intent.Priority != nil { + return *charge.Intent.Priority + } + + return ledger.DefaultCustomerFBOPriority +} + func (h *creditPurchaseHandler) resolverDependencies() transactions.ResolverDependencies { return transactions.ResolverDependencies{ AccountService: h.accountResolver, diff --git a/test/credits/sanity_test.go b/test/credits/sanity_test.go index a292547ee2..7d25acca8f 100644 --- a/test/credits/sanity_test.go +++ b/test/credits/sanity_test.go @@ -405,6 +405,47 @@ func (s *CreditsTestSuite) TestFlatFeeCreditThenInvoiceSanity() { }) } +func (s *CreditsTestSuite) TestCreditPurchasePersistsPriority() { + ctx := context.Background() + ns := s.GetUniqueNamespace("charges-creditpurchase-persists-priority") + + cust := s.createLedgerBackedCustomer(ns, "test-subject") + s.NotEmpty(cust.ID) + + priority := 7 + at := datetime.MustParseTimeInLocation(s.T(), "2026-01-01T12:34:56Z", time.UTC).AsTime() + + intent := s.createCreditPurchaseIntent(createCreditPurchaseIntentInput{ + customer: cust.GetID(), + currency: USD, + amount: alpacadecimal.NewFromInt(25), + priority: &priority, + servicePeriod: timeutil.ClosedPeriod{From: at, To: at}, + settlement: creditpurchase.NewSettlement(creditpurchase.PromotionalSettlement{}), + }) + + res, err := s.Charges.Create(ctx, charges.CreateInput{ + Namespace: ns, + Intents: charges.ChargeIntents{ + intent, + }, + }) + s.NoError(err) + s.Len(res, 1) + + cpCharge, err := res[0].AsCreditPurchaseCharge() + s.NoError(err) + s.NotNil(cpCharge.State.CreditGrantRealization) + + fetchedCharge, err := s.mustGetChargeByID(cpCharge.GetChargeID()).AsCreditPurchaseCharge() + s.NoError(err) + s.Equal(&priority, fetchedCharge.Intent.Priority) + + zeroCostBasis := alpacadecimal.Zero + s.True(s.mustCustomerFBOBalanceWithPriority(cust.GetID(), USD, &zeroCostBasis, priority).Equal(alpacadecimal.NewFromInt(25))) + s.True(s.mustCustomerFBOBalance(cust.GetID(), USD, &zeroCostBasis).Equal(alpacadecimal.Zero)) +} + func (s *CreditsTestSuite) TestFlatFeeCreditOnlySanity() { ctx := context.Background() ns := s.GetUniqueNamespace("charges-sanity-test-credit-only") @@ -914,6 +955,10 @@ func (s *CreditsTestSuite) createLedgerBackedCustomer(ns string, subjectKey stri } func (s *CreditsTestSuite) mustCustomerFBOBalance(customerID customer.CustomerID, code currencyx.Code, costBasis *alpacadecimal.Decimal) alpacadecimal.Decimal { + return s.mustCustomerFBOBalanceWithPriority(customerID, code, costBasis, ledger.DefaultCustomerFBOPriority) +} + +func (s *CreditsTestSuite) mustCustomerFBOBalanceWithPriority(customerID customer.CustomerID, code currencyx.Code, costBasis *alpacadecimal.Decimal, priority int) alpacadecimal.Decimal { s.T().Helper() customerAccounts, err := s.LedgerResolver.GetCustomerAccounts(s.T().Context(), customerID) @@ -922,7 +967,7 @@ func (s *CreditsTestSuite) mustCustomerFBOBalance(customerID customer.CustomerID subAccount, err := customerAccounts.FBOAccount.GetSubAccountForRoute(s.T().Context(), ledger.CustomerFBORouteParams{ Currency: code, CostBasis: costBasis, - CreditPriority: ledger.DefaultCustomerFBOPriority, + CreditPriority: priority, }) s.NoError(err) @@ -1066,6 +1111,8 @@ type createCreditPurchaseIntentInput struct { customer customer.CustomerID currency currencyx.Code amount alpacadecimal.Decimal + effectiveAt *time.Time + priority *int servicePeriod timeutil.ClosedPeriod settlement creditpurchase.Settlement } @@ -1109,6 +1156,8 @@ func (s *CreditsTestSuite) createCreditPurchaseIntent(input createCreditPurchase FullServicePeriod: input.servicePeriod, }, CreditAmount: input.amount, + EffectiveAt: input.effectiveAt, + Priority: input.priority, Settlement: input.settlement, }) } diff --git a/tools/migrate/migrations/20260402130955_add_credit_purchase_effective_at_priority.down.sql b/tools/migrate/migrations/20260402130955_add_credit_purchase_effective_at_priority.down.sql new file mode 100644 index 0000000000..0edd6db030 --- /dev/null +++ b/tools/migrate/migrations/20260402130955_add_credit_purchase_effective_at_priority.down.sql @@ -0,0 +1,2 @@ +-- reverse: modify "charge_credit_purchases" table +ALTER TABLE "charge_credit_purchases" DROP COLUMN "priority", DROP COLUMN "effective_at"; diff --git a/tools/migrate/migrations/20260402130955_add_credit_purchase_effective_at_priority.up.sql b/tools/migrate/migrations/20260402130955_add_credit_purchase_effective_at_priority.up.sql new file mode 100644 index 0000000000..0a03c4e9e4 --- /dev/null +++ b/tools/migrate/migrations/20260402130955_add_credit_purchase_effective_at_priority.up.sql @@ -0,0 +1,2 @@ +-- modify "charge_credit_purchases" table +ALTER TABLE "charge_credit_purchases" ADD COLUMN "effective_at" timestamptz NULL, ADD COLUMN "priority" bigint NULL; diff --git a/tools/migrate/migrations/atlas.sum b/tools/migrate/migrations/atlas.sum index c4c65cb0a8..577bba55ed 100644 --- a/tools/migrate/migrations/atlas.sum +++ b/tools/migrate/migrations/atlas.sum @@ -1,4 +1,4 @@ -h1:k0HgNn13A0qDlL8wsujNWV7Vkh+GHSDBf+l19azl6YY= +h1:1EqE4oZQ3szvQBy8G9zuBoOZoDyNddvpn5aHNtGaiJE= 20240826120919_init.up.sql h1:tc1V91/smlmaeJGQ8h+MzTEeFjjnrrFDbDAjOYJK91o= 20240903155435_entitlement-expired-index.up.sql h1:Hp8u5uckmLXc1cRvWU0AtVnnK8ShlpzZNp8pbiJLhac= 20240917172257_billing-entities.up.sql h1:Q1dAMo0Vjiit76OybClNfYPGC5nmvov2/M2W1ioi4Kw= @@ -173,3 +173,4 @@ h1:k0HgNn13A0qDlL8wsujNWV7Vkh+GHSDBf+l19azl6YY= 20260326000000_llmcost_normalize_providers.up.sql h1:9wlY/e9rXm80IdWcOXAPXgL9e8/vqOSyFKTS1sxY6jk= 20260326163949_add_ledger_transaction_authorization_status.up.sql h1:IzLE0+Xg0jpDmkjLuOgPygWO1TVUzhDQuxj/Pq/oJXQ= 20260331103521_charges-negative-realization-runs.up.sql h1:KumaFFsXqo6SskhFBeLvZi8LFq5GFt5uXNevx4iNZ6A= +20260402130955_add_credit_purchase_effective_at_priority.up.sql h1:Gi2Xk3hjOlTgfaL3M/08RS0VyZ3/Lt2pcoanmhbApRM=