Skip to content

Commit

Permalink
feat: billing profiles (#1703)
Browse files Browse the repository at this point in the history
This patch adds support for billing profiles as the first 
implemented API.

For testing purposes, a new OpenMeter Sandbox is added. The sandbox 
app gets installed by default if apps are enabled for easier testing.

The change also makes sure that the customeroverrides codebase 
works as expected, however we are going to implement the APIs at a 
later point.
  • Loading branch information
turip authored Oct 23, 2024
1 parent 1f15210 commit 19c721b
Show file tree
Hide file tree
Showing 104 changed files with 7,822 additions and 6,552 deletions.
1,516 changes: 823 additions & 693 deletions api/api.gen.go

Large diffs are not rendered by default.

360 changes: 239 additions & 121 deletions api/openapi.yaml

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions api/spec/src/app/app.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ model ListAppsRequest extends PaginatedQuery {}
@friendlyName("AppType")
enum Type {
Stripe: "stripe",
Sandbox: "sandbox",
}

/**
Expand All @@ -71,6 +72,7 @@ enum Type {
@discriminator("type")
union App {
stripe: StripeApp,
sandbox: SandboxApp,
}

/**
Expand Down
1 change: 1 addition & 0 deletions api/spec/src/app/main.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ import "./app.tsp";
import "./capability.tsp";
import "./marketplace.tsp";
import "./stripe.tsp";
import "./sandbox.tsp";

namespace OpenMeter.App;
11 changes: 11 additions & 0 deletions api/spec/src/app/sandbox.tsp
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace OpenMeter.App;

@friendlyName("SandboxApp")
model SandboxApp {
...AppBase;

/**
* The app's type is Sandbox.
*/
type: Type.Sandbox;
}
3 changes: 0 additions & 3 deletions api/spec/src/billing/invoices/tax.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,6 @@ scalar TaxIdentificationCode extends string;
@friendlyName("BillingTaxIdentity")
@summary("Identity stores the details required to identify an entity for tax purposes in a specific country.")
model TaxIdentity {
@summary("Tax country code for Where the tax identity was issued.")
country: CountryCode;

@summary("Normalized tax code shown on the original identity document.")
code?: TaxIdentificationCode;
}
132 changes: 79 additions & 53 deletions api/spec/src/billing/profile.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -78,25 +78,55 @@ interface Profiles {
enum ProfileOrderBy {
createdAt: "createdAt",
updatedAt: "updatedAt",
default: "default",
name: "name",
}

/**
* Profile represents a billing profile
*/
@friendlyName("BillingProfile")
model Profile {
...global.Resource;
...OmitProperties<global.Resource, "updatedAt">;

/**
* When the resource was last updated.
*
* For updates this field must be set to the last update time to detect conflicts.
*/
@summary("Last update time of the resource")
@visibility("read", "query", "update")
updatedAt: DateTime;

@summary("The name and contact information for the supplier this billing profile represents")
supplier: Invoices.Party;

@summary("The billing workflow settings for this profile")
workflow: Workflow;

@summary("The applications used by this billing profile")
@visibility("read", "query")
apps: ProfileApps;

@summary("Is this the default profile?")
default: boolean;
}

/**
* ProfileApps represents the applications used by a billing profile
*/
@friendlyName("BillingProfileApps")
model ProfileApps {
@summary("The tax app used for this workflow")
tax: OpenMeter.App.App;

@summary("The invoicing app used for this workflow")
invoicing: OpenMeter.App.App;

@summary("The payment app used for this workflow")
payment: OpenMeter.App.App;
}

/**
* Workflow represents a billing workflow
*/
Expand All @@ -116,67 +146,78 @@ model Workflow {

@friendlyName("BillingWorkflowSettings")
model WorkflowSettings {
@summary("The collection settings for this workflow")
collection?: WorkflowCollectionSettings;

@summary("The invoicing settings for this workflow")
invoicing?: WorkflowInvoicingSettings;

@summary("The payment settings for this workflow")
payment?: WorkflowPaymentSettings;
}

@summary("Workflow collection specifies how to collect the pending items for an invoice")
@friendlyName("BillingWorkflowCollectionSettings")
model WorkflowCollectionSettings {
/**
* When to collect the pending line items into an invoice.
*/
collectionAlignment?: CollectionAlignment = CollectionAlignment.subscription;
alignment?: CollectionAlignment = CollectionAlignment.subscription;

/**
* The period for collecting the pending line items into an invoice.
* The interval for collecting the pending line items into an invoice.
*/
@encode(DurationKnownEncoding.ISO8601)
@example("P1D")
itemCollectionPeriod?: string = "PT1H";

invoice?: WorkflowInvoice;

@summary("The tax app used for this workflow")
taxApp: OpenMeter.App.App;

@summary("The invoicing app used for this workflow")
invoicingApp: OpenMeter.App.App;

@summary("The payment app used for this workflow")
paymentApp: OpenMeter.App.App;
interval?: string = "PT1H";
}

/**
* AppReference can be used to reference an app during creation only.
* WorkflowPaymentSettings represents the payment settings for a billing workflow
*/
@friendlyName("BillingWorkflowAppReference")
model AppReference {
@summary("The ID of the app, if not specified the type is used to find the default app")
id?: ULID;
@summary("Workflow payment settings")
@friendlyName("BillingWorkflowPaymentSettings")
model WorkflowPaymentSettings {
collectionMethod?: CollectionMethod = CollectionMethod.chargeAutomatically;
}

@summary("The type of the app, if specified the default app is used")
type?: OpenMeter.App.Type;
@summary("App reference type specifies the type of reference inside an app reference")
@friendlyName("BillingWorkflowAppReferenceType")
enum AppReferenceType {
appId: "app_id",
appType: "app_type",
}

/**
* AppIdOrType can be used to reference an app during creation only.
*
* This can be either an AppType or the ULID of an app.
*/
@friendlyName("BillingWorkflowAppIdOrType")
scalar AppIdOrType extends string;

/**
* ProfileCreateInput represents the input for creating a billing profile
*/
@friendlyName("BillingProfileCreateInput")
model ProfileCreateInput {
...OmitProperties<Profile, "workflow">;
workflow: WorkflowCreateInput;
...OmitProperties<Profile, "apps">;
apps: ProfileCreateAppsInput;
}

/**
* WorkflowCreate represents the workflow settings for creation of a billing profile
* ProfileCreateAppsInput represents the input for creating a billing profile's apps
*/
@friendlyName("BillingWorkflowCreateInput")
model WorkflowCreateInput {
...OmitProperties<Workflow, "taxApp" | "invoicingApp" | "paymentApp">;

// provider settings for creation (update is not supported)
@visibility("create")
taxApp?: AppReference;
@friendlyName("BillingProfileCreateAppsInput")
model ProfileCreateAppsInput {
@summary("The tax app used for this workflow")
tax: AppIdOrType;

@visibility("create")
invoicingApp?: AppReference;
@summary("The invoicing app used for this workflow")
invoicing: AppIdOrType;

@visibility("create")
paymentApp?: AppReference;
@summary("The payment app used for this workflow")
payment: AppIdOrType;
}

/**
Expand All @@ -196,8 +237,8 @@ enum CollectionAlignment {
* WorkflowInvoice represents the invoice settings for a billing workflow
*/
@summary("Workflow invoice settings")
@friendlyName("BillingWorkflowInvoice")
model WorkflowInvoice {
@friendlyName("BillingWorkflowInvoicingSettings")
model WorkflowInvoicingSettings {
/**
* Whether to automatically issue the invoice after the draftPeriod has passed.
*/
Expand All @@ -216,21 +257,6 @@ model WorkflowInvoice {
@encode(DurationKnownEncoding.ISO8601)
@example("P1D")
dueAfter?: string = "P7D";

/**
* The method to collect the invoice.
*/
collectionMethod?: CollectionMethod = CollectionMethod.chargeAutomatically;

/**
* The resolution of the line items in the invoice.
*/
itemResolution?: ItemResolution = ItemResolution.period;

/**
* Whether to create one line item per subject in the invoice.
*/
itemPerSubject?: boolean = true;
}

/**
Expand Down
8 changes: 0 additions & 8 deletions api/spec/src/types.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -283,13 +283,6 @@ model Annotations {
/**
* Numeric represents an arbitrary precision number.
*/
@extension("x-go-type", "alpacadecimal.Decimal")
@extension(
"x-go-type-import",
{
path: "github.com/alpacahq/alpacadecimal",
}
)
@pattern("^\\-?[0-9]+(\\.[0-9]+)?$")
@friendlyName("Numeric")
scalar Numeric extends string;
Expand All @@ -300,7 +293,6 @@ alias Money = Numeric;
*/
@friendlyName("Percentage")
@pattern("^\\-?[0-9]+(\\.[0-9]+)?%$")
// TODO: let's add some default go implementation
scalar Percentage extends Numeric;

/**
Expand Down
15 changes: 15 additions & 0 deletions app/config/apps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package config

import "github.com/spf13/viper"

type AppsConfiguration struct {
Enabled bool
}

func (c AppsConfiguration) Validate() error {
return nil
}

func ConfigureApps(v *viper.Viper) {
v.SetDefault("apps.enabled", false)
}
15 changes: 15 additions & 0 deletions app/config/billing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package config

import "github.com/spf13/viper"

type BillingConfiguration struct {
Enabled bool
}

func (c BillingConfiguration) Validate() error {
return nil
}

func ConfigureBilling(v *viper.Viper) {
v.SetDefault("billing.enabled", false)
}
8 changes: 8 additions & 0 deletions app/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ type Configuration struct {
Sink SinkConfiguration
BalanceWorker BalanceWorkerConfiguration
Notification NotificationConfiguration
Billing BillingConfiguration
Apps AppsConfiguration
StripeApp StripeAppConfig
Svix SvixConfig
}
Expand Down Expand Up @@ -112,6 +114,10 @@ func (c Configuration) Validate() error {
errs = append(errs, errorsx.WithPrefix(err, "stripe app"))
}

if err := c.Apps.Validate(); err != nil {
errs = append(errs, errorsx.WithPrefix(err, "apps"))
}

return errors.Join(errs...)
}

Expand Down Expand Up @@ -148,4 +154,6 @@ func SetViperDefaults(v *viper.Viper, flags *pflag.FlagSet) {
ConfigureBalanceWorker(v)
ConfigureNotification(v)
ConfigureStripe(v)
ConfigureBilling(v)
ConfigureApps(v)
}
Loading

0 comments on commit 19c721b

Please sign in to comment.