Skip to content

Commit

Permalink
Add Metronome business logic
Browse files Browse the repository at this point in the history
  • Loading branch information
MauAraujo committed Apr 3, 2024
1 parent 2c63366 commit 8bb2d3e
Show file tree
Hide file tree
Showing 14 changed files with 373 additions and 131 deletions.
4 changes: 2 additions & 2 deletions api/server/handlers/billing/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func (c *CreateBillingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)

proj, _ := ctx.Value(types.ProjectScope).(*models.Project)

clientSecret, err := c.Config().BillingManager.CreatePaymentMethod(ctx, proj)
clientSecret, err := c.Config().BillingManager.StripeClient.CreatePaymentMethod(ctx, proj)
if err != nil {
err := telemetry.Error(ctx, span, err, "error creating payment method")
c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error creating payment method: %w", err)))
Expand Down Expand Up @@ -81,7 +81,7 @@ func (c *SetDefaultBillingHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
return
}

err := c.Config().BillingManager.SetDefaultPaymentMethod(ctx, paymentMethodID, proj)
err := c.Config().BillingManager.StripeClient.SetDefaultPaymentMethod(ctx, paymentMethodID, proj)
if err != nil {
err := telemetry.Error(ctx, span, err, "error setting default payment method")
c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error setting default payment method: %w", err)))
Expand Down
4 changes: 2 additions & 2 deletions api/server/handlers/billing/customer.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func (c *CreateBillingCustomerHandler) ServeHTTP(w http.ResponseWriter, r *http.
}

// Create customer in Stripe
customerID, err := c.Config().BillingManager.CreateCustomer(ctx, user.Email, proj)
customerID, err := c.Config().BillingManager.StripeClient.CreateCustomer(ctx, user.Email, proj)
if err != nil {
err := telemetry.Error(ctx, span, err, "error creating billing customer")
c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error creating billing customer: %w", err)))
Expand Down Expand Up @@ -92,7 +92,7 @@ func (c *GetPublishableKeyHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
// There is no easy way to pass environment variables to the frontend,
// so for now pass via the backend. This is acceptable because the key is
// meant to be public
publishableKey := c.Config().BillingManager.GetPublishableKey(ctx)
publishableKey := c.Config().BillingManager.StripeClient.GetPublishableKey(ctx)

telemetry.WithAttributes(span,
telemetry.AttributeKV{Key: "project-id", Value: proj.ID},
Expand Down
2 changes: 1 addition & 1 deletion api/server/handlers/billing/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func (c *DeleteBillingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
return
}

err := c.Config().BillingManager.DeletePaymentMethod(ctx, paymentMethodID)
err := c.Config().BillingManager.StripeClient.DeletePaymentMethod(ctx, paymentMethodID)
if err != nil {
err := telemetry.Error(ctx, span, err, "error deleting payment method")
c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error deleting payment method: %w", err)))
Expand Down
4 changes: 2 additions & 2 deletions api/server/handlers/billing/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func (c *ListBillingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {

proj, _ := ctx.Value(types.ProjectScope).(*models.Project)

paymentMethods, err := c.Config().BillingManager.ListPaymentMethod(ctx, proj)
paymentMethods, err := c.Config().BillingManager.StripeClient.ListPaymentMethod(ctx, proj)
if err != nil {
err := telemetry.Error(ctx, span, err, "error listing payment method")
c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error listing payment method: %w", err)))
Expand All @@ -65,7 +65,7 @@ func (c *CheckPaymentEnabledHandler) ServeHTTP(w http.ResponseWriter, r *http.Re

proj, _ := ctx.Value(types.ProjectScope).(*models.Project)

paymentEnabled, err := c.Config().BillingManager.CheckPaymentEnabled(ctx, proj)
paymentEnabled, err := c.Config().BillingManager.StripeClient.CheckPaymentEnabled(ctx, proj)
if err != nil {
err := telemetry.Error(ctx, span, err, "error checking if payment enabled")
c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error checking if payment enabled: %w", err)))
Expand Down
70 changes: 54 additions & 16 deletions api/server/handlers/project/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package project
import (
"net/http"

"github.com/google/uuid"
"github.com/porter-dev/porter/api/server/handlers"
"github.com/porter-dev/porter/api/server/shared"
"github.com/porter-dev/porter/api/server/shared/apierrors"
Expand Down Expand Up @@ -55,9 +56,34 @@ func (p *ProjectCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)

var err error

proj, _, err = CreateProjectWithUser(p.Repo().Project(), proj, user)
if err != nil {
err = telemetry.Error(ctx, span, err, "error creating project with user")
p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
return
}

step := types.StepConnectSource

if p.Config().ServerConf.EnableSandbox {
step = types.StepCleanUp
}

// create onboarding flow set to the first step. Read in env var
_, err = p.Repo().Onboarding().CreateProjectOnboarding(&models.Onboarding{
ProjectID: proj.ID,
CurrentStep: step,
})
if err != nil {
err = telemetry.Error(ctx, span, err, "error creating project onboarding")
p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
return
}

// Create Stripe Customer
if p.Config().ServerConf.StripeSecretKey != "" && p.Config().ServerConf.StripePublishableKey != "" {
// Create billing customer for project and set the billing ID
billingID, err := p.Config().BillingManager.CreateCustomer(ctx, user.Email, proj)
billingID, err := p.Config().BillingManager.StripeClient.CreateCustomer(ctx, user.Email, proj)
if err != nil {
err = telemetry.Error(ctx, span, err, "error creating billing customer")
p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
Expand All @@ -72,26 +98,38 @@ func (p *ProjectCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
)
}

proj, _, err = CreateProjectWithUser(p.Repo().Project(), proj, user)
if err != nil {
err = telemetry.Error(ctx, span, err, "error creating project with user")
p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
return
// Create Metronome Customer
if p.Config().ServerConf.MetronomeAPIKey != "" {
usageID, err := p.Config().BillingManager.MetronomeClient.CreateCustomer(user.CompanyName, proj.Name, proj.ID, proj.BillingID)
if err != nil {
err = telemetry.Error(ctx, span, err, "error creating billing customer")
p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
return
}
proj.UsageID = usageID
}

step := types.StepConnectSource

if p.Config().ServerConf.EnableSandbox {
step = types.StepCleanUp
// Add customer to starter plan
if p.Config().ServerConf.MetronomeAPIKey != "" && p.Config().ServerConf.PorterCloudPlanID != "" &&
p.Config().ServerConf.EnableSandbox {
porterCloudPlanID, err := uuid.Parse(p.Config().ServerConf.PorterCloudPlanID)
if err != nil {
err = telemetry.Error(ctx, span, err, "error parsing starter plan id")
p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
return
}
customerPlanID, err := p.Config().BillingManager.MetronomeClient.AddCustomerPlan(proj.UsageID, porterCloudPlanID)
if err != nil {
err = telemetry.Error(ctx, span, err, "error adding customer to starter plan")
p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
return
}
proj.UsagePlanID = customerPlanID
}

// create onboarding flow set to the first step. Read in env var
_, err = p.Repo().Onboarding().CreateProjectOnboarding(&models.Onboarding{
ProjectID: proj.ID,
CurrentStep: step,
})
_, err = p.Repo().Project().UpdateProject(proj)
if err != nil {
err = telemetry.Error(ctx, span, err, "error creating project onboarding")
err := telemetry.Error(ctx, span, err, "error updating project")
p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
return
}
Expand Down
28 changes: 27 additions & 1 deletion api/server/handlers/project/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"net/http"

"connectrpc.com/connect"
"github.com/google/uuid"
porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
"github.com/porter-dev/porter/api/server/handlers"
"github.com/porter-dev/porter/api/server/shared"
Expand Down Expand Up @@ -92,7 +93,32 @@ func (p *ProjectDeleteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
return
}

err = p.Config().BillingManager.DeleteCustomer(ctx, proj)
if p.Config().ServerConf.MetronomeAPIKey != "" && p.Config().ServerConf.PorterCloudPlanID != "" {
porterCloudPlanID, err := uuid.Parse(p.Config().ServerConf.PorterCloudPlanID)
if err != nil {
err = telemetry.Error(ctx, span, err, "error parsing starter plan id")
p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
return
}

err = p.Config().BillingManager.MetronomeClient.EndCustomerPlan(proj.UsageID, porterCloudPlanID)
if err != nil {
e := "error deleting project in usage provider"
err = telemetry.Error(ctx, span, err, e)
p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
return
}

err = p.Config().BillingManager.MetronomeClient.DeleteCustomer(proj.UsageID)
if err != nil {
e := "error deleting project in usage provider"
err = telemetry.Error(ctx, span, err, e)
p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
return
}
}

err = p.Config().BillingManager.StripeClient.DeleteCustomer(ctx, proj)
if err != nil {
e := "error deleting project in billing provider"
err = telemetry.Error(ctx, span, err, e)
Expand Down
15 changes: 10 additions & 5 deletions api/server/shared/config/env/envconfs.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package env

import "time"
import (
"time"
)

// ServerConf is the server configuration
type ServerConf struct {
Expand Down Expand Up @@ -69,10 +71,13 @@ type ServerConf struct {
SendgridDeleteProjectTemplateID string `env:"SENDGRID_DELETE_PROJECT_TEMPLATE_ID"`
SendgridSenderEmail string `env:"SENDGRID_SENDER_EMAIL"`

StripeSecretKey string `env:"STRIPE_SECRET_KEY"`
StripePublishableKey string `env:"STRIPE_PUBLISHABLE_KEY"`
SlackClientID string `env:"SLACK_CLIENT_ID"`
SlackClientSecret string `env:"SLACK_CLIENT_SECRET"`
StripeSecretKey string `env:"STRIPE_SECRET_KEY"`
StripePublishableKey string `env:"STRIPE_PUBLISHABLE_KEY"`
MetronomeAPIKey string `env:"METRONOME_API_KEY"`
PorterCloudPlanID string `env:"PORTER_CLOUD_PLAN_ID"`

SlackClientID string `env:"SLACK_CLIENT_ID"`
SlackClientSecret string `env:"SLACK_CLIENT_SECRET"`

BillingPrivateKey string `env:"BILLING_PRIVATE_KEY"`
BillingPrivateServerURL string `env:"BILLING_PRIVATE_URL"`
Expand Down
12 changes: 9 additions & 3 deletions api/server/shared/config/loader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,11 @@ func sharedInit() {
panic(err)
}

InstanceBillingManager = &billing.StripeBillingManager{
StripeSecretKey: InstanceEnvConf.ServerConf.StripeSecretKey,
StripePublishableKey: InstanceEnvConf.ServerConf.StripePublishableKey,
stripeClient := billing.NewStripeClient(InstanceEnvConf.ServerConf.StripeSecretKey, InstanceEnvConf.ServerConf.StripePublishableKey)
metronomeClient := billing.NewMetronomeClient(InstanceEnvConf.ServerConf.MetronomeAPIKey)
InstanceBillingManager = billing.BillingManager{
StripeClient: stripeClient,
MetronomeClient: metronomeClient,
}
}

Expand Down Expand Up @@ -256,6 +258,10 @@ func (e *EnvConfigLoader) LoadConfig() (res *config.Config, err error) {
res.Logger.Info().Msg("STRIPE_SECRET_KEY not set, all Stripe functionality will be disabled")
}

if sc.MetronomeAPIKey == "" {
res.Logger.Info().Msg("METRONOME_API_KEY not set, all Metronome functionality will be disabled")
}

if sc.SlackClientID != "" && sc.SlackClientSecret != "" {
res.Logger.Info().Msg("Creating Slack client")
res.SlackConf = oauth.NewSlackClient(&oauth.Config{
Expand Down
51 changes: 51 additions & 0 deletions api/types/billing.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
package types

import (
"github.com/google/uuid"
)

// Stripe types

// PaymentMethod is a subset of the Stripe PaymentMethod type,
// with only the fields used in the dashboard
type PaymentMethod = struct {
Expand All @@ -10,3 +16,48 @@ type PaymentMethod = struct {
ExpYear int64 `json:"exp_year"`
Default bool `json:"is_default"`
}

// Metronome Types

// Customer represents a customer in Metronome
type Customer struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"` // Required. Name of the customer
Aliases []string `json:"ingest_aliases"` // Aliases that can be used to refer to this customer in usage events
BillingConfig BillingConfig `json:"billing_config,omitempty"`
CustomFields map[string]string `json:"custom_fields,omitempty"`
}

// CustomerArchiveRequest will archive the customer in Metronome.
type CustomerArchiveRequest struct {
CustomerID uuid.UUID `json:"id"`
}

// BillingConfig is the configuration for the billing provider (Stripe, etc.)
type BillingConfig struct {
BillingProviderType string `json:"billing_provider_type"` // Required. Can be any of "aws_marketplace", "stripe", "netsuite", "custom", "azure_marketplace", "quickbooks_online", or "workday"
BillingProviderCustomerID string `json:"billing_provider_customer_id"`
StripeCollectionMethod string `json:"stripe_collection_method"` // Can be any of "charge_automatically" or "send_invoice"
}

// AddCustomerPlanRequest represents a request to add a customer plan with specific details.
type AddCustomerPlanRequest struct {
PlanID uuid.UUID `json:"plan_id"` // Required. The customer ID, plan ID, and date range for the plan to be applied.
StartingOn string `json:"starting_on"` // Required. RFC 3339 timestamp for when the plan becomes active for this customer. Must be at 0:00 UTC (midnight).
EndingBefore string `json:"ending_before,omitempty"` // Optional. RFC 3339 timestamp for when the plan ends (exclusive) for this customer. Must be at 0:00 UTC (midnight).
NetPaymentTermsDays int `json:"net_payment_terms_days,omitempty"` // Number of days after issuance of invoice after which the invoice is due (e.g., Net 30).
}

// AddCustomerPlanResponse is a response to the add customer plan request. Returns customer-plan relationship id.
type AddCustomerPlanResponse struct {
Data struct {
CustomerPlanID uuid.UUID `json:"id"`
} `json:"data"`
}

// EndCustomerPlanRequest represents a request to end the plan for a specific customer.
type EndCustomerPlanRequest struct {
EndingBefore string `json:"ending_before,omitempty"` // RFC 3339 timestamp for when the plan ends (exclusive) for this customer. Must be at 0:00 UTC (midnight).
VoidInvoices bool `json:"void_invoices"` // If true, plan end date can be before the last finalized invoice date. Any invoices generated after the plan end date will be voided.
VoidStripeInvoices bool `json:"void_stripe_invoices"` // Will void Stripe invoices if VoidInvoices is set to true. Drafts will be deleted.
}
Loading

0 comments on commit 8bb2d3e

Please sign in to comment.