diff --git a/openmeter/billing/adapter/invoice.go b/openmeter/billing/adapter/invoice.go index 27082890f..a35789a99 100644 --- a/openmeter/billing/adapter/invoice.go +++ b/openmeter/billing/adapter/invoice.go @@ -460,6 +460,7 @@ func (a *adapter) UpdateInvoice(ctx context.Context, in billing.UpdateInvoiceAda SetMetadata(in.Metadata). // Currency is immutable SetStatus(in.Status). + SetOrClearStatusDetailsCache(lo.EmptyableToPtr(in.StatusDetails)). // Type is immutable SetNumber(in.Number). SetOrClearDescription(in.Description). @@ -668,6 +669,7 @@ func (a *adapter) mapInvoiceFromDB(ctx context.Context, invoice *db.BillingInvoi Metadata: invoice.Metadata, Currency: invoice.Currency, Status: invoice.Status, + StatusDetails: invoice.StatusDetailsCache, Type: invoice.Type, Number: invoice.Number, Description: invoice.Description, diff --git a/openmeter/billing/httpdriver/invoice.go b/openmeter/billing/httpdriver/invoice.go index 42ee56472..c08044a65 100644 --- a/openmeter/billing/httpdriver/invoice.go +++ b/openmeter/billing/httpdriver/invoice.go @@ -59,7 +59,7 @@ func (h *handler) ListInvoices() ListInvoicesHandler { IssuedAfter: input.IssuedAfter, IssuedBefore: input.IssuedBefore, - Expand: mapInvoiceExpandToEntity(lo.FromPtrOr(input.Expand, nil)).SetGatheringTotals(true), + Expand: mapInvoiceExpandToEntity(lo.FromPtrOr(input.Expand, nil)).SetRecalculateGatheringInvoice(true), Order: sortx.Order(lo.FromPtrOr(input.Order, api.InvoiceOrderByOrderingOrder(sortx.OrderDefault))), OrderBy: lo.FromPtrOr(input.OrderBy, ""), @@ -196,7 +196,7 @@ func (h *handler) GetInvoice() GetInvoiceHandler { ID: params.InvoiceID, Namespace: ns, }, - Expand: mapInvoiceExpandToEntity(params.Expand).SetDeletedLines(params.IncludeDeletedLines).SetGatheringTotals(true), + Expand: mapInvoiceExpandToEntity(params.Expand).SetDeletedLines(params.IncludeDeletedLines).SetRecalculateGatheringInvoice(true), }, nil }, func(ctx context.Context, request GetInvoiceRequest) (GetInvoiceResponse, error) { diff --git a/openmeter/billing/invoice.go b/openmeter/billing/invoice.go index f01bf3cd1..b1cd04851 100644 --- a/openmeter/billing/invoice.go +++ b/openmeter/billing/invoice.go @@ -208,9 +208,9 @@ type InvoiceExpand struct { DeletedLines bool SplitLines bool - // GatheringTotals is used to calculate the totals of the invoice when gathering, this is temporary - // until we implement the full progressive billing stack. - GatheringTotals bool + // RecalculateGatheringInvoice is used to calculate the totals and status details of the invoice when gathering, + // this is temporary until we implement the full progressive billing stack, including gathering invoice recalculations. + RecalculateGatheringInvoice bool } var InvoiceExpandAll = InvoiceExpand{ @@ -241,8 +241,8 @@ func (e InvoiceExpand) SetSplitLines(v bool) InvoiceExpand { return e } -func (e InvoiceExpand) SetGatheringTotals(v bool) InvoiceExpand { - e.GatheringTotals = v +func (e InvoiceExpand) SetRecalculateGatheringInvoice(v bool) InvoiceExpand { + e.RecalculateGatheringInvoice = v return e } diff --git a/openmeter/billing/service/invoice.go b/openmeter/billing/service/invoice.go index fe77a9a2d..4f80349a5 100644 --- a/openmeter/billing/service/invoice.go +++ b/openmeter/billing/service/invoice.go @@ -39,7 +39,7 @@ func (s *Service) ListInvoices(ctx context.Context, input billing.ListInvoicesIn return billing.ListInvoicesResponse{}, fmt.Errorf("error resolving status details for invoice [%s]: %w", invoices.Items[i].ID, err) } - if input.Expand.GatheringTotals { + if input.Expand.RecalculateGatheringInvoice { invoices.Items[i], err = s.recalculateGatheringInvoice(ctx, recalculateGatheringInvoiceInput{ Invoice: invoices.Items[i], Expand: input.Expand, @@ -71,6 +71,18 @@ func (s *Service) resolveWorkflowApps(ctx context.Context, invoice billing.Invoi } func (s *Service) resolveStatusDetails(ctx context.Context, invoice billing.Invoice) (billing.Invoice, error) { + if invoice.Status == billing.InvoiceStatusGathering { + // Let's use the default and recalculateGatheringInvoice will fix the gaps + return invoice, nil + } + + // If we are not in a time sensitive state and the status details is not empty, we can return the invoice as is, so we + // don't have to lock the invoice in the DB + if !lo.IsEmpty(invoice.StatusDetails) && + invoice.Status != billing.InvoiceStatusDraftWaitingAutoApproval { // The status details depends on the current time, so we should recalculate + return invoice, nil + } + // let's resolve the statatus details (the invoice state machine has this side-effect after the callback) resolvedInvoice, err := s.WithInvoiceStateMachine(ctx, invoice, func(ctx context.Context, sm *InvoiceStateMachine) error { return nil @@ -176,10 +188,11 @@ func (s *Service) recalculateGatheringInvoice(ctx context.Context, in recalculat // TODO[later]: If this sugar is removed due to properly implemented progressive billing stack, we need to cache the when the invoice is first invoicable in the db // so that we don't have to fetch all the lines to have proper status details. - if hasInvoicableLines.IsAbsent() { - invoice.StatusDetails.AvailableActions.Invoice = nil - } else { - invoice.StatusDetails.AvailableActions.Invoice = &billing.InvoiceAvailableActionInvoiceDetails{} + invoice.StatusDetails = billing.InvoiceStatusDetails{ + Immutable: false, + AvailableActions: billing.InvoiceAvailableActions{ + Invoice: lo.If(hasInvoicableLines.IsPresent(), &billing.InvoiceAvailableActionInvoiceDetails{}).Else(nil), + }, } return invoice, nil @@ -201,7 +214,7 @@ func (s *Service) GetInvoiceByID(ctx context.Context, input billing.GetInvoiceBy return billing.Invoice{}, fmt.Errorf("error resolving status details for invoice [%s]: %w", invoice.ID, err) } - if input.Expand.GatheringTotals { + if input.Expand.RecalculateGatheringInvoice { invoice, err = s.recalculateGatheringInvoice(ctx, recalculateGatheringInvoiceInput{ Invoice: invoice, Expand: input.Expand, diff --git a/openmeter/billing/service/invoicestate.go b/openmeter/billing/service/invoicestate.go index e1f80c49d..6ff11cfba 100644 --- a/openmeter/billing/service/invoicestate.go +++ b/openmeter/billing/service/invoicestate.go @@ -262,18 +262,9 @@ func (s *Service) WithInvoiceStateMachine(ctx context.Context, invoice billing.I func (m *InvoiceStateMachine) StatusDetails(ctx context.Context) (billing.InvoiceStatusDetails, error) { if m.Invoice.Status == billing.InvoiceStatusGathering { // Gathering is a special state that is not part of the state machine, due to - // cross invoice operations - return billing.InvoiceStatusDetails{ - Immutable: false, - // The invoicable state is calculated in the services recalculateGatheringInvoice for now, as the - // line data is available there. On the long run we need to cache this information. - // - // For now, as a safety measure we lie here, as the recalculation will be performed either ways - // and the CreateInvoice method will validate this once more. - AvailableActions: billing.InvoiceAvailableActions{ - Invoice: &billing.InvoiceAvailableActionInvoiceDetails{}, - }, - }, nil + // cross invoice operations, for now the sugar around grathering invoices will handle + // the status details. + return billing.InvoiceStatusDetails{}, nil } var outErr, err error diff --git a/openmeter/ent/db/billinginvoice.go b/openmeter/ent/db/billinginvoice.go index 43caa961d..7c5a6a713 100644 --- a/openmeter/ent/db/billinginvoice.go +++ b/openmeter/ent/db/billinginvoice.go @@ -110,6 +110,8 @@ type BillingInvoice struct { DueAt *time.Time `json:"due_at,omitempty"` // Status holds the value of the "status" field. Status billing.InvoiceStatus `json:"status,omitempty"` + // StatusDetailsCache holds the value of the "status_details_cache" field. + StatusDetailsCache billing.InvoiceStatusDetails `json:"status_details_cache,omitempty"` // WorkflowConfigID holds the value of the "workflow_config_id" field. WorkflowConfigID string `json:"workflow_config_id,omitempty"` // TaxAppID holds the value of the "tax_app_id" field. @@ -259,7 +261,7 @@ func (*BillingInvoice) scanValues(columns []string) ([]any, error) { values := make([]any, len(columns)) for i := range columns { switch columns[i] { - case billinginvoice.FieldMetadata, billinginvoice.FieldCustomerUsageAttribution: + case billinginvoice.FieldMetadata, billinginvoice.FieldCustomerUsageAttribution, billinginvoice.FieldStatusDetailsCache: values[i] = new([]byte) case billinginvoice.FieldAmount, billinginvoice.FieldTaxesTotal, billinginvoice.FieldTaxesInclusiveTotal, billinginvoice.FieldTaxesExclusiveTotal, billinginvoice.FieldChargesTotal, billinginvoice.FieldDiscountsTotal, billinginvoice.FieldTotal: values[i] = new(alpacadecimal.Decimal) @@ -566,6 +568,14 @@ func (bi *BillingInvoice) assignValues(columns []string, values []any) error { } else if value.Valid { bi.Status = billing.InvoiceStatus(value.String) } + case billinginvoice.FieldStatusDetailsCache: + if value, ok := values[i].(*[]byte); !ok { + return fmt.Errorf("unexpected type %T for field status_details_cache", values[i]) + } else if value != nil && len(*value) > 0 { + if err := json.Unmarshal(*value, &bi.StatusDetailsCache); err != nil { + return fmt.Errorf("unmarshal field status_details_cache: %w", err) + } + } case billinginvoice.FieldWorkflowConfigID: if value, ok := values[i].(*sql.NullString); !ok { return fmt.Errorf("unexpected type %T for field workflow_config_id", values[i]) @@ -882,6 +892,9 @@ func (bi *BillingInvoice) String() string { builder.WriteString("status=") builder.WriteString(fmt.Sprintf("%v", bi.Status)) builder.WriteString(", ") + builder.WriteString("status_details_cache=") + builder.WriteString(fmt.Sprintf("%v", bi.StatusDetailsCache)) + builder.WriteString(", ") builder.WriteString("workflow_config_id=") builder.WriteString(bi.WorkflowConfigID) builder.WriteString(", ") diff --git a/openmeter/ent/db/billinginvoice/billinginvoice.go b/openmeter/ent/db/billinginvoice/billinginvoice.go index d8fc1ae3d..311a6cc35 100644 --- a/openmeter/ent/db/billinginvoice/billinginvoice.go +++ b/openmeter/ent/db/billinginvoice/billinginvoice.go @@ -100,6 +100,8 @@ const ( FieldDueAt = "due_at" // FieldStatus holds the string denoting the status field in the database. FieldStatus = "status" + // FieldStatusDetailsCache holds the string denoting the status_details_cache field in the database. + FieldStatusDetailsCache = "status_details_cache" // FieldWorkflowConfigID holds the string denoting the workflow_config_id field in the database. FieldWorkflowConfigID = "workflow_config_id" // FieldTaxAppID holds the string denoting the tax_app_id field in the database. @@ -250,6 +252,7 @@ var Columns = []string{ FieldCurrency, FieldDueAt, FieldStatus, + FieldStatusDetailsCache, FieldWorkflowConfigID, FieldTaxAppID, FieldInvoicingAppID, diff --git a/openmeter/ent/db/billinginvoice/where.go b/openmeter/ent/db/billinginvoice/where.go index 537236e04..3ec76f33f 100644 --- a/openmeter/ent/db/billinginvoice/where.go +++ b/openmeter/ent/db/billinginvoice/where.go @@ -2754,6 +2754,16 @@ func StatusNotIn(vs ...billing.InvoiceStatus) predicate.BillingInvoice { return predicate.BillingInvoice(sql.FieldNotIn(FieldStatus, v...)) } +// StatusDetailsCacheIsNil applies the IsNil predicate on the "status_details_cache" field. +func StatusDetailsCacheIsNil() predicate.BillingInvoice { + return predicate.BillingInvoice(sql.FieldIsNull(FieldStatusDetailsCache)) +} + +// StatusDetailsCacheNotNil applies the NotNil predicate on the "status_details_cache" field. +func StatusDetailsCacheNotNil() predicate.BillingInvoice { + return predicate.BillingInvoice(sql.FieldNotNull(FieldStatusDetailsCache)) +} + // WorkflowConfigIDEQ applies the EQ predicate on the "workflow_config_id" field. func WorkflowConfigIDEQ(v string) predicate.BillingInvoice { return predicate.BillingInvoice(sql.FieldEQ(FieldWorkflowConfigID, v)) diff --git a/openmeter/ent/db/billinginvoice_create.go b/openmeter/ent/db/billinginvoice_create.go index 1f4c5c5eb..cfa5a974a 100644 --- a/openmeter/ent/db/billinginvoice_create.go +++ b/openmeter/ent/db/billinginvoice_create.go @@ -478,6 +478,20 @@ func (bic *BillingInvoiceCreate) SetStatus(bs billing.InvoiceStatus) *BillingInv return bic } +// SetStatusDetailsCache sets the "status_details_cache" field. +func (bic *BillingInvoiceCreate) SetStatusDetailsCache(bsd billing.InvoiceStatusDetails) *BillingInvoiceCreate { + bic.mutation.SetStatusDetailsCache(bsd) + return bic +} + +// SetNillableStatusDetailsCache sets the "status_details_cache" field if the given value is not nil. +func (bic *BillingInvoiceCreate) SetNillableStatusDetailsCache(bsd *billing.InvoiceStatusDetails) *BillingInvoiceCreate { + if bsd != nil { + bic.SetStatusDetailsCache(*bsd) + } + return bic +} + // SetWorkflowConfigID sets the "workflow_config_id" field. func (bic *BillingInvoiceCreate) SetWorkflowConfigID(s string) *BillingInvoiceCreate { bic.mutation.SetWorkflowConfigID(s) @@ -1075,6 +1089,10 @@ func (bic *BillingInvoiceCreate) createSpec() (*BillingInvoice, *sqlgraph.Create _spec.SetField(billinginvoice.FieldStatus, field.TypeEnum, value) _node.Status = value } + if value, ok := bic.mutation.StatusDetailsCache(); ok { + _spec.SetField(billinginvoice.FieldStatusDetailsCache, field.TypeJSON, value) + _node.StatusDetailsCache = value + } if value, ok := bic.mutation.InvoicingAppExternalID(); ok { _spec.SetField(billinginvoice.FieldInvoicingAppExternalID, field.TypeString, value) _node.InvoicingAppExternalID = &value @@ -1883,6 +1901,24 @@ func (u *BillingInvoiceUpsert) UpdateStatus() *BillingInvoiceUpsert { return u } +// SetStatusDetailsCache sets the "status_details_cache" field. +func (u *BillingInvoiceUpsert) SetStatusDetailsCache(v billing.InvoiceStatusDetails) *BillingInvoiceUpsert { + u.Set(billinginvoice.FieldStatusDetailsCache, v) + return u +} + +// UpdateStatusDetailsCache sets the "status_details_cache" field to the value that was provided on create. +func (u *BillingInvoiceUpsert) UpdateStatusDetailsCache() *BillingInvoiceUpsert { + u.SetExcluded(billinginvoice.FieldStatusDetailsCache) + return u +} + +// ClearStatusDetailsCache clears the value of the "status_details_cache" field. +func (u *BillingInvoiceUpsert) ClearStatusDetailsCache() *BillingInvoiceUpsert { + u.SetNull(billinginvoice.FieldStatusDetailsCache) + return u +} + // SetWorkflowConfigID sets the "workflow_config_id" field. func (u *BillingInvoiceUpsert) SetWorkflowConfigID(v string) *BillingInvoiceUpsert { u.Set(billinginvoice.FieldWorkflowConfigID, v) @@ -2754,6 +2790,27 @@ func (u *BillingInvoiceUpsertOne) UpdateStatus() *BillingInvoiceUpsertOne { }) } +// SetStatusDetailsCache sets the "status_details_cache" field. +func (u *BillingInvoiceUpsertOne) SetStatusDetailsCache(v billing.InvoiceStatusDetails) *BillingInvoiceUpsertOne { + return u.Update(func(s *BillingInvoiceUpsert) { + s.SetStatusDetailsCache(v) + }) +} + +// UpdateStatusDetailsCache sets the "status_details_cache" field to the value that was provided on create. +func (u *BillingInvoiceUpsertOne) UpdateStatusDetailsCache() *BillingInvoiceUpsertOne { + return u.Update(func(s *BillingInvoiceUpsert) { + s.UpdateStatusDetailsCache() + }) +} + +// ClearStatusDetailsCache clears the value of the "status_details_cache" field. +func (u *BillingInvoiceUpsertOne) ClearStatusDetailsCache() *BillingInvoiceUpsertOne { + return u.Update(func(s *BillingInvoiceUpsert) { + s.ClearStatusDetailsCache() + }) +} + // SetWorkflowConfigID sets the "workflow_config_id" field. func (u *BillingInvoiceUpsertOne) SetWorkflowConfigID(v string) *BillingInvoiceUpsertOne { return u.Update(func(s *BillingInvoiceUpsert) { @@ -3812,6 +3869,27 @@ func (u *BillingInvoiceUpsertBulk) UpdateStatus() *BillingInvoiceUpsertBulk { }) } +// SetStatusDetailsCache sets the "status_details_cache" field. +func (u *BillingInvoiceUpsertBulk) SetStatusDetailsCache(v billing.InvoiceStatusDetails) *BillingInvoiceUpsertBulk { + return u.Update(func(s *BillingInvoiceUpsert) { + s.SetStatusDetailsCache(v) + }) +} + +// UpdateStatusDetailsCache sets the "status_details_cache" field to the value that was provided on create. +func (u *BillingInvoiceUpsertBulk) UpdateStatusDetailsCache() *BillingInvoiceUpsertBulk { + return u.Update(func(s *BillingInvoiceUpsert) { + s.UpdateStatusDetailsCache() + }) +} + +// ClearStatusDetailsCache clears the value of the "status_details_cache" field. +func (u *BillingInvoiceUpsertBulk) ClearStatusDetailsCache() *BillingInvoiceUpsertBulk { + return u.Update(func(s *BillingInvoiceUpsert) { + s.ClearStatusDetailsCache() + }) +} + // SetWorkflowConfigID sets the "workflow_config_id" field. func (u *BillingInvoiceUpsertBulk) SetWorkflowConfigID(v string) *BillingInvoiceUpsertBulk { return u.Update(func(s *BillingInvoiceUpsert) { diff --git a/openmeter/ent/db/billinginvoice_update.go b/openmeter/ent/db/billinginvoice_update.go index df74a8441..298c55aaf 100644 --- a/openmeter/ent/db/billinginvoice_update.go +++ b/openmeter/ent/db/billinginvoice_update.go @@ -667,6 +667,26 @@ func (biu *BillingInvoiceUpdate) SetNillableStatus(bs *billing.InvoiceStatus) *B return biu } +// SetStatusDetailsCache sets the "status_details_cache" field. +func (biu *BillingInvoiceUpdate) SetStatusDetailsCache(bsd billing.InvoiceStatusDetails) *BillingInvoiceUpdate { + biu.mutation.SetStatusDetailsCache(bsd) + return biu +} + +// SetNillableStatusDetailsCache sets the "status_details_cache" field if the given value is not nil. +func (biu *BillingInvoiceUpdate) SetNillableStatusDetailsCache(bsd *billing.InvoiceStatusDetails) *BillingInvoiceUpdate { + if bsd != nil { + biu.SetStatusDetailsCache(*bsd) + } + return biu +} + +// ClearStatusDetailsCache clears the value of the "status_details_cache" field. +func (biu *BillingInvoiceUpdate) ClearStatusDetailsCache() *BillingInvoiceUpdate { + biu.mutation.ClearStatusDetailsCache() + return biu +} + // SetWorkflowConfigID sets the "workflow_config_id" field. func (biu *BillingInvoiceUpdate) SetWorkflowConfigID(s string) *BillingInvoiceUpdate { biu.mutation.SetWorkflowConfigID(s) @@ -1212,6 +1232,12 @@ func (biu *BillingInvoiceUpdate) sqlSave(ctx context.Context) (n int, err error) if value, ok := biu.mutation.Status(); ok { _spec.SetField(billinginvoice.FieldStatus, field.TypeEnum, value) } + if value, ok := biu.mutation.StatusDetailsCache(); ok { + _spec.SetField(billinginvoice.FieldStatusDetailsCache, field.TypeJSON, value) + } + if biu.mutation.StatusDetailsCacheCleared() { + _spec.ClearField(billinginvoice.FieldStatusDetailsCache, field.TypeJSON) + } if value, ok := biu.mutation.InvoicingAppExternalID(); ok { _spec.SetField(billinginvoice.FieldInvoicingAppExternalID, field.TypeString, value) } @@ -2064,6 +2090,26 @@ func (biuo *BillingInvoiceUpdateOne) SetNillableStatus(bs *billing.InvoiceStatus return biuo } +// SetStatusDetailsCache sets the "status_details_cache" field. +func (biuo *BillingInvoiceUpdateOne) SetStatusDetailsCache(bsd billing.InvoiceStatusDetails) *BillingInvoiceUpdateOne { + biuo.mutation.SetStatusDetailsCache(bsd) + return biuo +} + +// SetNillableStatusDetailsCache sets the "status_details_cache" field if the given value is not nil. +func (biuo *BillingInvoiceUpdateOne) SetNillableStatusDetailsCache(bsd *billing.InvoiceStatusDetails) *BillingInvoiceUpdateOne { + if bsd != nil { + biuo.SetStatusDetailsCache(*bsd) + } + return biuo +} + +// ClearStatusDetailsCache clears the value of the "status_details_cache" field. +func (biuo *BillingInvoiceUpdateOne) ClearStatusDetailsCache() *BillingInvoiceUpdateOne { + biuo.mutation.ClearStatusDetailsCache() + return biuo +} + // SetWorkflowConfigID sets the "workflow_config_id" field. func (biuo *BillingInvoiceUpdateOne) SetWorkflowConfigID(s string) *BillingInvoiceUpdateOne { biuo.mutation.SetWorkflowConfigID(s) @@ -2639,6 +2685,12 @@ func (biuo *BillingInvoiceUpdateOne) sqlSave(ctx context.Context) (_node *Billin if value, ok := biuo.mutation.Status(); ok { _spec.SetField(billinginvoice.FieldStatus, field.TypeEnum, value) } + if value, ok := biuo.mutation.StatusDetailsCache(); ok { + _spec.SetField(billinginvoice.FieldStatusDetailsCache, field.TypeJSON, value) + } + if biuo.mutation.StatusDetailsCacheCleared() { + _spec.ClearField(billinginvoice.FieldStatusDetailsCache, field.TypeJSON) + } if value, ok := biuo.mutation.InvoicingAppExternalID(); ok { _spec.SetField(billinginvoice.FieldInvoicingAppExternalID, field.TypeString, value) } diff --git a/openmeter/ent/db/migrate/schema.go b/openmeter/ent/db/migrate/schema.go index 2ec42570d..84d14ae65 100644 --- a/openmeter/ent/db/migrate/schema.go +++ b/openmeter/ent/db/migrate/schema.go @@ -346,6 +346,7 @@ var ( {Name: "currency", Type: field.TypeString, SchemaType: map[string]string{"postgres": "varchar(3)"}}, {Name: "due_at", Type: field.TypeTime, Nullable: true}, {Name: "status", Type: field.TypeEnum, Enums: []string{"gathering", "draft.created", "draft.updating", "draft.manual_approval_needed", "draft.validating", "draft.invalid", "draft.syncing", "draft.sync_failed", "draft.waiting_auto_approval", "draft.ready_to_issue", "delete.in_progress", "delete.syncing", "delete.failed", "deleted", "issuing.syncing", "issuing.failed", "issued", "payment_processing.pending", "payment_processing.failed", "payment_processing.action_required", "overdue", "paid", "uncollectible", "voided"}}, + {Name: "status_details_cache", Type: field.TypeJSON, Nullable: true}, {Name: "invoicing_app_external_id", Type: field.TypeString, Nullable: true}, {Name: "payment_app_external_id", Type: field.TypeString, Nullable: true}, {Name: "tax_app_external_id", Type: field.TypeString, Nullable: true}, @@ -367,37 +368,37 @@ var ( ForeignKeys: []*schema.ForeignKey{ { Symbol: "billing_invoices_apps_billing_invoice_tax_app", - Columns: []*schema.Column{BillingInvoicesColumns[47]}, + Columns: []*schema.Column{BillingInvoicesColumns[48]}, RefColumns: []*schema.Column{AppsColumns[0]}, OnDelete: schema.NoAction, }, { Symbol: "billing_invoices_apps_billing_invoice_invoicing_app", - Columns: []*schema.Column{BillingInvoicesColumns[48]}, + Columns: []*schema.Column{BillingInvoicesColumns[49]}, RefColumns: []*schema.Column{AppsColumns[0]}, OnDelete: schema.NoAction, }, { Symbol: "billing_invoices_apps_billing_invoice_payment_app", - Columns: []*schema.Column{BillingInvoicesColumns[49]}, + Columns: []*schema.Column{BillingInvoicesColumns[50]}, RefColumns: []*schema.Column{AppsColumns[0]}, OnDelete: schema.NoAction, }, { Symbol: "billing_invoices_billing_profiles_billing_invoices", - Columns: []*schema.Column{BillingInvoicesColumns[50]}, + Columns: []*schema.Column{BillingInvoicesColumns[51]}, RefColumns: []*schema.Column{BillingProfilesColumns[0]}, OnDelete: schema.NoAction, }, { Symbol: "billing_invoices_billing_workflow_configs_billing_invoices", - Columns: []*schema.Column{BillingInvoicesColumns[51]}, + Columns: []*schema.Column{BillingInvoicesColumns[52]}, RefColumns: []*schema.Column{BillingWorkflowConfigsColumns[0]}, OnDelete: schema.NoAction, }, { Symbol: "billing_invoices_customers_billing_invoice", - Columns: []*schema.Column{BillingInvoicesColumns[52]}, + Columns: []*schema.Column{BillingInvoicesColumns[53]}, RefColumns: []*schema.Column{CustomersColumns[0]}, OnDelete: schema.NoAction, }, @@ -421,7 +422,22 @@ var ( { Name: "billinginvoice_namespace_customer_id", Unique: false, - Columns: []*schema.Column{BillingInvoicesColumns[1], BillingInvoicesColumns[52]}, + Columns: []*schema.Column{BillingInvoicesColumns[1], BillingInvoicesColumns[53]}, + }, + { + Name: "billinginvoice_namespace_status", + Unique: false, + Columns: []*schema.Column{BillingInvoicesColumns[1], BillingInvoicesColumns[40]}, + }, + { + Name: "billinginvoice_status_details_cache", + Unique: false, + Columns: []*schema.Column{BillingInvoicesColumns[41]}, + Annotation: &entsql.IndexAnnotation{ + Types: map[string]string{ + "postgres": "GIN", + }, + }, }, }, } diff --git a/openmeter/ent/db/mutation.go b/openmeter/ent/db/mutation.go index 2ac96edda..a10711c21 100644 --- a/openmeter/ent/db/mutation.go +++ b/openmeter/ent/db/mutation.go @@ -6371,6 +6371,7 @@ type BillingInvoiceMutation struct { currency *currencyx.Code due_at *time.Time status *billing.InvoiceStatus + status_details_cache *billing.InvoiceStatusDetails invoicing_app_external_id *string payment_app_external_id *string tax_app_external_id *string @@ -8319,6 +8320,55 @@ func (m *BillingInvoiceMutation) ResetStatus() { m.status = nil } +// SetStatusDetailsCache sets the "status_details_cache" field. +func (m *BillingInvoiceMutation) SetStatusDetailsCache(bsd billing.InvoiceStatusDetails) { + m.status_details_cache = &bsd +} + +// StatusDetailsCache returns the value of the "status_details_cache" field in the mutation. +func (m *BillingInvoiceMutation) StatusDetailsCache() (r billing.InvoiceStatusDetails, exists bool) { + v := m.status_details_cache + if v == nil { + return + } + return *v, true +} + +// OldStatusDetailsCache returns the old "status_details_cache" 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) OldStatusDetailsCache(ctx context.Context) (v billing.InvoiceStatusDetails, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldStatusDetailsCache is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldStatusDetailsCache requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldStatusDetailsCache: %w", err) + } + return oldValue.StatusDetailsCache, nil +} + +// ClearStatusDetailsCache clears the value of the "status_details_cache" field. +func (m *BillingInvoiceMutation) ClearStatusDetailsCache() { + m.status_details_cache = nil + m.clearedFields[billinginvoice.FieldStatusDetailsCache] = struct{}{} +} + +// StatusDetailsCacheCleared returns if the "status_details_cache" field was cleared in this mutation. +func (m *BillingInvoiceMutation) StatusDetailsCacheCleared() bool { + _, ok := m.clearedFields[billinginvoice.FieldStatusDetailsCache] + return ok +} + +// ResetStatusDetailsCache resets all changes to the "status_details_cache" field. +func (m *BillingInvoiceMutation) ResetStatusDetailsCache() { + m.status_details_cache = nil + delete(m.clearedFields, billinginvoice.FieldStatusDetailsCache) +} + // SetWorkflowConfigID sets the "workflow_config_id" field. func (m *BillingInvoiceMutation) SetWorkflowConfigID(s string) { m.billing_workflow_config = &s @@ -9141,7 +9191,7 @@ func (m *BillingInvoiceMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *BillingInvoiceMutation) Fields() []string { - fields := make([]string, 0, 52) + fields := make([]string, 0, 53) if m.namespace != nil { fields = append(fields, billinginvoice.FieldNamespace) } @@ -9268,6 +9318,9 @@ func (m *BillingInvoiceMutation) Fields() []string { if m.status != nil { fields = append(fields, billinginvoice.FieldStatus) } + if m.status_details_cache != nil { + fields = append(fields, billinginvoice.FieldStatusDetailsCache) + } if m.billing_workflow_config != nil { fields = append(fields, billinginvoice.FieldWorkflowConfigID) } @@ -9390,6 +9443,8 @@ func (m *BillingInvoiceMutation) Field(name string) (ent.Value, bool) { return m.DueAt() case billinginvoice.FieldStatus: return m.Status() + case billinginvoice.FieldStatusDetailsCache: + return m.StatusDetailsCache() case billinginvoice.FieldWorkflowConfigID: return m.WorkflowConfigID() case billinginvoice.FieldTaxAppID: @@ -9503,6 +9558,8 @@ func (m *BillingInvoiceMutation) OldField(ctx context.Context, name string) (ent return m.OldDueAt(ctx) case billinginvoice.FieldStatus: return m.OldStatus(ctx) + case billinginvoice.FieldStatusDetailsCache: + return m.OldStatusDetailsCache(ctx) case billinginvoice.FieldWorkflowConfigID: return m.OldWorkflowConfigID(ctx) case billinginvoice.FieldTaxAppID: @@ -9826,6 +9883,13 @@ func (m *BillingInvoiceMutation) SetField(name string, value ent.Value) error { } m.SetStatus(v) return nil + case billinginvoice.FieldStatusDetailsCache: + v, ok := value.(billing.InvoiceStatusDetails) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetStatusDetailsCache(v) + return nil case billinginvoice.FieldWorkflowConfigID: v, ok := value.(string) if !ok { @@ -9995,6 +10059,9 @@ func (m *BillingInvoiceMutation) ClearedFields() []string { if m.FieldCleared(billinginvoice.FieldDueAt) { fields = append(fields, billinginvoice.FieldDueAt) } + if m.FieldCleared(billinginvoice.FieldStatusDetailsCache) { + fields = append(fields, billinginvoice.FieldStatusDetailsCache) + } if m.FieldCleared(billinginvoice.FieldInvoicingAppExternalID) { fields = append(fields, billinginvoice.FieldInvoicingAppExternalID) } @@ -10096,6 +10163,9 @@ func (m *BillingInvoiceMutation) ClearField(name string) error { case billinginvoice.FieldDueAt: m.ClearDueAt() return nil + case billinginvoice.FieldStatusDetailsCache: + m.ClearStatusDetailsCache() + return nil case billinginvoice.FieldInvoicingAppExternalID: m.ClearInvoicingAppExternalID() return nil @@ -10248,6 +10318,9 @@ func (m *BillingInvoiceMutation) ResetField(name string) error { case billinginvoice.FieldStatus: m.ResetStatus() return nil + case billinginvoice.FieldStatusDetailsCache: + m.ResetStatusDetailsCache() + return nil case billinginvoice.FieldWorkflowConfigID: m.ResetWorkflowConfigID() return nil diff --git a/openmeter/ent/db/runtime.go b/openmeter/ent/db/runtime.go index ff78c76f6..07eb8e25b 100644 --- a/openmeter/ent/db/runtime.go +++ b/openmeter/ent/db/runtime.go @@ -308,7 +308,7 @@ func init() { // billinginvoice.CurrencyValidator is a validator for the "currency" field. It is called by the builders before save. billinginvoice.CurrencyValidator = billinginvoiceDescCurrency.Validators[0].(func(string) error) // billinginvoiceDescCollectionAt is the schema descriptor for collection_at field. - billinginvoiceDescCollectionAt := billinginvoiceFields[25].Descriptor() + billinginvoiceDescCollectionAt := billinginvoiceFields[26].Descriptor() // billinginvoice.DefaultCollectionAt holds the default value on creation for the collection_at field. billinginvoice.DefaultCollectionAt = billinginvoiceDescCollectionAt.Default.(func() time.Time) // billinginvoiceDescID is the schema descriptor for id field. diff --git a/openmeter/ent/db/setorclear.go b/openmeter/ent/db/setorclear.go index 848b9fcfa..5b227e236 100644 --- a/openmeter/ent/db/setorclear.go +++ b/openmeter/ent/db/setorclear.go @@ -587,6 +587,20 @@ func (u *BillingInvoiceUpdateOne) SetOrClearDueAt(value *time.Time) *BillingInvo return u.SetDueAt(*value) } +func (u *BillingInvoiceUpdate) SetOrClearStatusDetailsCache(value *billing.InvoiceStatusDetails) *BillingInvoiceUpdate { + if value == nil { + return u.ClearStatusDetailsCache() + } + return u.SetStatusDetailsCache(*value) +} + +func (u *BillingInvoiceUpdateOne) SetOrClearStatusDetailsCache(value *billing.InvoiceStatusDetails) *BillingInvoiceUpdateOne { + if value == nil { + return u.ClearStatusDetailsCache() + } + return u.SetStatusDetailsCache(*value) +} + func (u *BillingInvoiceUpdate) SetOrClearInvoicingAppExternalID(value *string) *BillingInvoiceUpdate { if value == nil { return u.ClearInvoicingAppExternalID() diff --git a/openmeter/ent/schema/billing.go b/openmeter/ent/schema/billing.go index aa0795116..64830edc6 100644 --- a/openmeter/ent/schema/billing.go +++ b/openmeter/ent/schema/billing.go @@ -680,6 +680,9 @@ func (BillingInvoice) Fields() []ent.Field { field.Enum("status"). GoType(billing.InvoiceStatus("")), + field.JSON("status_details_cache", billing.InvoiceStatusDetails{}). + Optional(), + // Cloned profile settings field.String("workflow_config_id"). SchemaType(map[string]string{ @@ -741,6 +744,13 @@ func (BillingInvoice) Indexes() []ent.Index { return []ent.Index{ index.Fields("namespace", "id"), index.Fields("namespace", "customer_id"), + index.Fields("namespace", "status"), + index.Fields("status_details_cache"). + Annotations( + entsql.IndexTypes(map[string]string{ + dialect.Postgres: "GIN", + }), + ), } } diff --git a/test/billing/invoice_test.go b/test/billing/invoice_test.go index fa6330491..0d5b7f8f1 100644 --- a/test/billing/invoice_test.go +++ b/test/billing/invoice_test.go @@ -270,11 +270,6 @@ func (s *InvoicingTestSuite) TestPendingLineCreation() { Number: "GATHER-TECU-USD-1", Currency: currencyx.Code(currency.USD), Status: billing.InvoiceStatusGathering, - StatusDetails: billing.InvoiceStatusDetails{ - AvailableActions: billing.InvoiceAvailableActions{ - Invoice: &billing.InvoiceAvailableActionInvoiceDetails{}, - }, - }, CreatedAt: usdInvoice.CreatedAt, UpdatedAt: usdInvoice.UpdatedAt, @@ -307,7 +302,6 @@ func (s *InvoicingTestSuite) TestPendingLineCreation() { ExpandedFields: billing.InvoiceExpandAll, } - _ = billingservice.UpdateInvoiceCollectionAt(&expectedInvoice, billingProfile.WorkflowConfig.Collection) require.Equal(s.T(), @@ -3166,7 +3160,7 @@ func (s *InvoicingTestSuite) TestGatheringInvoiceRecalculation() { Customers: []string{customerEntity.ID}, ExtendedStatuses: []billing.InvoiceStatus{billing.InvoiceStatusGathering}, Expand: billing.InvoiceExpand{ - GatheringTotals: true, + RecalculateGatheringInvoice: true, }, }) @@ -3186,7 +3180,7 @@ func (s *InvoicingTestSuite) TestGatheringInvoiceRecalculation() { Customers: []string{customerEntity.ID}, ExtendedStatuses: []billing.InvoiceStatus{billing.InvoiceStatusGathering}, Expand: billing.InvoiceExpand{ - GatheringTotals: true, + RecalculateGatheringInvoice: true, }, }) @@ -3206,7 +3200,7 @@ func (s *InvoicingTestSuite) TestGatheringInvoiceRecalculation() { Customers: []string{customerEntity.ID}, ExtendedStatuses: []billing.InvoiceStatus{billing.InvoiceStatusGathering}, Expand: billing.InvoiceExpand{ - GatheringTotals: true, + RecalculateGatheringInvoice: true, }, }) diff --git a/tools/migrate/migrations/20250207101313_billing-store-status-details.down.sql b/tools/migrate/migrations/20250207101313_billing-store-status-details.down.sql new file mode 100644 index 000000000..389c3c820 --- /dev/null +++ b/tools/migrate/migrations/20250207101313_billing-store-status-details.down.sql @@ -0,0 +1,6 @@ +-- reverse: create index "billinginvoice_status_details_cache" to table: "billing_invoices" +DROP INDEX "billinginvoice_status_details_cache"; +-- reverse: create index "billinginvoice_namespace_status" to table: "billing_invoices" +DROP INDEX "billinginvoice_namespace_status"; +-- reverse: modify "billing_invoices" table +ALTER TABLE "billing_invoices" DROP COLUMN "status_details_cache"; diff --git a/tools/migrate/migrations/20250207101313_billing-store-status-details.up.sql b/tools/migrate/migrations/20250207101313_billing-store-status-details.up.sql new file mode 100644 index 000000000..3f74e025c --- /dev/null +++ b/tools/migrate/migrations/20250207101313_billing-store-status-details.up.sql @@ -0,0 +1,6 @@ +-- modify "billing_invoices" table +ALTER TABLE "billing_invoices" ADD COLUMN "status_details_cache" jsonb NULL; +-- create index "billinginvoice_namespace_status" to table: "billing_invoices" +CREATE INDEX "billinginvoice_namespace_status" ON "billing_invoices" ("namespace", "status"); +-- create index "billinginvoice_status_details_cache" to table: "billing_invoices" +CREATE INDEX "billinginvoice_status_details_cache" ON "billing_invoices" USING gin ("status_details_cache"); diff --git a/tools/migrate/migrations/atlas.sum b/tools/migrate/migrations/atlas.sum index aa82055a4..a4debeba3 100644 --- a/tools/migrate/migrations/atlas.sum +++ b/tools/migrate/migrations/atlas.sum @@ -1,4 +1,4 @@ -h1:8rJdFojK//kweKvPargk+IChAVkaQmma1YkY6A0niz4= +h1:9opi3NtAFYz4gmDEF3LHSs0XjdwyDyKvVU6c7grmVUM= 20240826120919_init.down.sql h1:AIbgwwngjkJEYa3yRZsIXQyBa2+qoZttwMXHxXEbHLI= 20240826120919_init.up.sql h1:/hYHWF3Z3dab8SMKnw99ixVktCuJe2bAw5wstCZIEN8= 20240903155435_entitlement-expired-index.down.sql h1:np2xgYs3KQ2z7qPBcobtGNhqWQ3V8NwEP9E5U3TmpSA= @@ -111,3 +111,5 @@ h1:8rJdFojK//kweKvPargk+IChAVkaQmma1YkY6A0niz4= 20250204184648_subscription-alignment.up.sql h1:TUwMEQvaor3AMo8+c975ma1EUfrY9F8rk7nKUF8ERJE= 20250204185046_subscription-billing-override.down.sql h1:qbYSzSca4DqJhYss01lpF7Mx1ZqKrxV2IHzXEcs7iHU= 20250204185046_subscription-billing-override.up.sql h1:CzwzD60z4zEYrz8GRX0GrTlecNDU24GZed4K809nFAA= +20250207101313_billing-store-status-details.down.sql h1:jSp7q6ln3oryyGWuAarjuTkH72zSUErZj6K3i15KHrE= +20250207101313_billing-store-status-details.up.sql h1:lWyM8nZex9vZJmpea89MH4649GcDXUn2Dk1hD7Js7m8=