Skip to content

Commit

Permalink
feat: implement invoice lifecycle
Browse files Browse the repository at this point in the history
  • Loading branch information
turip committed Oct 30, 2024
1 parent eed5fcd commit b46f243
Show file tree
Hide file tree
Showing 39 changed files with 2,513 additions and 1,123 deletions.
1,533 changes: 805 additions & 728 deletions api/api.gen.go

Large diffs are not rendered by default.

235 changes: 159 additions & 76 deletions api/openapi.yaml

Large diffs are not rendered by default.

43 changes: 32 additions & 11 deletions api/spec/src/billing/invoices.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ interface CustomerInvoicesEndpoints {
* When creating an invoice, the pending line items will be marked as invoiced and the invoice will be created with the total amount of the pending items.
*
* New pending line items will be created for the period between now() and the next billing cycle's begining date for any metered item.
*
* The call can return multiple invoices if the pending line items are in different currencies.
*/
@post
@summary("Create an invoice")
Expand All @@ -55,7 +57,7 @@ interface CustomerInvoicesEndpoints {
request: InvoiceCreateInput,
): {
@statusCode _: 201;
@body body: Invoices.Invoice;
@body body: Invoices.Invoice[];
} | OpenMeter.CommonErrors;

/**
Expand Down Expand Up @@ -207,22 +209,37 @@ interface CustomerInvoiceEndpoints {
} | OpenMeter.NotFoundError | OpenMeter.CommonErrors;

/**
* Retry advancing the invoice's state to the next status.
* Advance the invoice's state to the next status.
*
* The call doesn't "approve the invoice", it only advances the invoice to the next status if the transition would be automatic.
*
* This call can be used to retry advancing the invoice's state to the next status if the previous attempt failed.
*
* This call is valid in the following invoice statuses:
* - `draft_sync_failed`
* - `issuing_failed`
* - `validation_failed`
* The action can be called when the invoice's statusDetails' actions field contain the "advance" action.
*/
@post
@route("/workflow/advance")
@route("/advance")
@summary("Advance the invoice's state to the next status")
@operationId("billingInvoiceWorkflowAdvance")
advanceWorkflow(
@operationId("billingInvoiceAdvance")
advance(
@path
customerId: ULID,

@path
invoiceId: ULID,
): {
@statusCode _: 200;
@body body: Invoices.Invoice;
} | OpenMeter.NotFoundError | OpenMeter.CommonErrors;

/**
* Retry advancing the invoice after a failed attempt.
*
* The action can be called when the invoice's statusDetails' actions field contain the "retry" action.
*/
@post
@route("/retry")
@summary("Retry advancing the invoice after a failed attempt.")
@operationId("billingInvoiceRetry")
retry(
@path
customerId: ULID,

Expand Down Expand Up @@ -376,6 +393,10 @@ model InvoiceListParams {
@summary("Filter by the invoice status")
statuses?: Array<Invoices.InvoiceStatus>;

@query
@summary("Filter by invoice extended statuses")
extendedStatuses?: Array<Invoices.InvoiceExtendedStatus>;

@query
@summary("Filter by invoice creation time")
issuedAfter?: DateTime;
Expand Down
107 changes: 93 additions & 14 deletions api/spec/src/billing/invoices/invoice.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ model Invoice {
@summary("The status of the invoice.")
status: InvoiceStatus;

@visibility("read", "query")
@summary("The details of the current invoice status")
statusDetails: InvoiceStatusDetails;

/**
* The time the invoice was issued.
*
Expand All @@ -106,6 +110,17 @@ model Invoice {
@visibility("read", "query")
issuedAt?: DateTime;

/**
* The time until the invoice is in draft status.
*
* On draft invoice creation it is calculated from the workflow settings.
*
* If manual approval is required, the draftUntil time is set.
*/
@summary("The time until the invoice is in draft status.")
@visibility("read", "query", "update")
draftUntil?: DateTime;

@summary("Due time of the fulfillment of the invoice.")
@visibility("read", "query")
dueAt?: DateTime;
Expand Down Expand Up @@ -148,6 +163,39 @@ model Invoice {
payment?: Payment;
}

@friendlyName("BillingInvoiceAction")
enum InvoiceAction {
@summary("Advance the invoice to the next status.")
advance: "advance",

@summary("Approve an invoice that requires manual approval.")
approve: "approve",

@summary("Delete the invoice (only non-issued invoices can be deleted).")
delete: "delete",

@summary("Retry an invoice issuing step that failed.")
retry: "retry",

@summary("Void an already issued invoice.")
`void`: "void",
}

@friendlyName("BillingInvoiceStatusDetails")
model InvoiceStatusDetails {
@summary("Is the invoice editable?")
immutable: boolean;

@summary("Is the invoice in a failed state?")
failed: boolean;

@summary("Extended status information for the invoice.")
extendedStatus: InvoiceExtendedStatus;

@summary("The actions that can be performed on the invoice.")
availableActions: InvoiceAction[];
}

@friendlyName("BillingInvoiceWorkflowSettings")
model InvoiceWorkflowSettings {
@summary("The apps that will be used to orchestrate the invoice's workflow.")
Expand Down Expand Up @@ -181,29 +229,60 @@ enum InvoiceStatus {
gathering: "gathering",

/**
* The invoice is waiting for review by our partner.
* The invoice is in draft status.
*/
review: "review",
draft: "draft",

/**
* The invoice has been issued to the customer.
* The invoice is in the process of being issued.
*/
issued: "issued",
issuing: "issuing",

/**
* The invoice has been paid by the customer.
* The invoice has been issued to the customer.
*/
paymentReceived: "payment_received",
issued: "issued",
}

/**
* A manual approval is required before the invoice can be issued.
*/
manualApprovalRequired: "manual_approval_required",
/**
* InvoiceExtendedStatus describes the extended status of an invoice.
*
* This is used to provide more detailed information about the status of the invoice. Useful for
* troubelshooting invoice workflow issues.
*/
@friendlyName("BillingInvoiceExtendedStatus")
enum InvoiceExtendedStatus {
...InvoiceStatus,

/**
* There's a validation issue with the invoice.
*/
validationFailed: "validation_failed",
@summary("The draft is available for processing.")
draftCreated: "draft_created",

@summary("The draft is waiting for manual approval.")
draftManualApprovalNeeded: "draft_manual_approval_needed",

@summary("The draft is being validated.")
draftValidating: "draft_validating",

@summary("The draft is invalid, needs fixes to apps or contents.")
draftInvalid: "draft_invalid",

@summary("The draft is syncing with external systems.")
draftSyncing: "draft_syncing",

@summary("The draft failed to sync with external systems.")
draftSyncFailed: "draft_sync_failed",

@summary("The draft is waiting for auto-approval.")
draftWaitingAutoApproval: "draft_waiting_auto_approval",

@summary("The draft is ready to be issued.")
draftReadyToIssue: "draft_ready_to_issue",

@summary("The draft is being issued.")
issuingSyncing: "issuing_syncing",

@summary("The draft failed to issue.")
issuingSyncFailed: "issuing_sync_failed",
}

/*
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ require (
github.com/prometheus/client_golang v1.20.4
github.com/prometheus/client_model v0.6.1
github.com/prometheus/common v0.60.0
github.com/qmuntal/stateless v1.7.1
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475
github.com/redis/go-redis/extra/redisotel/v9 v9.5.3
github.com/redis/go-redis/v9 v9.7.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1161,6 +1161,8 @@ github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0 h1:sadMIs
github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0/go.mod h1:jgxiZysxFPM+iWKwQwPR+y+Jvo54ARd4EisXxKYpB5c=
github.com/pusher/pusher-http-go v4.0.1+incompatible h1:4u6tomPG1WhHaST7Wi9mw83Y+MS/j2EplR2YmDh8Xp4=
github.com/pusher/pusher-http-go v4.0.1+incompatible/go.mod h1:XAv1fxRmVTI++2xsfofDhg7whapsLRG/gH/DXbF3a18=
github.com/qmuntal/stateless v1.7.1 h1:dI+BtLHq/nD6u46POkOINTDjY9uE33/4auEzfX3TWp0=
github.com/qmuntal/stateless v1.7.1/go.mod h1:n1HjRBM/cq4uCr3rfUjaMkgeGcd+ykAZwkjLje6jGBM=
github.com/quipo/dependencysolver v0.0.0-20170801134659-2b009cb4ddcc h1:hK577yxEJ2f5s8w2iy2KimZmgrdAUZUNftE1ESmg2/Q=
github.com/quipo/dependencysolver v0.0.0-20170801134659-2b009cb4ddcc/go.mod h1:OQt6Zo5B3Zs+C49xul8kcHo+fZ1mCLPvd0LFxiZ2DHc=
github.com/r3labs/diff/v3 v3.0.1 h1:CBKqf3XmNRHXKmdU7mZP1w7TV0pDyVCis1AUHtA4Xtg=
Expand Down
14 changes: 1 addition & 13 deletions openmeter/billing/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,6 @@ type CustomerOverrideAdapter interface {
UpdateCustomerOverride(ctx context.Context, input UpdateCustomerOverrideAdapterInput) (*billingentity.CustomerOverride, error)
DeleteCustomerOverride(ctx context.Context, input DeleteCustomerOverrideInput) error

// UpsertCustomerOverrideIgnoringTrns upserts a customer override ignoring the transactional context, the override
// will be empty.
//
// This allows us to provision a customer override outside of a transactional context, and rely on LockCustomerForUpdate
// to coordinate ongoing transactions for a single customer.
//
// This approach is used for gathering invoices where we can ensure that no more than one gathering invoice
// is created per customer, per currency.
//
// For invoice specific changes the LockInvoicesForUpdate should be used, which will be more performant.
UpsertCustomerOverrideIgnoringTrns(ctx context.Context, input UpsertCustomerOverrideIgnoringTrnsAdapterInput) error
LockCustomerForUpdate(ctx context.Context, input LockCustomerForUpdateAdapterInput) error

// UpsertCustomerOverride upserts a customer override ignoring the transactional context, the override
// will be empty.
UpsertCustomerOverride(ctx context.Context, input UpsertCustomerOverrideAdapterInput) error
Expand All @@ -69,4 +56,5 @@ type InvoiceAdapter interface {
DeleteInvoices(ctx context.Context, input DeleteInvoicesAdapterInput) error
ListInvoices(ctx context.Context, input ListInvoicesInput) (ListInvoicesResponse, error)
AssociatedLineCounts(ctx context.Context, input AssociatedLineCountsAdapterInput) (AssociatedLineCountsAdapterResponse, error)
UpdateInvoice(ctx context.Context, input UpdateInvoiceAdapterInput) error
}
16 changes: 6 additions & 10 deletions openmeter/billing/adapter/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,16 @@ func New(config Config) (billing.Adapter, error) {
}

return &adapter{
db: config.Client,
dbWithoutTrns: config.Client,
logger: config.Logger,
db: config.Client,
logger: config.Logger,
}, nil
}

var _ billing.Adapter = (*adapter)(nil)

type adapter struct {
db *entdb.Client
// dbWithoutTrns is used to execute any upsert operations outside of ctx driven transactions
dbWithoutTrns *entdb.Client
logger *slog.Logger
db *entdb.Client
logger *slog.Logger
}

func (a adapter) Tx(ctx context.Context) (context.Context, transaction.Driver, error) {
Expand All @@ -66,8 +63,7 @@ func (a adapter) WithTx(ctx context.Context, tx *entutils.TxDriver) billing.Adap
txDb := db.NewTxClientFromRawConfig(ctx, *tx.GetConfig())

return &adapter{
db: txDb.Client(),
dbWithoutTrns: a.dbWithoutTrns,
logger: a.logger,
db: txDb.Client(),
logger: a.logger,
}
}
Loading

0 comments on commit b46f243

Please sign in to comment.