Skip to content

Commit

Permalink
Fix wallets and referral credits
Browse files Browse the repository at this point in the history
  • Loading branch information
MauAraujo committed May 15, 2024
1 parent b972c65 commit 99dfe64
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 63 deletions.
10 changes: 5 additions & 5 deletions api/server/handlers/project/referrals.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ func (c *GetProjectReferralDetailsHandler) ServeHTTP(w http.ResponseWriter, r *h

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

telemetry.WithAttributes(span,
telemetry.AttributeKV{Key: "lago-config-exists", Value: c.Config().BillingManager.LagoConfigLoaded},
telemetry.AttributeKV{Key: "lago-enabled", Value: proj.GetFeatureFlag(models.LagoEnabled, c.Config().LaunchDarklyClient)},
)

if !c.Config().BillingManager.LagoConfigLoaded || !proj.GetFeatureFlag(models.LagoEnabled, c.Config().LaunchDarklyClient) || !proj.EnableSandbox {
c.WriteResult(w, r, "")

telemetry.WithAttributes(span,
telemetry.AttributeKV{Key: "lago-config-exists", Value: c.Config().BillingManager.LagoConfigLoaded},
telemetry.AttributeKV{Key: "lago-enabled", Value: proj.GetFeatureFlag(models.LagoEnabled, c.Config().LaunchDarklyClient)},
)
return
}

Expand Down
12 changes: 8 additions & 4 deletions api/types/billing_usage.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package types

import "github.com/google/uuid"

// ListCreditGrantsResponse returns the total remaining and granted credits for a customer.
type ListCreditGrantsResponse struct {
RemainingBalanceCents int `json:"remaining_credits"`
Expand Down Expand Up @@ -60,10 +62,12 @@ type BillingEvent struct {

// Wallet represents a customer credits wallet
type Wallet struct {
Status string `json:"status"`
BalanceCents int `json:"balance_cents,omitempty"`
OngoingBalanceCents int `json:"ongoing_balance_cents,omitempty"`
OngoingUsageBalanceCents int `json:"ongoing_usage_balance_cents,omitempty"`
LagoID uuid.UUID `json:"lago_id,omitempty"`
Status string `json:"status"`
BalanceCents int `json:"balance_cents,omitempty"`
CreditsOngoingBalance string `json:"credits_ongoing_balance,omitempty"`
OngoingBalanceCents int `json:"ongoing_balance_cents,omitempty"`
OngoingUsageBalanceCents int `json:"ongoing_usage_balance_cents,omitempty"`
}

// Invoice represents an invoice in the billing system.
Expand Down
7 changes: 6 additions & 1 deletion dashboard/src/main/home/project-settings/UsagePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ function UsagePage(): JSX.Element {

const totalCost = periodUsage?.total_amount_cents
? (periodUsage.total_amount_cents / 100).toFixed(4)
: "";
: "0";
const totalCpuHours =
periodUsage?.charges_usage.find((x) =>
x.billable_metric.name.includes("CPU")
Expand All @@ -88,6 +88,11 @@ function UsagePage(): JSX.Element {
x.billable_metric.name.includes("GiB")
)?.units ?? "";
const currency = periodUsage?.charges_usage[0].amount_currency ?? "";

if (totalCpuHours === "" || totalGibHours === "") {
return null;
}

return {
total_cost: totalCost,
total_cpu_hours: totalCpuHours,
Expand Down
129 changes: 76 additions & 53 deletions internal/billing/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,10 @@ func (m LagoClient) CreateCustomerWithPlan(ctx context.Context, userEmail string
return telemetry.Error(ctx, span, err, fmt.Sprintf("error while adding customer to plan %s", m.PorterCloudPlanCode))
}

starterWalletName := "Free Starter Credits"
walletName := "Porter Credits"
expiresAt := time.Now().UTC().AddDate(0, 1, 0).Truncate(24 * time.Hour)

err = m.CreateCreditsGrant(ctx, projectID, starterWalletName, defaultStarterCreditsCents, &expiresAt, sandboxEnabled)
err = m.CreateCreditsGrant(ctx, projectID, walletName, defaultStarterCreditsCents, &expiresAt, sandboxEnabled)
if err != nil {
return telemetry.Error(ctx, span, err, "error while creating starter credits grant")
}
Expand Down Expand Up @@ -216,34 +216,13 @@ func (m LagoClient) ListCustomerCredits(ctx context.Context, projectID uint, san
}
customerID := m.generateLagoID(CustomerIDPrefix, projectID, sandboxEnabled)

// We manually do the request in this function because the Lago client has an issue
// with types for this specific request
url := fmt.Sprintf("%s/api/v1/wallets?external_customer_id=%s", lagoBaseURL, customerID)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return credits, telemetry.Error(ctx, span, err, "failed to create wallets request")
}

req.Header.Set("Authorization", "Bearer "+m.lagoApiKey)

client := &http.Client{}
resp, err := client.Do(req)
walletList, err := m.listCustomerWallets(ctx, customerID)
if err != nil {
return credits, telemetry.Error(ctx, span, err, "failed to get customer credits")
}

type ListWalletsResponse struct {
Wallets []types.Wallet `json:"wallets"`
}

var walletList ListWalletsResponse
err = json.NewDecoder(resp.Body).Decode(&walletList)
if err != nil {
return credits, telemetry.Error(ctx, span, err, "failed to decode wallet list response")
return credits, telemetry.Error(ctx, span, err, "failed to list customer wallets")
}

var response types.ListCreditGrantsResponse
for _, wallet := range walletList.Wallets {
for _, wallet := range walletList {
if wallet.Status != string(lago.Active) {
continue
}
Expand All @@ -252,11 +231,6 @@ func (m LagoClient) ListCustomerCredits(ctx context.Context, projectID uint, san
response.RemainingBalanceCents += wallet.OngoingBalanceCents
}

err = resp.Body.Close()
if err != nil {
return credits, telemetry.Error(ctx, span, err, "failed to close response body")
}

return response, nil
}

Expand All @@ -270,19 +244,42 @@ func (m LagoClient) CreateCreditsGrant(ctx context.Context, projectID uint, name
}

customerID := m.generateLagoID(CustomerIDPrefix, projectID, sandboxEnabled)
walletInput := &lago.WalletInput{
ExternalCustomerID: customerID,
Name: name,
Currency: lago.USD,
GrantedCredits: strconv.FormatInt(grantAmount, 10),
// Rate is 1 credit = 1 cent
RateAmount: "0.01",
ExpirationAt: expiresAt,

walletList, err := m.listCustomerWallets(ctx, customerID)
if err != nil {
return telemetry.Error(ctx, span, err, "failed to list customer wallets")
}

if len(walletList) == 0 {
walletInput := &lago.WalletInput{
ExternalCustomerID: customerID,
Name: name,
Currency: lago.USD,
GrantedCredits: strconv.FormatInt(grantAmount, 10),
// Rate is 1 credit = 1 cent
RateAmount: "0.01",
ExpirationAt: expiresAt,
}

_, lagoErr := m.client.Wallet().Create(ctx, walletInput)
if lagoErr != nil {
return telemetry.Error(ctx, span, fmt.Errorf(lagoErr.ErrorCode), "failed to create credits grant")
}

return nil
}

_, lagoErr := m.client.Wallet().Create(ctx, walletInput)
// Currently only one wallet per customer is supported in Lago
wallet := walletList[0]
walletTransactionInput := &lago.WalletTransactionInput{
WalletID: wallet.LagoID.String(),
GrantedCredits: strconv.FormatInt(grantAmount, 10),
}

// If the wallet already exists, we need to update the balance
_, lagoErr := m.client.WalletTransaction().Create(ctx, walletTransactionInput)
if lagoErr != nil {
return telemetry.Error(ctx, span, fmt.Errorf(lagoErr.ErrorCode), "failed to create credits grant")
return telemetry.Error(ctx, span, fmt.Errorf(lagoErr.ErrorCode), "failed to update credits grant")
}

return nil
Expand Down Expand Up @@ -360,20 +357,9 @@ func (m LagoClient) IngestEvents(ctx context.Context, subscriptionID string, eve
batch := events[i:end]
var batchInput []lago.EventInput
for i := range batch {
externalSubscriptionID := subscriptionID
if enableSandbox {
// This hack has to be done because we can't infer the project id from the
// context in Porter Cloud
customerID, err := strconv.ParseUint(batch[i].CustomerID, 10, 64)
if err != nil {
return telemetry.Error(ctx, span, err, "failed to parse customer ID")
}
externalSubscriptionID = m.generateLagoID(SubscriptionIDPrefix, uint(customerID), enableSandbox)
}

event := lago.EventInput{
TransactionID: batch[i].TransactionID,
ExternalSubscriptionID: externalSubscriptionID,
ExternalSubscriptionID: subscriptionID,
Code: batch[i].EventType,
Properties: batch[i].Properties,
}
Expand Down Expand Up @@ -492,6 +478,43 @@ func (m LagoClient) addCustomerPlan(ctx context.Context, customerID string, plan
return nil
}

func (m LagoClient) listCustomerWallets(ctx context.Context, customerID string) (walletList []types.Wallet, err error) {
ctx, span := telemetry.NewSpan(ctx, "list-lago-customer-wallets")
defer span.End()

// We manually do the request in this function because the Lago client has an issue
// with types for this specific request
url := fmt.Sprintf("%s/api/v1/wallets?external_customer_id=%s", lagoBaseURL, customerID)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return walletList, telemetry.Error(ctx, span, err, "failed to create wallets list request")
}

req.Header.Set("Authorization", "Bearer "+m.lagoApiKey)

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return walletList, telemetry.Error(ctx, span, err, "failed to get customer credits")
}

response := struct {
Wallets []types.Wallet `json:"wallets"`
}{}

err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil {
return walletList, telemetry.Error(ctx, span, err, "failed to decode wallet list response")
}

err = resp.Body.Close()
if err != nil {
return walletList, telemetry.Error(ctx, span, err, "failed to close response body")
}

return response.Wallets, nil
}

func createUsageFromLagoUsage(lagoUsage lago.CustomerUsage) types.Usage {
usage := types.Usage{}
usage.FromDatetime = lagoUsage.FromDatetime.Format(time.RFC3339)
Expand Down

0 comments on commit 99dfe64

Please sign in to comment.