diff --git a/openmeter/billing/README.md b/openmeter/billing/README.md index 90b3ffb4b..3a606107d 100644 --- a/openmeter/billing/README.md +++ b/openmeter/billing/README.md @@ -37,31 +37,31 @@ TODO: document when implemented ### Invoices -The invoices are goverened by the [invoice state machine](./service/invoicestate.go). +The invoices are governed by the [invoice state machine](./service/invoicestate.go). Invoices are composed of [lines](./entity/invoiceline.go). Each invoice can only have lines from the same currency. The lines can be of different types: - ManualFee: one time manually added charge -- ManualUsageBased: manually added usage-based charge (can be used to charge addition usage-based prices without the product catalog features) +- ManualUsageBased: manually added usage-based charge (can be used to charge additional usage-based prices without the product catalog features) Each line has a `period` (`start`, `end`) and an `invoiceAt` property. The period specifies which period of time the line is referring to (in case of usage-based pricing, the underlying meter will be queried for this time-period). `invoiceAt` specifies the time when it is expected to create an invoice that contains this line. The invoice's collection settings can defer this. -Invoices are always created by collecting one or more line from the `gathering` invoices. The `/v1/api/billing/invoices/lines` endpoint can be used to create new future line items. A new invoice can be created any time. In such case the `gathering` items that are to be invoiced (`invoiceAt`) already are added to the invoice. Any usage-based line, that we can bill early is also added to the invoice for the period between the `period.start` of the line and the time of invoice creation. +Invoices are always created by collecting one or more line from the `gathering` invoices. The `/v1/api/billing/invoices/lines` endpoint can be used to create new future line items. A new invoice can be created any time. In such case, the `gathering` items to be invoiced (`invoiceAt`) are already added to the invoice. Any usage-based line, that we can bill early is also added to the invoice for the period between the `period.start` of the line and the time of invoice creation. ### Line splitting To achieve the behavior described above, we are using line splitting. By default we would have one line per billing period that would eventually be part of an invoice: ``` - period.start period.end + period.start period.end Line1 [status=valid] |--------------------------------------------------------| ``` When the usage-based line can be billed mid-period, we `split` the line into two: ``` - period.start asOf period.end + period.start asOf period.end Line1 [status=split] |--------------------------------------------------------| SplitLine1 [status=valid] |------------------| SplitLine2 [status=valid] |-------------------------------------| @@ -75,18 +75,18 @@ As visible: When creating a new invoice between `asof` and `period.end` the same logic continues, but without marking SplitLine2 `split`, instead the new line is added to the original line's parent line: ``` - period.start asOf1 asof2 period.end + period.start asOf1 asof2 period.end Line1 [status=split] |--------------------------------------------------------| SplitLine1 [status=valid] |------------------| SplitLine2 [status=valid] |---------------| SplitLine3 [status=valid] |---------------------| ``` -This flattening approach allows us to not to have to recusively traverse lines in the database. +This flattening approach allows us not to have to recursively traverse lines in the database. ### Usage-based quantity -When a line is created for an invoice, the quantity of the underlying merter is captured into the line's qty field. This information is never updated, so late events will have to create new invoice lines when needed. +When a line is created for an invoice, the quantity of the underlying meter is captured into the line's qty field. This information is never updated, so late events will have to create new invoice lines when needed. ### Detailed Lines @@ -116,16 +116,14 @@ Apps can choose to syncronize the original line (if the upstream system understa TODO: this is TBD as not implemented. -When we are dealing with a split line, the calculation is by taking the meter's quantity for the whole line period ([`parent.period.start`, `splitline.period.end`]) and the splitline's period (`splitline.period.start`, `splitline.period.end`). +When we are dealing with a split line, the calculation of the quantity is by taking the meter's quantity for the whole line period ([`parent.period.start`, `splitline.period.end`]) and the amount before the period (`splitline.period.start`, `splitline.period.start`). -When substracting the two we get two values: -- line qty: splitline's period -- before line usage: whole line period - splitline's period +When substracting the two we get the delta for the period (this gets the delta for all supported meter types except of Min and Avg). We execute the pricing logic (e.g. tiered pricing) for the line qty, while considering the before usage, as it reflects the already billed for items. Corner cases: -- Graduating tiered prices cannot be billed mid billing period (always arrears) -- Min, Avg meters are always billed arrears as they are not composable +- Graduating tiered prices cannot be billed mid-billing period (always arrears, as the calculation cannot be split into multiple items) +- Min, Avg meters are always billed arrears as we cannot calculate the delta. diff --git a/openmeter/billing/adapter/invoice.go b/openmeter/billing/adapter/invoice.go index 33665f117..6f3a76c23 100644 --- a/openmeter/billing/adapter/invoice.go +++ b/openmeter/billing/adapter/invoice.go @@ -246,7 +246,10 @@ func (r *adapter) CreateInvoice(ctx context.Context, input billing.CreateInvoice SetNillableDueAt(input.DueAt). SetNillableCustomerTimezone(customer.Timezone). SetNillableIssuedAt(lo.EmptyableToPtr(input.IssuedAt)). - SetCustomerSubjectKeys(input.Customer.UsageAttribution.SubjectKeys). + SetCustomerUsageAttribution(&billingentity.VersionedCustomerUsageAttribution{ + Type: billingentity.CustomerUsageAttributionTypeVersion, + CustomerUsageAttribution: input.Customer.UsageAttribution, + }). // Workflow (cloned) SetBillingWorkflowConfigID(clonedWorkflowConfig.ID). // TODO[later]: By cloning the AppIDs here we could support changing the apps in the billing profile if needed @@ -490,8 +493,8 @@ func mapInvoiceFromDB(invoice db.BillingInvoice, expand billingentity.InvoiceExp Line2: invoice.CustomerAddressLine2, PhoneNumber: invoice.CustomerAddressPhoneNumber, }, - Timezone: invoice.CustomerTimezone, - Subjects: invoice.CustomerSubjectKeys, + Timezone: invoice.CustomerTimezone, + UsageAttribution: invoice.CustomerUsageAttribution.CustomerUsageAttribution, }, Period: mapPeriodFromDB(invoice.PeriodStart, invoice.PeriodEnd), IssuedAt: invoice.IssuedAt, diff --git a/openmeter/billing/entity/invoice.go b/openmeter/billing/entity/invoice.go index daa3c48d5..b292237f6 100644 --- a/openmeter/billing/entity/invoice.go +++ b/openmeter/billing/entity/invoice.go @@ -9,6 +9,7 @@ import ( "github.com/invopop/gobl/bill" "github.com/samber/lo" + customerentity "github.com/openmeterio/openmeter/openmeter/customer/entity" "github.com/openmeterio/openmeter/pkg/currencyx" "github.com/openmeterio/openmeter/pkg/models" "github.com/openmeterio/openmeter/pkg/timezone" @@ -215,13 +216,25 @@ type InvoiceStatusDetails struct { AvailableActions []InvoiceAction `json:"availableActions"` } +const ( + CustomerUsageAttributionTypeVersion = "customer_usage_attribution.v1" +) + +type ( + CustomerUsageAttribution = customerentity.CustomerUsageAttribution + VersionedCustomerUsageAttribution struct { + CustomerUsageAttribution `json:",inline"` + Type string `json:"type"` + } +) + type InvoiceCustomer struct { CustomerID string `json:"customerId,omitempty"` - Name string `json:"name"` - BillingAddress *models.Address `json:"billingAddress,omitempty"` - Timezone *timezone.Timezone `json:"timezone,omitempty"` - Subjects []string `json:"subjects,omitempty"` + Name string `json:"name"` + BillingAddress *models.Address `json:"billingAddress,omitempty"` + Timezone *timezone.Timezone `json:"timezone,omitempty"` + UsageAttribution CustomerUsageAttribution `json:"usageAttribution"` } func (i *InvoiceCustomer) Validate() error { diff --git a/openmeter/billing/service/lineservice/manualusagebasedline.go b/openmeter/billing/service/lineservice/manualusagebasedline.go index dd4dacb17..30ee28f01 100644 --- a/openmeter/billing/service/lineservice/manualusagebasedline.go +++ b/openmeter/billing/service/lineservice/manualusagebasedline.go @@ -32,7 +32,7 @@ func (l manualUsageBasedLine) Validate(ctx context.Context, targetInvoice *billi return err } - if len(targetInvoice.Customer.Subjects) == 0 { + if len(targetInvoice.Customer.UsageAttribution.SubjectKeys) == 0 { return billingentity.ValidationError{ Err: billingentity.ErrInvoiceCreateUBPLineCustomerHasNoSubjects, } @@ -114,7 +114,7 @@ func (l manualUsageBasedLine) SnapshotQuantity(ctx context.Context, invoice *bil ParentLine: l.line.ParentLine, Feature: featureMeter.feature, Meter: featureMeter.meter, - Subjects: invoice.Customer.Subjects, + Subjects: invoice.Customer.UsageAttribution.SubjectKeys, }, ) if err != nil { diff --git a/openmeter/billing/service/lineservice/meters.go b/openmeter/billing/service/lineservice/meters.go index 08495c8ff..ba2902c33 100644 --- a/openmeter/billing/service/lineservice/meters.go +++ b/openmeter/billing/service/lineservice/meters.go @@ -83,42 +83,61 @@ func (s *Service) getFeatureUsage(ctx context.Context, in getFeatureUsageInput) } } - meterValues, err := s.StreamingConnector.QueryMeter( + // If we are the first line in the split, we don't need to calculate the pre period + if in.ParentLine == nil || in.ParentLine.Period.Start.Equal(in.Line.Period.Start) { + meterValues, err := s.StreamingConnector.QueryMeter( + ctx, + in.Line.Namespace, + in.Meter.Slug, + meterQueryParams, + ) + if err != nil { + return nil, fmt.Errorf("querying line[%s] meter[%s]: %w", in.Line.ID, in.Meter.Slug, err) + } + + return &featureUsageResponse{ + LinePeriodQty: summarizeMeterQueryRow(meterValues), + }, nil + } + + // Let's calculate [parent.start ... line.start] values + preLineQuery := meterQueryParams + preLineQuery.From = &in.ParentLine.Period.Start + preLineQuery.To = &in.Line.Period.Start + + preLineResult, err := s.StreamingConnector.QueryMeter( ctx, in.Line.Namespace, in.Meter.Slug, meterQueryParams, ) if err != nil { - return nil, fmt.Errorf("querying line[%s] meter[%s]: %w", in.Line.ID, in.Meter.Slug, err) + return nil, fmt.Errorf("querying pre line[%s] period meter[%s]: %w", in.ParentLine.ID, in.Meter.Slug, err) } - res := &featureUsageResponse{ - LinePeriodQty: summarizeMeterQueryRow(meterValues), - } - - // If we are the first line in the split, we don't need to calculate the pre period - if in.ParentLine == nil || in.ParentLine.Period.Start.Equal(in.Line.Period.Start) { - return res, nil - } + preLineQty := summarizeMeterQueryRow(preLineResult) - // Let's get the usage for the parent line to calculate the pre period - meterQueryParams.From = &in.ParentLine.Period.Start + // Let's calculate [parent.start ... line.end] values + upToLineEnd := meterQueryParams + upToLineEnd.From = &in.Line.Period.Start + upToLineEnd.To = &in.Line.Period.End - meterValues, err = s.StreamingConnector.QueryMeter( + upToLineEndResult, err := s.StreamingConnector.QueryMeter( ctx, in.Line.Namespace, in.Meter.Slug, - meterQueryParams, + upToLineEnd, ) if err != nil { - return nil, fmt.Errorf("querying parent line[%s] meter[%s]: %w", in.ParentLine.ID, in.Meter.Slug, err) + return nil, fmt.Errorf("querying up to line[%s] end meter[%s]: %w", in.ParentLine.ID, in.Meter.Slug, err) } - fullPeriodQty := summarizeMeterQueryRow(meterValues) - res.PreLinePeriodQty = fullPeriodQty.Sub(res.LinePeriodQty) + upToLineQty := summarizeMeterQueryRow(upToLineEndResult) - return res, nil + return &featureUsageResponse{ + LinePeriodQty: upToLineQty.Sub(preLineQty), + PreLinePeriodQty: preLineQty, + }, nil } func summarizeMeterQueryRow(in []models.MeterQueryRow) alpacadecimal.Decimal { diff --git a/openmeter/ent/db/billinginvoice.go b/openmeter/ent/db/billinginvoice.go index 825f56ce1..d6ca07f01 100644 --- a/openmeter/ent/db/billinginvoice.go +++ b/openmeter/ent/db/billinginvoice.go @@ -72,8 +72,8 @@ type BillingInvoice struct { CustomerName string `json:"customer_name,omitempty"` // CustomerTimezone holds the value of the "customer_timezone" field. CustomerTimezone *timezone.Timezone `json:"customer_timezone,omitempty"` - // CustomerSubjectKeys holds the value of the "customer_subject_keys" field. - CustomerSubjectKeys []string `json:"customer_subject_keys,omitempty"` + // CustomerUsageAttribution holds the value of the "customer_usage_attribution" field. + CustomerUsageAttribution *billingentity.VersionedCustomerUsageAttribution `json:"customer_usage_attribution,omitempty"` // Number holds the value of the "number" field. Number *string `json:"number,omitempty"` // Type holds the value of the "type" field. @@ -226,7 +226,7 @@ func (*BillingInvoice) scanValues(columns []string) ([]any, error) { values := make([]any, len(columns)) for i := range columns { switch columns[i] { - case billinginvoice.FieldMetadata, billinginvoice.FieldCustomerSubjectKeys: + case billinginvoice.FieldMetadata, billinginvoice.FieldCustomerUsageAttribution: values[i] = new([]byte) case billinginvoice.FieldID, billinginvoice.FieldNamespace, billinginvoice.FieldSupplierAddressCountry, billinginvoice.FieldSupplierAddressPostalCode, billinginvoice.FieldSupplierAddressState, billinginvoice.FieldSupplierAddressCity, billinginvoice.FieldSupplierAddressLine1, billinginvoice.FieldSupplierAddressLine2, billinginvoice.FieldSupplierAddressPhoneNumber, billinginvoice.FieldCustomerAddressCountry, billinginvoice.FieldCustomerAddressPostalCode, billinginvoice.FieldCustomerAddressState, billinginvoice.FieldCustomerAddressCity, billinginvoice.FieldCustomerAddressLine1, billinginvoice.FieldCustomerAddressLine2, billinginvoice.FieldCustomerAddressPhoneNumber, billinginvoice.FieldSupplierName, billinginvoice.FieldSupplierTaxCode, billinginvoice.FieldCustomerName, billinginvoice.FieldCustomerTimezone, billinginvoice.FieldNumber, billinginvoice.FieldType, billinginvoice.FieldDescription, billinginvoice.FieldCustomerID, billinginvoice.FieldSourceBillingProfileID, billinginvoice.FieldCurrency, billinginvoice.FieldStatus, billinginvoice.FieldWorkflowConfigID, billinginvoice.FieldTaxAppID, billinginvoice.FieldInvoicingAppID, billinginvoice.FieldPaymentAppID: values[i] = new(sql.NullString) @@ -410,12 +410,12 @@ func (bi *BillingInvoice) assignValues(columns []string, values []any) error { bi.CustomerTimezone = new(timezone.Timezone) *bi.CustomerTimezone = timezone.Timezone(value.String) } - case billinginvoice.FieldCustomerSubjectKeys: + case billinginvoice.FieldCustomerUsageAttribution: if value, ok := values[i].(*[]byte); !ok { - return fmt.Errorf("unexpected type %T for field customer_subject_keys", values[i]) + return fmt.Errorf("unexpected type %T for field customer_usage_attribution", values[i]) } else if value != nil && len(*value) > 0 { - if err := json.Unmarshal(*value, &bi.CustomerSubjectKeys); err != nil { - return fmt.Errorf("unmarshal field customer_subject_keys: %w", err) + if err := json.Unmarshal(*value, &bi.CustomerUsageAttribution); err != nil { + return fmt.Errorf("unmarshal field customer_usage_attribution: %w", err) } } case billinginvoice.FieldNumber: @@ -707,8 +707,8 @@ func (bi *BillingInvoice) String() string { builder.WriteString(fmt.Sprintf("%v", *v)) } builder.WriteString(", ") - builder.WriteString("customer_subject_keys=") - builder.WriteString(fmt.Sprintf("%v", bi.CustomerSubjectKeys)) + builder.WriteString("customer_usage_attribution=") + builder.WriteString(fmt.Sprintf("%v", bi.CustomerUsageAttribution)) builder.WriteString(", ") if v := bi.Number; v != nil { builder.WriteString("number=") diff --git a/openmeter/ent/db/billinginvoice/billinginvoice.go b/openmeter/ent/db/billinginvoice/billinginvoice.go index b02c6be73..c90e3ce87 100644 --- a/openmeter/ent/db/billinginvoice/billinginvoice.go +++ b/openmeter/ent/db/billinginvoice/billinginvoice.go @@ -62,8 +62,8 @@ const ( FieldCustomerName = "customer_name" // FieldCustomerTimezone holds the string denoting the customer_timezone field in the database. FieldCustomerTimezone = "customer_timezone" - // FieldCustomerSubjectKeys holds the string denoting the customer_subject_keys field in the database. - FieldCustomerSubjectKeys = "customer_subject_keys" + // FieldCustomerUsageAttribution holds the string denoting the customer_usage_attribution field in the database. + FieldCustomerUsageAttribution = "customer_usage_attribution" // FieldNumber holds the string denoting the number field in the database. FieldNumber = "number" // FieldType holds the string denoting the type field in the database. @@ -200,7 +200,7 @@ var Columns = []string{ FieldSupplierTaxCode, FieldCustomerName, FieldCustomerTimezone, - FieldCustomerSubjectKeys, + FieldCustomerUsageAttribution, FieldNumber, FieldType, FieldDescription, diff --git a/openmeter/ent/db/billinginvoice/where.go b/openmeter/ent/db/billinginvoice/where.go index efc291a9c..b1c447dc4 100644 --- a/openmeter/ent/db/billinginvoice/where.go +++ b/openmeter/ent/db/billinginvoice/where.go @@ -1850,16 +1850,6 @@ func CustomerTimezoneContainsFold(v timezone.Timezone) predicate.BillingInvoice return predicate.BillingInvoice(sql.FieldContainsFold(FieldCustomerTimezone, vc)) } -// CustomerSubjectKeysIsNil applies the IsNil predicate on the "customer_subject_keys" field. -func CustomerSubjectKeysIsNil() predicate.BillingInvoice { - return predicate.BillingInvoice(sql.FieldIsNull(FieldCustomerSubjectKeys)) -} - -// CustomerSubjectKeysNotNil applies the NotNil predicate on the "customer_subject_keys" field. -func CustomerSubjectKeysNotNil() predicate.BillingInvoice { - return predicate.BillingInvoice(sql.FieldNotNull(FieldCustomerSubjectKeys)) -} - // NumberEQ applies the EQ predicate on the "number" field. func NumberEQ(v string) predicate.BillingInvoice { return predicate.BillingInvoice(sql.FieldEQ(FieldNumber, v)) diff --git a/openmeter/ent/db/billinginvoice_create.go b/openmeter/ent/db/billinginvoice_create.go index 28b6cb25d..91d9ad077 100644 --- a/openmeter/ent/db/billinginvoice_create.go +++ b/openmeter/ent/db/billinginvoice_create.go @@ -323,9 +323,9 @@ func (bic *BillingInvoiceCreate) SetNillableCustomerTimezone(t *timezone.Timezon return bic } -// SetCustomerSubjectKeys sets the "customer_subject_keys" field. -func (bic *BillingInvoiceCreate) SetCustomerSubjectKeys(s []string) *BillingInvoiceCreate { - bic.mutation.SetCustomerSubjectKeys(s) +// SetCustomerUsageAttribution sets the "customer_usage_attribution" field. +func (bic *BillingInvoiceCreate) SetCustomerUsageAttribution(bcua *billingentity.VersionedCustomerUsageAttribution) *BillingInvoiceCreate { + bic.mutation.SetCustomerUsageAttribution(bcua) return bic } @@ -677,6 +677,9 @@ func (bic *BillingInvoiceCreate) check() error { return &ValidationError{Name: "customer_timezone", err: fmt.Errorf(`db: validator failed for field "BillingInvoice.customer_timezone": %w`, err)} } } + if _, ok := bic.mutation.CustomerUsageAttribution(); !ok { + return &ValidationError{Name: "customer_usage_attribution", err: errors.New(`db: missing required field "BillingInvoice.customer_usage_attribution"`)} + } if _, ok := bic.mutation.GetType(); !ok { return &ValidationError{Name: "type", err: errors.New(`db: missing required field "BillingInvoice.type"`)} } @@ -875,9 +878,9 @@ func (bic *BillingInvoiceCreate) createSpec() (*BillingInvoice, *sqlgraph.Create _spec.SetField(billinginvoice.FieldCustomerTimezone, field.TypeString, value) _node.CustomerTimezone = &value } - if value, ok := bic.mutation.CustomerSubjectKeys(); ok { - _spec.SetField(billinginvoice.FieldCustomerSubjectKeys, field.TypeJSON, value) - _node.CustomerSubjectKeys = value + if value, ok := bic.mutation.CustomerUsageAttribution(); ok { + _spec.SetField(billinginvoice.FieldCustomerUsageAttribution, field.TypeJSON, value) + _node.CustomerUsageAttribution = value } if value, ok := bic.mutation.Number(); ok { _spec.SetField(billinginvoice.FieldNumber, field.TypeString, value) @@ -1469,21 +1472,15 @@ func (u *BillingInvoiceUpsert) ClearCustomerTimezone() *BillingInvoiceUpsert { return u } -// SetCustomerSubjectKeys sets the "customer_subject_keys" field. -func (u *BillingInvoiceUpsert) SetCustomerSubjectKeys(v []string) *BillingInvoiceUpsert { - u.Set(billinginvoice.FieldCustomerSubjectKeys, v) - return u -} - -// UpdateCustomerSubjectKeys sets the "customer_subject_keys" field to the value that was provided on create. -func (u *BillingInvoiceUpsert) UpdateCustomerSubjectKeys() *BillingInvoiceUpsert { - u.SetExcluded(billinginvoice.FieldCustomerSubjectKeys) +// SetCustomerUsageAttribution sets the "customer_usage_attribution" field. +func (u *BillingInvoiceUpsert) SetCustomerUsageAttribution(v *billingentity.VersionedCustomerUsageAttribution) *BillingInvoiceUpsert { + u.Set(billinginvoice.FieldCustomerUsageAttribution, v) return u } -// ClearCustomerSubjectKeys clears the value of the "customer_subject_keys" field. -func (u *BillingInvoiceUpsert) ClearCustomerSubjectKeys() *BillingInvoiceUpsert { - u.SetNull(billinginvoice.FieldCustomerSubjectKeys) +// UpdateCustomerUsageAttribution sets the "customer_usage_attribution" field to the value that was provided on create. +func (u *BillingInvoiceUpsert) UpdateCustomerUsageAttribution() *BillingInvoiceUpsert { + u.SetExcluded(billinginvoice.FieldCustomerUsageAttribution) return u } @@ -2159,24 +2156,17 @@ func (u *BillingInvoiceUpsertOne) ClearCustomerTimezone() *BillingInvoiceUpsertO }) } -// SetCustomerSubjectKeys sets the "customer_subject_keys" field. -func (u *BillingInvoiceUpsertOne) SetCustomerSubjectKeys(v []string) *BillingInvoiceUpsertOne { - return u.Update(func(s *BillingInvoiceUpsert) { - s.SetCustomerSubjectKeys(v) - }) -} - -// UpdateCustomerSubjectKeys sets the "customer_subject_keys" field to the value that was provided on create. -func (u *BillingInvoiceUpsertOne) UpdateCustomerSubjectKeys() *BillingInvoiceUpsertOne { +// SetCustomerUsageAttribution sets the "customer_usage_attribution" field. +func (u *BillingInvoiceUpsertOne) SetCustomerUsageAttribution(v *billingentity.VersionedCustomerUsageAttribution) *BillingInvoiceUpsertOne { return u.Update(func(s *BillingInvoiceUpsert) { - s.UpdateCustomerSubjectKeys() + s.SetCustomerUsageAttribution(v) }) } -// ClearCustomerSubjectKeys clears the value of the "customer_subject_keys" field. -func (u *BillingInvoiceUpsertOne) ClearCustomerSubjectKeys() *BillingInvoiceUpsertOne { +// UpdateCustomerUsageAttribution sets the "customer_usage_attribution" field to the value that was provided on create. +func (u *BillingInvoiceUpsertOne) UpdateCustomerUsageAttribution() *BillingInvoiceUpsertOne { return u.Update(func(s *BillingInvoiceUpsert) { - s.ClearCustomerSubjectKeys() + s.UpdateCustomerUsageAttribution() }) } @@ -3049,24 +3039,17 @@ func (u *BillingInvoiceUpsertBulk) ClearCustomerTimezone() *BillingInvoiceUpsert }) } -// SetCustomerSubjectKeys sets the "customer_subject_keys" field. -func (u *BillingInvoiceUpsertBulk) SetCustomerSubjectKeys(v []string) *BillingInvoiceUpsertBulk { - return u.Update(func(s *BillingInvoiceUpsert) { - s.SetCustomerSubjectKeys(v) - }) -} - -// UpdateCustomerSubjectKeys sets the "customer_subject_keys" field to the value that was provided on create. -func (u *BillingInvoiceUpsertBulk) UpdateCustomerSubjectKeys() *BillingInvoiceUpsertBulk { +// SetCustomerUsageAttribution sets the "customer_usage_attribution" field. +func (u *BillingInvoiceUpsertBulk) SetCustomerUsageAttribution(v *billingentity.VersionedCustomerUsageAttribution) *BillingInvoiceUpsertBulk { return u.Update(func(s *BillingInvoiceUpsert) { - s.UpdateCustomerSubjectKeys() + s.SetCustomerUsageAttribution(v) }) } -// ClearCustomerSubjectKeys clears the value of the "customer_subject_keys" field. -func (u *BillingInvoiceUpsertBulk) ClearCustomerSubjectKeys() *BillingInvoiceUpsertBulk { +// UpdateCustomerUsageAttribution sets the "customer_usage_attribution" field to the value that was provided on create. +func (u *BillingInvoiceUpsertBulk) UpdateCustomerUsageAttribution() *BillingInvoiceUpsertBulk { return u.Update(func(s *BillingInvoiceUpsert) { - s.ClearCustomerSubjectKeys() + s.UpdateCustomerUsageAttribution() }) } diff --git a/openmeter/ent/db/billinginvoice_update.go b/openmeter/ent/db/billinginvoice_update.go index 6bb427698..76de53321 100644 --- a/openmeter/ent/db/billinginvoice_update.go +++ b/openmeter/ent/db/billinginvoice_update.go @@ -10,7 +10,6 @@ import ( "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" - "entgo.io/ent/dialect/sql/sqljson" "entgo.io/ent/schema/field" billingentity "github.com/openmeterio/openmeter/openmeter/billing/entity" "github.com/openmeterio/openmeter/openmeter/ent/db/billinginvoice" @@ -421,21 +420,9 @@ func (biu *BillingInvoiceUpdate) ClearCustomerTimezone() *BillingInvoiceUpdate { return biu } -// SetCustomerSubjectKeys sets the "customer_subject_keys" field. -func (biu *BillingInvoiceUpdate) SetCustomerSubjectKeys(s []string) *BillingInvoiceUpdate { - biu.mutation.SetCustomerSubjectKeys(s) - return biu -} - -// AppendCustomerSubjectKeys appends s to the "customer_subject_keys" field. -func (biu *BillingInvoiceUpdate) AppendCustomerSubjectKeys(s []string) *BillingInvoiceUpdate { - biu.mutation.AppendCustomerSubjectKeys(s) - return biu -} - -// ClearCustomerSubjectKeys clears the value of the "customer_subject_keys" field. -func (biu *BillingInvoiceUpdate) ClearCustomerSubjectKeys() *BillingInvoiceUpdate { - biu.mutation.ClearCustomerSubjectKeys() +// SetCustomerUsageAttribution sets the "customer_usage_attribution" field. +func (biu *BillingInvoiceUpdate) SetCustomerUsageAttribution(bcua *billingentity.VersionedCustomerUsageAttribution) *BillingInvoiceUpdate { + biu.mutation.SetCustomerUsageAttribution(bcua) return biu } @@ -958,16 +945,8 @@ func (biu *BillingInvoiceUpdate) sqlSave(ctx context.Context) (n int, err error) if biu.mutation.CustomerTimezoneCleared() { _spec.ClearField(billinginvoice.FieldCustomerTimezone, field.TypeString) } - if value, ok := biu.mutation.CustomerSubjectKeys(); ok { - _spec.SetField(billinginvoice.FieldCustomerSubjectKeys, field.TypeJSON, value) - } - if value, ok := biu.mutation.AppendedCustomerSubjectKeys(); ok { - _spec.AddModifier(func(u *sql.UpdateBuilder) { - sqljson.Append(u, billinginvoice.FieldCustomerSubjectKeys, value) - }) - } - if biu.mutation.CustomerSubjectKeysCleared() { - _spec.ClearField(billinginvoice.FieldCustomerSubjectKeys, field.TypeJSON) + if value, ok := biu.mutation.CustomerUsageAttribution(); ok { + _spec.SetField(billinginvoice.FieldCustomerUsageAttribution, field.TypeJSON, value) } if value, ok := biu.mutation.Number(); ok { _spec.SetField(billinginvoice.FieldNumber, field.TypeString, value) @@ -1548,21 +1527,9 @@ func (biuo *BillingInvoiceUpdateOne) ClearCustomerTimezone() *BillingInvoiceUpda return biuo } -// SetCustomerSubjectKeys sets the "customer_subject_keys" field. -func (biuo *BillingInvoiceUpdateOne) SetCustomerSubjectKeys(s []string) *BillingInvoiceUpdateOne { - biuo.mutation.SetCustomerSubjectKeys(s) - return biuo -} - -// AppendCustomerSubjectKeys appends s to the "customer_subject_keys" field. -func (biuo *BillingInvoiceUpdateOne) AppendCustomerSubjectKeys(s []string) *BillingInvoiceUpdateOne { - biuo.mutation.AppendCustomerSubjectKeys(s) - return biuo -} - -// ClearCustomerSubjectKeys clears the value of the "customer_subject_keys" field. -func (biuo *BillingInvoiceUpdateOne) ClearCustomerSubjectKeys() *BillingInvoiceUpdateOne { - biuo.mutation.ClearCustomerSubjectKeys() +// SetCustomerUsageAttribution sets the "customer_usage_attribution" field. +func (biuo *BillingInvoiceUpdateOne) SetCustomerUsageAttribution(bcua *billingentity.VersionedCustomerUsageAttribution) *BillingInvoiceUpdateOne { + biuo.mutation.SetCustomerUsageAttribution(bcua) return biuo } @@ -2115,16 +2082,8 @@ func (biuo *BillingInvoiceUpdateOne) sqlSave(ctx context.Context) (_node *Billin if biuo.mutation.CustomerTimezoneCleared() { _spec.ClearField(billinginvoice.FieldCustomerTimezone, field.TypeString) } - if value, ok := biuo.mutation.CustomerSubjectKeys(); ok { - _spec.SetField(billinginvoice.FieldCustomerSubjectKeys, field.TypeJSON, value) - } - if value, ok := biuo.mutation.AppendedCustomerSubjectKeys(); ok { - _spec.AddModifier(func(u *sql.UpdateBuilder) { - sqljson.Append(u, billinginvoice.FieldCustomerSubjectKeys, value) - }) - } - if biuo.mutation.CustomerSubjectKeysCleared() { - _spec.ClearField(billinginvoice.FieldCustomerSubjectKeys, field.TypeJSON) + if value, ok := biuo.mutation.CustomerUsageAttribution(); ok { + _spec.SetField(billinginvoice.FieldCustomerUsageAttribution, field.TypeJSON, value) } if value, ok := biuo.mutation.Number(); ok { _spec.SetField(billinginvoice.FieldNumber, field.TypeString, value) diff --git a/openmeter/ent/db/migrate/schema.go b/openmeter/ent/db/migrate/schema.go index dc9e66442..d9ec61c7b 100644 --- a/openmeter/ent/db/migrate/schema.go +++ b/openmeter/ent/db/migrate/schema.go @@ -322,7 +322,7 @@ var ( {Name: "supplier_tax_code", Type: field.TypeString, Nullable: true}, {Name: "customer_name", Type: field.TypeString}, {Name: "customer_timezone", Type: field.TypeString, Nullable: true}, - {Name: "customer_subject_keys", Type: field.TypeJSON, Nullable: true}, + {Name: "customer_usage_attribution", Type: field.TypeJSON}, {Name: "number", Type: field.TypeString, Nullable: true}, {Name: "type", Type: field.TypeEnum, Enums: []string{"standard", "credit-note"}}, {Name: "description", Type: field.TypeString, Nullable: true}, diff --git a/openmeter/ent/db/mutation.go b/openmeter/ent/db/mutation.go index 5ba24af89..af56e9c91 100644 --- a/openmeter/ent/db/mutation.go +++ b/openmeter/ent/db/mutation.go @@ -6198,8 +6198,7 @@ type BillingInvoiceMutation struct { supplier_tax_code *string customer_name *string customer_timezone *timezone.Timezone - customer_subject_keys *[]string - appendcustomer_subject_keys []string + customer_usage_attribution **billingentity.VersionedCustomerUsageAttribution number *string _type *billingentity.InvoiceType description *string @@ -7401,69 +7400,40 @@ func (m *BillingInvoiceMutation) ResetCustomerTimezone() { delete(m.clearedFields, billinginvoice.FieldCustomerTimezone) } -// SetCustomerSubjectKeys sets the "customer_subject_keys" field. -func (m *BillingInvoiceMutation) SetCustomerSubjectKeys(s []string) { - m.customer_subject_keys = &s - m.appendcustomer_subject_keys = nil +// SetCustomerUsageAttribution sets the "customer_usage_attribution" field. +func (m *BillingInvoiceMutation) SetCustomerUsageAttribution(bcua *billingentity.VersionedCustomerUsageAttribution) { + m.customer_usage_attribution = &bcua } -// CustomerSubjectKeys returns the value of the "customer_subject_keys" field in the mutation. -func (m *BillingInvoiceMutation) CustomerSubjectKeys() (r []string, exists bool) { - v := m.customer_subject_keys +// CustomerUsageAttribution returns the value of the "customer_usage_attribution" field in the mutation. +func (m *BillingInvoiceMutation) CustomerUsageAttribution() (r *billingentity.VersionedCustomerUsageAttribution, exists bool) { + v := m.customer_usage_attribution if v == nil { return } return *v, true } -// OldCustomerSubjectKeys returns the old "customer_subject_keys" field's value of the BillingInvoice entity. +// OldCustomerUsageAttribution returns the old "customer_usage_attribution" field's value of the BillingInvoice entity. // If the BillingInvoice 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 *BillingInvoiceMutation) OldCustomerSubjectKeys(ctx context.Context) (v []string, err error) { +func (m *BillingInvoiceMutation) OldCustomerUsageAttribution(ctx context.Context) (v *billingentity.VersionedCustomerUsageAttribution, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldCustomerSubjectKeys is only allowed on UpdateOne operations") + return v, errors.New("OldCustomerUsageAttribution is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldCustomerSubjectKeys requires an ID field in the mutation") + return v, errors.New("OldCustomerUsageAttribution requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldCustomerSubjectKeys: %w", err) + return v, fmt.Errorf("querying old value for OldCustomerUsageAttribution: %w", err) } - return oldValue.CustomerSubjectKeys, nil + return oldValue.CustomerUsageAttribution, nil } -// AppendCustomerSubjectKeys adds s to the "customer_subject_keys" field. -func (m *BillingInvoiceMutation) AppendCustomerSubjectKeys(s []string) { - m.appendcustomer_subject_keys = append(m.appendcustomer_subject_keys, s...) -} - -// AppendedCustomerSubjectKeys returns the list of values that were appended to the "customer_subject_keys" field in this mutation. -func (m *BillingInvoiceMutation) AppendedCustomerSubjectKeys() ([]string, bool) { - if len(m.appendcustomer_subject_keys) == 0 { - return nil, false - } - return m.appendcustomer_subject_keys, true -} - -// ClearCustomerSubjectKeys clears the value of the "customer_subject_keys" field. -func (m *BillingInvoiceMutation) ClearCustomerSubjectKeys() { - m.customer_subject_keys = nil - m.appendcustomer_subject_keys = nil - m.clearedFields[billinginvoice.FieldCustomerSubjectKeys] = struct{}{} -} - -// CustomerSubjectKeysCleared returns if the "customer_subject_keys" field was cleared in this mutation. -func (m *BillingInvoiceMutation) CustomerSubjectKeysCleared() bool { - _, ok := m.clearedFields[billinginvoice.FieldCustomerSubjectKeys] - return ok -} - -// ResetCustomerSubjectKeys resets all changes to the "customer_subject_keys" field. -func (m *BillingInvoiceMutation) ResetCustomerSubjectKeys() { - m.customer_subject_keys = nil - m.appendcustomer_subject_keys = nil - delete(m.clearedFields, billinginvoice.FieldCustomerSubjectKeys) +// ResetCustomerUsageAttribution resets all changes to the "customer_usage_attribution" field. +func (m *BillingInvoiceMutation) ResetCustomerUsageAttribution() { + m.customer_usage_attribution = nil } // SetNumber sets the "number" field. @@ -8582,8 +8552,8 @@ func (m *BillingInvoiceMutation) Fields() []string { if m.customer_timezone != nil { fields = append(fields, billinginvoice.FieldCustomerTimezone) } - if m.customer_subject_keys != nil { - fields = append(fields, billinginvoice.FieldCustomerSubjectKeys) + if m.customer_usage_attribution != nil { + fields = append(fields, billinginvoice.FieldCustomerUsageAttribution) } if m.number != nil { fields = append(fields, billinginvoice.FieldNumber) @@ -8690,8 +8660,8 @@ func (m *BillingInvoiceMutation) Field(name string) (ent.Value, bool) { return m.CustomerName() case billinginvoice.FieldCustomerTimezone: return m.CustomerTimezone() - case billinginvoice.FieldCustomerSubjectKeys: - return m.CustomerSubjectKeys() + case billinginvoice.FieldCustomerUsageAttribution: + return m.CustomerUsageAttribution() case billinginvoice.FieldNumber: return m.Number() case billinginvoice.FieldType: @@ -8781,8 +8751,8 @@ func (m *BillingInvoiceMutation) OldField(ctx context.Context, name string) (ent return m.OldCustomerName(ctx) case billinginvoice.FieldCustomerTimezone: return m.OldCustomerTimezone(ctx) - case billinginvoice.FieldCustomerSubjectKeys: - return m.OldCustomerSubjectKeys(ctx) + case billinginvoice.FieldCustomerUsageAttribution: + return m.OldCustomerUsageAttribution(ctx) case billinginvoice.FieldNumber: return m.OldNumber(ctx) case billinginvoice.FieldType: @@ -8987,12 +8957,12 @@ func (m *BillingInvoiceMutation) SetField(name string, value ent.Value) error { } m.SetCustomerTimezone(v) return nil - case billinginvoice.FieldCustomerSubjectKeys: - v, ok := value.([]string) + case billinginvoice.FieldCustomerUsageAttribution: + v, ok := value.(*billingentity.VersionedCustomerUsageAttribution) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetCustomerSubjectKeys(v) + m.SetCustomerUsageAttribution(v) return nil case billinginvoice.FieldNumber: v, ok := value.(string) @@ -9197,9 +9167,6 @@ func (m *BillingInvoiceMutation) ClearedFields() []string { if m.FieldCleared(billinginvoice.FieldCustomerTimezone) { fields = append(fields, billinginvoice.FieldCustomerTimezone) } - if m.FieldCleared(billinginvoice.FieldCustomerSubjectKeys) { - fields = append(fields, billinginvoice.FieldCustomerSubjectKeys) - } if m.FieldCleared(billinginvoice.FieldNumber) { fields = append(fields, billinginvoice.FieldNumber) } @@ -9292,9 +9259,6 @@ func (m *BillingInvoiceMutation) ClearField(name string) error { case billinginvoice.FieldCustomerTimezone: m.ClearCustomerTimezone() return nil - case billinginvoice.FieldCustomerSubjectKeys: - m.ClearCustomerSubjectKeys() - return nil case billinginvoice.FieldNumber: m.ClearNumber() return nil @@ -9396,8 +9360,8 @@ func (m *BillingInvoiceMutation) ResetField(name string) error { case billinginvoice.FieldCustomerTimezone: m.ResetCustomerTimezone() return nil - case billinginvoice.FieldCustomerSubjectKeys: - m.ResetCustomerSubjectKeys() + case billinginvoice.FieldCustomerUsageAttribution: + m.ResetCustomerUsageAttribution() return nil case billinginvoice.FieldNumber: m.ResetNumber() diff --git a/openmeter/ent/db/setorclear.go b/openmeter/ent/db/setorclear.go index 9cb8d0c7e..24e640c9f 100644 --- a/openmeter/ent/db/setorclear.go +++ b/openmeter/ent/db/setorclear.go @@ -490,20 +490,6 @@ func (u *BillingInvoiceUpdateOne) SetOrClearCustomerTimezone(value *timezone.Tim return u.SetCustomerTimezone(*value) } -func (u *BillingInvoiceUpdate) SetOrClearCustomerSubjectKeys(value *[]string) *BillingInvoiceUpdate { - if value == nil { - return u.ClearCustomerSubjectKeys() - } - return u.SetCustomerSubjectKeys(*value) -} - -func (u *BillingInvoiceUpdateOne) SetOrClearCustomerSubjectKeys(value *[]string) *BillingInvoiceUpdateOne { - if value == nil { - return u.ClearCustomerSubjectKeys() - } - return u.SetCustomerSubjectKeys(*value) -} - func (u *BillingInvoiceUpdate) SetOrClearNumber(value *string) *BillingInvoiceUpdate { if value == nil { return u.ClearNumber() diff --git a/openmeter/ent/schema/billing.go b/openmeter/ent/schema/billing.go index f27ea4135..5829624db 100644 --- a/openmeter/ent/schema/billing.go +++ b/openmeter/ent/schema/billing.go @@ -403,8 +403,7 @@ func (BillingInvoice) Fields() []ent.Field { Optional(). Nillable(), - field.Strings("customer_subject_keys"). - Optional(), + field.JSON("customer_usage_attribution", &billingentity.VersionedCustomerUsageAttribution{}), // Invoice number field.String("number"). diff --git a/test/billing/invoice_test.go b/test/billing/invoice_test.go index 3591b299d..21a792bcc 100644 --- a/test/billing/invoice_test.go +++ b/test/billing/invoice_test.go @@ -276,7 +276,9 @@ func (s *InvoicingTestSuite) TestPendingLineCreation() { Name: customerEntity.Name, BillingAddress: customerEntity.BillingAddress, - Subjects: []string{"test"}, + UsageAttribution: billingentity.CustomerUsageAttribution{ + SubjectKeys: []string{"test"}, + }, }, Supplier: billingProfile.Supplier, diff --git a/tools/migrate/migrations/20241107195915_billing-invoice-line-splitting.down.sql b/tools/migrate/migrations/20241108081324_billing-line-splitting.down.sql similarity index 96% rename from tools/migrate/migrations/20241107195915_billing-invoice-line-splitting.down.sql rename to tools/migrate/migrations/20241108081324_billing-line-splitting.down.sql index 663c61d51..71d518702 100644 --- a/tools/migrate/migrations/20241107195915_billing-invoice-line-splitting.down.sql +++ b/tools/migrate/migrations/20241108081324_billing-line-splitting.down.sql @@ -14,4 +14,4 @@ DROP INDEX "billinginvoicemanualusagebasedlineconfig_id"; -- reverse: create "billing_invoice_manual_usage_based_line_configs" table DROP TABLE "billing_invoice_manual_usage_based_line_configs"; -- reverse: modify "billing_invoices" table -ALTER TABLE "billing_invoices" DROP COLUMN "customer_subject_keys"; +ALTER TABLE "billing_invoices" DROP COLUMN "customer_usage_attribution"; diff --git a/tools/migrate/migrations/20241107195915_billing-invoice-line-splitting.up.sql b/tools/migrate/migrations/20241108081324_billing-line-splitting.up.sql similarity index 96% rename from tools/migrate/migrations/20241107195915_billing-invoice-line-splitting.up.sql rename to tools/migrate/migrations/20241108081324_billing-line-splitting.up.sql index aec3e93b6..21c948a1d 100644 --- a/tools/migrate/migrations/20241107195915_billing-invoice-line-splitting.up.sql +++ b/tools/migrate/migrations/20241108081324_billing-line-splitting.up.sql @@ -1,5 +1,5 @@ -- modify "billing_invoices" table -ALTER TABLE "billing_invoices" ADD COLUMN "customer_subject_keys" jsonb NULL; +ALTER TABLE "billing_invoices" ADD COLUMN "customer_usage_attribution" jsonb NOT NULL; -- create "billing_invoice_manual_usage_based_line_configs" table CREATE TABLE "billing_invoice_manual_usage_based_line_configs" ( "id" character(26) NOT NULL, diff --git a/tools/migrate/migrations/atlas.sum b/tools/migrate/migrations/atlas.sum index 0152a54f9..dd8c050b6 100644 --- a/tools/migrate/migrations/atlas.sum +++ b/tools/migrate/migrations/atlas.sum @@ -1,4 +1,4 @@ -h1:Pfezf5K9nBANzAXZ0gTeRA6GUAoM18AaCcQ02jccQdA= +h1:9cVC3nxDnjQeXnPpyLPfeOWYW/L9EGpxNn3XsdNH3s8= 20240826120919_init.down.sql h1:AIbgwwngjkJEYa3yRZsIXQyBa2+qoZttwMXHxXEbHLI= 20240826120919_init.up.sql h1:/hYHWF3Z3dab8SMKnw99ixVktCuJe2bAw5wstCZIEN8= 20240903155435_entitlement-expired-index.down.sql h1:np2xgYs3KQ2z7qPBcobtGNhqWQ3V8NwEP9E5U3TmpSA= @@ -39,5 +39,5 @@ h1:Pfezf5K9nBANzAXZ0gTeRA6GUAoM18AaCcQ02jccQdA= 20241103150058_billing-validation-issues.up.sql h1:LtBQCcCPEtm2VQuIXYTfk9335xlKyeZLFR38knmTqPk= 20241105171821_plan.down.sql h1:TiTrI/fgGxJQgHZu+rnSjHKqCh+dlR02uXR9c4NPbI0= 20241105171821_plan.up.sql h1:sA1026KYY5Hhxst4QyrfyFVTV5FL6gDGtYwGCk8gOlg= -20241107195915_billing-invoice-line-splitting.down.sql h1:5C08ZqEYcbRRm+XsPNfKXu0iFHQHUvxFB18T4/RA9cE= -20241107195915_billing-invoice-line-splitting.up.sql h1:baBpRjSyr3hWh2f2PWZoE7Zdhko+L5BJ0/Qq92h3FUg= +20241108081324_billing-line-splitting.down.sql h1:/LCed+gctmAGuWSY9gZmSdGfkbobwhcJ7HDgnnvmgE0= +20241108081324_billing-line-splitting.up.sql h1:wMGYA1q9jw98xugtJIHMeUXad+BfhnIsIJDzvgeGjHg=