Skip to content

Commit

Permalink
feat: implement invoice number generation (#2142)
Browse files Browse the repository at this point in the history
  • Loading branch information
turip authored Jan 23, 2025
1 parent ee1c2ab commit a1994ac
Show file tree
Hide file tree
Showing 54 changed files with 4,329 additions and 1,487 deletions.
1,074 changes: 538 additions & 536 deletions api/api.gen.go

Large diffs are not rendered by default.

1,540 changes: 771 additions & 769 deletions api/client/go/client.gen.go

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions api/client/node/schemas/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3841,12 +3841,13 @@ export interface components {
customer: components['schemas']['BillingParty']
/** @description Number specifies the human readable key used to reference this Invoice.
*
* The number only gets populated after the invoice had been issued.
* The invoice number can change in the draft phases, as we are allocating temporary draft
* invoice numbers, but it's final as soon as the invoice gets finalized (issued state).
*
* Please note that the number is (depending on the upstream settings) either unique for the
* whole organization or unique for the customer, or in multi (stripe) account setups unique for the
* account. */
readonly number?: components['schemas']['InvoiceNumber']
readonly number: components['schemas']['InvoiceNumber']
/** @description Currency for all invoice line items.
*
* Multi currency invoices are not supported yet. */
Expand Down
5 changes: 3 additions & 2 deletions api/client/web/src/client/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3841,12 +3841,13 @@ export interface components {
customer: components['schemas']['BillingParty']
/** @description Number specifies the human readable key used to reference this Invoice.
*
* The number only gets populated after the invoice had been issued.
* The invoice number can change in the draft phases, as we are allocating temporary draft
* invoice numbers, but it's final as soon as the invoice gets finalized (issued state).
*
* Please note that the number is (depending on the upstream settings) either unique for the
* whole organization or unique for the customer, or in multi (stripe) account setups unique for the
* account. */
readonly number?: components['schemas']['InvoiceNumber']
readonly number: components['schemas']['InvoiceNumber']
/** @description Currency for all invoice line items.
*
* Multi currency invoices are not supported yet. */
Expand Down
4 changes: 3 additions & 1 deletion api/openapi.cloud.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10778,6 +10778,7 @@ components:
- type
- supplier
- customer
- number
- currency
- totals
- status
Expand Down Expand Up @@ -10850,7 +10851,8 @@ components:
description: |-
Number specifies the human readable key used to reference this Invoice.

The number only gets populated after the invoice had been issued.
The invoice number can change in the draft phases, as we are allocating temporary draft
invoice numbers, but it's final as soon as the invoice gets finalized (issued state).

Please note that the number is (depending on the upstream settings) either unique for the
whole organization or unique for the customer, or in multi (stripe) account setups unique for the
Expand Down
4 changes: 3 additions & 1 deletion api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10544,6 +10544,7 @@ components:
- type
- supplier
- customer
- number
- currency
- totals
- status
Expand Down Expand Up @@ -10616,7 +10617,8 @@ components:
description: |-
Number specifies the human readable key used to reference this Invoice.

The number only gets populated after the invoice had been issued.
The invoice number can change in the draft phases, as we are allocating temporary draft
invoice numbers, but it's final as soon as the invoice gets finalized (issued state).

Please note that the number is (depending on the upstream settings) either unique for the
whole organization or unique for the customer, or in multi (stripe) account setups unique for the
Expand Down
5 changes: 3 additions & 2 deletions api/spec/src/billing/invoices/invoice.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,15 @@ model Invoice {
/**
* Number specifies the human readable key used to reference this Invoice.
*
* The number only gets populated after the invoice had been issued.
* The invoice number can change in the draft phases, as we are allocating temporary draft
* invoice numbers, but it's final as soon as the invoice gets finalized (issued state).
*
* Please note that the number is (depending on the upstream settings) either unique for the
* whole organization or unique for the customer, or in multi (stripe) account setups unique for the
* account.
*/
@visibility(Lifecycle.Read)
number?: InvoiceNumber;
number: InvoiceNumber;

/**
* Currency for all invoice line items.
Expand Down
6 changes: 4 additions & 2 deletions app/common/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
appstripe "github.com/openmeterio/openmeter/openmeter/app/stripe"
appstripeadapter "github.com/openmeterio/openmeter/openmeter/app/stripe/adapter"
appstripeservice "github.com/openmeterio/openmeter/openmeter/app/stripe/service"
"github.com/openmeterio/openmeter/openmeter/billing"
"github.com/openmeterio/openmeter/openmeter/customer"
entdb "github.com/openmeterio/openmeter/openmeter/ent/db"
"github.com/openmeterio/openmeter/openmeter/namespace"
Expand Down Expand Up @@ -71,13 +72,14 @@ func NewAppStripeService(logger *slog.Logger, db *entdb.Client, appsConfig confi
})
}

func NewAppSandboxProvisioner(ctx context.Context, logger *slog.Logger, appsConfig config.AppsConfiguration, appService app.Service, namespaceManager *namespace.Manager) (AppSandboxProvisioner, error) {
func NewAppSandboxProvisioner(ctx context.Context, logger *slog.Logger, appsConfig config.AppsConfiguration, appService app.Service, namespaceManager *namespace.Manager, billingService billing.Service) (AppSandboxProvisioner, error) {
if !appsConfig.Enabled {
return nil, nil
}

_, err := appsandbox.NewFactory(appsandbox.Config{
AppService: appService,
AppService: appService,
BillingService: billingService,
})
if err != nil {
return nil, fmt.Errorf("failed to initialize app sandbox factory: %w", err)
Expand Down
2 changes: 1 addition & 1 deletion cmd/billing-worker/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions cmd/jobs/billing/advance/advance.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,6 @@ func NewAutoAdvancer(ctx context.Context, conf appconfig.Configuration, logger *
return nil, fmt.Errorf("failed to initialize namespace manager: %w", err)
}

_, err = common.NewAppSandboxProvisioner(ctx, logger, conf.Apps, appService, namespaceManager)
if err != nil {
return nil, fmt.Errorf("failed to initialize sandbox app provisioner: %w", err)
}

billingAdapter, err := billingadapter.New(billingadapter.Config{
Client: entPostgresDriver.Client(),
Logger: logger,
Expand All @@ -158,6 +153,11 @@ func NewAutoAdvancer(ctx context.Context, conf appconfig.Configuration, logger *
return nil, fmt.Errorf("failed to initialize billing service: %w", err)
}

_, err = common.NewAppSandboxProvisioner(ctx, logger, conf.Apps, appService, namespaceManager, billingService)
if err != nil {
return nil, fmt.Errorf("failed to initialize sandbox app provisioner: %w", err)
}

a, err := billingworkerautoadvance.NewAdvancer(billingworkerautoadvance.Config{
BillingService: billingService,
Logger: logger,
Expand Down
10 changes: 5 additions & 5 deletions cmd/server/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

57 changes: 36 additions & 21 deletions openmeter/app/sandbox/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import (
"context"
"fmt"

"github.com/oklog/ulid/v2"

"github.com/openmeterio/openmeter/openmeter/app"
appentity "github.com/openmeterio/openmeter/openmeter/app/entity"
appentitybase "github.com/openmeterio/openmeter/openmeter/app/entity/base"
Expand All @@ -14,18 +12,21 @@ import (
customerentity "github.com/openmeterio/openmeter/openmeter/customer/entity"
)

const (
InvoiceTSFormat = "20060102-150405"
)

var (
_ customerapp.App = (*App)(nil)
_ billing.InvoicingApp = (*App)(nil)
_ appentity.CustomerData = (*CustomerData)(nil)

InvoiceSequenceNumber = billing.SequenceDefinition{
Template: "OM-SANDBOX-{{.CustomerPrefix}}-{{.NextSequenceNumber}}",
Scope: "invoices/app/sandbox",
}
)

type App struct {
appentitybase.AppBase

billingService billing.Service
}

func (a App) ValidateCustomer(ctx context.Context, customer *customerentity.Customer, capabilities []appentitybase.CapabilityType) error {
Expand Down Expand Up @@ -53,21 +54,27 @@ func (a App) ValidateInvoice(ctx context.Context, invoice billing.Invoice) error
}

func (a App) UpsertInvoice(ctx context.Context, invoice billing.Invoice) (*billing.UpsertInvoiceResult, error) {
id, err := ulid.Parse(invoice.ID)
if err != nil {
return nil, fmt.Errorf("failed to parse invoice ID: %w", err)
}

idTime := ulid.Time(id.Time())

out := billing.NewUpsertInvoiceResult()
out.SetInvoiceNumber(fmt.Sprintf("SANDBOX-%s", idTime.Format(InvoiceTSFormat)))

return billing.NewUpsertInvoiceResult(), nil
}

func (a App) FinalizeInvoice(ctx context.Context, invoice billing.Invoice) (*billing.FinalizeInvoiceResult, error) {
return nil, nil
invoiceNumber, err := a.billingService.GenerateInvoiceSequenceNumber(
ctx,
billing.SequenceGenerationInput{
Namespace: invoice.Namespace,
CustomerName: invoice.Customer.Name,
Currency: invoice.Currency,
},
InvoiceSequenceNumber,
)
if err != nil {
return nil, fmt.Errorf("failed to generate invoice sequence number: %w", err)
}

finalizeResult := billing.NewFinalizeInvoiceResult()
finalizeResult.SetInvoiceNumber(invoiceNumber)

return finalizeResult, nil
}

func (a App) DeleteInvoice(ctx context.Context, invoice billing.Invoice) error {
Expand All @@ -81,18 +88,24 @@ func (c CustomerData) Validate() error {
}

type Factory struct {
appService app.Service
appService app.Service
billingService billing.Service
}

type Config struct {
AppService app.Service
AppService app.Service
BillingService billing.Service
}

func (c Config) Validate() error {
if c.AppService == nil {
return fmt.Errorf("app service is required")
}

if c.BillingService == nil {
return fmt.Errorf("billing service is required")
}

return nil
}

Expand All @@ -102,7 +115,8 @@ func NewFactory(config Config) (*Factory, error) {
}

fact := &Factory{
appService: config.AppService,
appService: config.AppService,
billingService: config.BillingService,
}

err := config.AppService.RegisterMarketplaceListing(appentity.RegistryItem{
Expand All @@ -119,7 +133,8 @@ func NewFactory(config Config) (*Factory, error) {
// Factory
func (a *Factory) NewApp(_ context.Context, appBase appentitybase.AppBase) (appentity.App, error) {
return App{
AppBase: appBase,
AppBase: appBase,
billingService: a.billingService,
}, nil
}

Expand Down
3 changes: 2 additions & 1 deletion openmeter/app/sandbox/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,8 @@ func NewMockableFactory(_ *testing.T, config Config) (*MockableFactory, error) {

fact := &MockableFactory{
Factory: &Factory{
appService: config.AppService,
appService: config.AppService,
billingService: config.BillingService,
},
}

Expand Down
7 changes: 7 additions & 0 deletions openmeter/billing/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package billing
import (
"context"

"github.com/alpacahq/alpacadecimal"

appentitybase "github.com/openmeterio/openmeter/openmeter/app/entity/base"
customerentity "github.com/openmeterio/openmeter/openmeter/customer/entity"
"github.com/openmeterio/openmeter/pkg/framework/entutils"
Expand All @@ -14,6 +16,7 @@ type Adapter interface {
CustomerOverrideAdapter
InvoiceLineAdapter
InvoiceAdapter
SequenceAdapter

entutils.TxCreator
}
Expand Down Expand Up @@ -61,3 +64,7 @@ type InvoiceAdapter interface {

GetInvoiceOwnership(ctx context.Context, input GetInvoiceOwnershipAdapterInput) (GetOwnershipAdapterResponse, error)
}

type SequenceAdapter interface {
NextSequenceNumber(ctx context.Context, input NextSequenceNumberInput) (alpacadecimal.Decimal, error)
}
3 changes: 2 additions & 1 deletion openmeter/billing/adapter/invoice.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ func (a *adapter) CreateInvoice(ctx context.Context, input billing.CreateInvoice
SetSourceBillingProfileID(input.Profile.ID).
SetCustomerID(input.Customer.ID).
SetType(input.Type).
SetNumber(input.Number).
SetNillableDescription(input.Description).
SetNillableDueAt(input.DueAt).
SetNillableIssuedAt(lo.EmptyableToPtr(input.IssuedAt)).
Expand Down Expand Up @@ -439,7 +440,7 @@ func (a *adapter) UpdateInvoice(ctx context.Context, in billing.UpdateInvoiceAda
// Currency is immutable
SetStatus(in.Status).
// Type is immutable
SetOrClearNumber(in.Number).
SetNumber(in.Number).
SetOrClearDescription(in.Description).
SetOrClearDueAt(in.DueAt).
SetOrClearDraftUntil(in.DraftUntil).
Expand Down
Loading

0 comments on commit a1994ac

Please sign in to comment.