Skip to content

Commit

Permalink
Wrap up Metronome logic for granting credits
Browse files Browse the repository at this point in the history
  • Loading branch information
MauAraujo committed Apr 30, 2024
1 parent b7f856f commit 54aa00b
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 25 deletions.
10 changes: 7 additions & 3 deletions api/server/handlers/billing/credits.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package billing

import (
"net/http"
"time"

"github.com/porter-dev/porter/api/server/handlers"
"github.com/porter-dev/porter/api/server/shared"
Expand All @@ -18,10 +19,10 @@ const (
referralRewardRequirement = 5
// defaultRewardAmountUSD is the default amount in USD rewarded to users
// who reach the reward requirement
defaultRewardAmountUSD = 20
defaultRewardAmountCents = 2000
// defaultPaidAmountUSD is the amount paid by the user to get the credits
// grant, if set to 0 it means they were free
defaultPaidAmountUSD = 0
defaultPaidAmountCents = 0
)

// ListCreditsHandler is a handler for getting available credits
Expand Down Expand Up @@ -117,7 +118,10 @@ func (c *ClaimReferralRewardHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
}

if !user.ReferralRewardClaimed && referralCount >= referralRewardRequirement {
err := c.Config().BillingManager.MetronomeClient.CreateCreditsGrant(ctx, proj.UsageID, defaultRewardAmountUSD, defaultPaidAmountUSD)
// Metronome requires an expiration to be passed in, so we set it to 5 years which in
// practice will mean the credits will run out before expiring
expiresAt := time.Now().AddDate(5, 0, 0).Format(time.RFC3339)
err := c.Config().BillingManager.MetronomeClient.CreateCreditsGrant(ctx, proj.UsageID, defaultRewardAmountCents, defaultPaidAmountCents, expiresAt)
if err != nil {
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
return
Expand Down
30 changes: 22 additions & 8 deletions api/types/billing_metronome.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,13 @@ type EndCustomerPlanRequest struct {
// CreateCreditsGrantRequest is the request to create a credit grant for a customer
type CreateCreditsGrantRequest struct {
// CustomerID is the id of the customer
CustomerID uuid.UUID `json:"customer_id"`
UniquenessKey string `json:"uniqueness_key"`
GrantAmount GrantAmount `json:"grant_amount"`
PaidAmount PaidAmount `json:"paid_amount"`
Name string `json:"name"`
ExpiresAt string `json:"expires_at"`
Priority int `json:"priority"`
CustomerID uuid.UUID `json:"customer_id"`
UniquenessKey string `json:"uniqueness_key"`
GrantAmount GrantAmountID `json:"grant_amount"`
PaidAmount PaidAmount `json:"paid_amount"`
Name string `json:"name"`
ExpiresAt string `json:"expires_at"`
Priority int `json:"priority"`
}

// ListCreditGrantsRequest is the request to list a user's credit grants. Note that only one of
Expand Down Expand Up @@ -138,7 +138,15 @@ type CreditType struct {
ID string `json:"id"`
}

// GrantAmount represents the amount of credits granted
// GrantAmountID represents the amount of credits granted with the credit type ID
// for the create credits grant request
type GrantAmountID struct {
Amount float64 `json:"amount"`
CreditTypeID uuid.UUID `json:"credit_type_id"`
}

// GrantAmount represents the amount of credits granted with the credit type
// for the list credit grants response
type GrantAmount struct {
Amount float64 `json:"amount"`
CreditType CreditType `json:"credit_type"`
Expand All @@ -150,6 +158,12 @@ type PaidAmount struct {
CreditTypeID uuid.UUID `json:"credit_type_id"`
}

type PricingUnit struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
IsCurrency bool `json:"is_currency"`
}

// Balance represents the effective balance of the grant as of the end of the customer's
// current billing period.
type Balance struct {
Expand Down
1 change: 1 addition & 0 deletions api/types/referral.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package types

// Referral is a struct that represents a referral in the Porter API
type Referral struct {
ID uint `json:"id"`
// Code is the referral code that is shared with the referred user
Expand Down
55 changes: 42 additions & 13 deletions internal/billing/metronome.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,8 @@ func (m MetronomeClient) ListCustomerCredits(ctx context.Context, customerID uui
return response, nil
}

func (m MetronomeClient) CreateCreditsGrant(ctx context.Context, customerID uuid.UUID, grantAmount float64, paidAmount float64) (err error) {
// CreateCreditsGrant will create a new credit grant for the customer with the specified amount
func (m MetronomeClient) CreateCreditsGrant(ctx context.Context, customerID uuid.UUID, grantAmount float64, paidAmount float64, expiresAt string) (err error) {
ctx, span := telemetry.NewSpan(ctx, "create-credits-grant")
defer span.End()

Expand All @@ -251,29 +252,33 @@ func (m MetronomeClient) CreateCreditsGrant(ctx context.Context, customerID uuid
}

path := "credits/createGrant"
creditTypeID, err := m.getCreditTypeID(ctx, "USD (cents)")
if err != nil {
return telemetry.Error(ctx, span, err, "failed to get credit type id")
}

// Uniqueness key is used to prevent duplicate grants
uniquenessKey := fmt.Sprintf("%s-referral-reward", customerID)

req := types.CreateCreditsGrantRequest{
CustomerID: customerID,
UniquenessKey: "porter-credits",
GrantAmount: types.GrantAmount{
Amount: grantAmount,
CreditType: types.CreditType{},
UniquenessKey: uniquenessKey,
GrantAmount: types.GrantAmountID{
Amount: grantAmount,
CreditTypeID: creditTypeID,
},
PaidAmount: types.PaidAmount{
Amount: paidAmount,
CreditTypeID: uuid.UUID{},
CreditTypeID: creditTypeID,
},
Name: "Porter Credits",
ExpiresAt: "", // never expires
ExpiresAt: expiresAt,
Priority: 1,
}

var result struct {
Data []types.CreditGrant `json:"data"`
}

_, err = m.do(http.MethodPost, path, req, &result)
if err != nil {
statusCode, err := m.do(http.MethodPost, path, req, nil)
if err != nil && statusCode != http.StatusConflict {
// a conflict response indicates the grant already exists
return telemetry.Error(ctx, span, err, "failed to create credits grant")
}

Expand Down Expand Up @@ -397,6 +402,30 @@ func (m MetronomeClient) listBillableMetricIDs(ctx context.Context, customerID u
return result.Data, nil
}

func (m MetronomeClient) getCreditTypeID(ctx context.Context, currencyCode string) (creditTypeID uuid.UUID, err error) {
ctx, span := telemetry.NewSpan(ctx, "get-credit-type-id")
defer span.End()

path := "/credit-types/list"

var result struct {
Data []types.PricingUnit `json:"data"`
}

_, err = m.do(http.MethodGet, path, nil, &result)
if err != nil {
return creditTypeID, telemetry.Error(ctx, span, err, "failed to retrieve billable metrics from metronome")
}

for _, pricingUnit := range result.Data {
if pricingUnit.Name == currencyCode {
return pricingUnit.ID, nil
}
}

return creditTypeID, telemetry.Error(ctx, span, fmt.Errorf("credit type not found for currency code %s", currencyCode), "failed to find credit type")
}

func (m MetronomeClient) do(method string, path string, body interface{}, data interface{}) (statusCode int, err error) {
client := http.Client{}
endpoint, err := url.JoinPath(metronomeBaseUrl, path)
Expand Down
1 change: 1 addition & 0 deletions internal/models/referral.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Referral struct {
Status string
}

// NewReferralCode generates a new referral code
func NewReferralCode() string {
return shortuuid.New()
}
Expand Down
3 changes: 2 additions & 1 deletion internal/repository/gorm/referrals.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func NewReferralRepository(db *gorm.DB) repository.ReferralRepository {
return &ReferralRepository{db}
}

// CreateInvite creates a new invite
// CreateReferral creates a new referral in the database
func (repo *ReferralRepository) CreateReferral(referral *models.Referral) (*models.Referral, error) {
user := &models.User{}

Expand All @@ -38,6 +38,7 @@ func (repo *ReferralRepository) CreateReferral(referral *models.Referral) (*mode
return referral, nil
}

// GetReferralByCode returns the number of referrals a user has made
func (repo *ReferralRepository) GetReferralCountByUserID(userID uint) (int, error) {
referrals := []models.Referral{}
if err := repo.db.Where("user_id = ?", userID).Find(&referrals).Error; err != nil {
Expand Down

0 comments on commit 54aa00b

Please sign in to comment.