| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651 |
- package billing
- import (
- "context"
- "encoding/json"
- "fmt"
- "net/http"
- "strconv"
- "strings"
- "time"
- "github.com/getlago/lago-go-client"
- "github.com/porter-dev/porter/api/types"
- "github.com/porter-dev/porter/internal/telemetry"
- )
- const (
- lagoBaseURL = "https://api.getlago.com"
- defaultStarterCreditsCents = 500
- defaultRewardAmountCents = 1000
- maxReferralRewards = 10
- defaultMaxRetries = 10
- maxIngestEventLimit = 100
- // porterStandardTrialDays is the number of days for the trial
- porterStandardTrialDays = 15
- // These prefixes are used to build the customer and subscription IDs
- // in Lago. This way we can reuse the project IDs instead of storing
- // the Lago IDs in the database.
- // TrialIDPrefix is the prefix for the trial ID
- TrialIDPrefix = "trial"
- // SubscriptionIDPrefix is the prefix for the subscription ID
- SubscriptionIDPrefix = "sub"
- // CustomerIDPrefix is the prefix for the customer ID
- CustomerIDPrefix = "cus"
- )
- // LagoClient is the client used to call the Lago API
- type LagoClient struct {
- client lago.Client
- lagoApiKey string
- PorterCloudPlanCode string
- PorterStandardPlanCode string
- PorterTrialCode string
- // DefaultRewardAmountCents is the default amount in USD cents rewarded to users
- // who successfully refer a new user
- DefaultRewardAmountCents int64
- // MaxReferralRewards is the maximum number of referral rewards a user can receive
- MaxReferralRewards int64
- }
- // NewLagoClient returns a new Lago client
- func NewLagoClient(lagoApiKey string, porterCloudPlanCode string, porterStandardPlanCode string, porterTrialCode string) (client LagoClient, err error) {
- lagoClient := lago.New().SetApiKey(lagoApiKey)
- if lagoClient == nil {
- return client, fmt.Errorf("failed to create lago client")
- }
- return LagoClient{
- lagoApiKey: lagoApiKey,
- client: *lagoClient,
- PorterCloudPlanCode: porterCloudPlanCode,
- PorterStandardPlanCode: porterStandardPlanCode,
- PorterTrialCode: porterTrialCode,
- DefaultRewardAmountCents: defaultRewardAmountCents,
- MaxReferralRewards: maxReferralRewards,
- }, nil
- }
- // CreateCustomerWithPlan will create the customer in Lago and immediately add it to the plan
- func (m LagoClient) CreateCustomerWithPlan(ctx context.Context, userEmail string, projectName string, projectID uint, billingID string, sandboxEnabled bool) (err error) {
- ctx, span := telemetry.NewSpan(ctx, "add-lago-customer-plan")
- defer span.End()
- if projectID == 0 {
- return telemetry.Error(ctx, span, err, "project id empty")
- }
- customerID, err := m.createCustomer(ctx, userEmail, projectName, projectID, billingID, sandboxEnabled)
- if err != nil {
- return telemetry.Error(ctx, span, err, "error while creating customer")
- }
- trialID := m.generateLagoID(TrialIDPrefix, projectID, sandboxEnabled)
- subscriptionID := m.generateLagoID(SubscriptionIDPrefix, projectID, sandboxEnabled)
- // The dates need to be at midnight UTC
- now := time.Now().UTC()
- now = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
- trialEndTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC).Add(time.Hour * 24 * porterStandardTrialDays).UTC()
- if sandboxEnabled {
- err = m.addCustomerPlan(ctx, customerID, m.PorterCloudPlanCode, subscriptionID, &now, nil)
- if err != nil {
- return telemetry.Error(ctx, span, err, fmt.Sprintf("error while adding customer to plan %s", m.PorterCloudPlanCode))
- }
- walletName := "Porter Credits"
- err = m.CreateCreditsGrant(ctx, projectID, walletName, defaultStarterCreditsCents, sandboxEnabled)
- if err != nil {
- return telemetry.Error(ctx, span, err, "error while creating starter credits grant")
- }
- return nil
- }
- // First, start the new customer on the trial
- err = m.addCustomerPlan(ctx, customerID, m.PorterTrialCode, trialID, &now, &trialEndTime)
- if err != nil {
- return telemetry.Error(ctx, span, err, fmt.Sprintf("error while starting customer trial %s", m.PorterTrialCode))
- }
- // Then, add the customer to the actual plan. The date of the subscription will be the end of the trial
- err = m.addCustomerPlan(ctx, customerID, m.PorterStandardPlanCode, subscriptionID, &trialEndTime, nil)
- if err != nil {
- return telemetry.Error(ctx, span, err, fmt.Sprintf("error while adding customer to plan %s", m.PorterStandardPlanCode))
- }
- return err
- }
- // CheckIfCustomerExists will check if the customer exists in Lago
- func (m LagoClient) CheckIfCustomerExists(ctx context.Context, projectID uint, enableSandbox bool) (exists bool, err error) {
- ctx, span := telemetry.NewSpan(ctx, "check-lago-customer-exists")
- defer span.End()
- if projectID == 0 {
- return exists, telemetry.Error(ctx, span, err, "project id empty")
- }
- customerID := m.generateLagoID(CustomerIDPrefix, projectID, enableSandbox)
- _, lagoErr := m.client.Customer().Get(ctx, customerID)
- if lagoErr != nil {
- if lagoErr.ErrorCode == "customer_not_found" {
- return false, nil
- }
- return exists, telemetry.Error(ctx, span, lagoErr.Err, "failed to get customer")
- }
- return true, nil
- }
- // GetCustomerActivePlan will return the active plan for the customer
- func (m LagoClient) GetCustomerActivePlan(ctx context.Context, projectID uint, sandboxEnabled bool) (plan types.Plan, err error) {
- ctx, span := telemetry.NewSpan(ctx, "get-active-subscription")
- defer span.End()
- if projectID == 0 {
- return plan, telemetry.Error(ctx, span, err, "project id empty")
- }
- customerID := m.generateLagoID(CustomerIDPrefix, projectID, sandboxEnabled)
- telemetry.WithAttributes(span,
- telemetry.AttributeKV{Key: "customer_id", Value: customerID},
- )
- activeSubscriptions, err := m.getCustomerActiveSubscription(ctx, customerID)
- if err != nil {
- return plan, telemetry.Error(ctx, span, err, "failed to get active subscriptions")
- }
- if activeSubscriptions == nil {
- return plan, telemetry.Error(ctx, span, err, "no active subscriptions found")
- }
- for _, subscription := range activeSubscriptions {
- if subscription.Status != string(lago.SubscriptionStatusActive) {
- continue
- }
- plan.ID = subscription.ExternalID
- plan.CustomerID = subscription.ExternalCustomerID
- plan.StartingOn = subscription.SubscriptionAt
- if subscription.EndingAt != "" {
- plan.EndingBefore = subscription.EndingAt
- }
- if strings.Contains(subscription.ExternalID, TrialIDPrefix) {
- plan.TrialInfo.EndingBefore = subscription.EndingAt
- }
- break
- }
- return plan, nil
- }
- // DeleteCustomer will delete the customer and terminate all subscriptions
- func (m LagoClient) DeleteCustomer(ctx context.Context, projectID uint, sandboxEnabled bool) (err error) {
- ctx, span := telemetry.NewSpan(ctx, "delete-lago-customer")
- defer span.End()
- if projectID == 0 {
- return telemetry.Error(ctx, span, err, "subscription id empty")
- }
- customerID := m.generateLagoID(CustomerIDPrefix, projectID, sandboxEnabled)
- _, lagoErr := m.client.Customer().Delete(ctx, customerID)
- if lagoErr != nil {
- return telemetry.Error(ctx, span, lagoErr.Err, "failed to terminate subscription")
- }
- return nil
- }
- // ListCustomerCredits will return the total number of credits for the customer
- func (m LagoClient) ListCustomerCredits(ctx context.Context, projectID uint, sandboxEnabled bool) (credits types.ListCreditGrantsResponse, err error) {
- ctx, span := telemetry.NewSpan(ctx, "list-customer-credits")
- defer span.End()
- if projectID == 0 {
- return credits, telemetry.Error(ctx, span, err, "project id empty")
- }
- customerID := m.generateLagoID(CustomerIDPrefix, projectID, sandboxEnabled)
- walletList, err := m.listCustomerWallets(ctx, customerID)
- if err != nil {
- return credits, telemetry.Error(ctx, span, err, "failed to list customer wallets")
- }
- var response types.ListCreditGrantsResponse
- for _, wallet := range walletList {
- if wallet.Status != string(lago.Active) {
- continue
- }
- response.GrantedBalanceCents += wallet.BalanceCents
- response.RemainingBalanceCents += wallet.OngoingBalanceCents
- }
- return response, nil
- }
- // CheckCustomerCouponExpiration will return the expiration date of the customer's coupon
- func (m LagoClient) CheckCustomerCouponExpiration(ctx context.Context, projectID uint, sandboxEnabled bool) (trialEndDate string, err error) {
- ctx, span := telemetry.NewSpan(ctx, "list-customer-coupons")
- defer span.End()
- if projectID == 0 {
- return trialEndDate, telemetry.Error(ctx, span, err, "project id empty")
- }
- customerID := m.generateLagoID(CustomerIDPrefix, projectID, sandboxEnabled)
- couponList, err := m.listCustomerAppliedCoupons(ctx, customerID)
- if err != nil {
- return trialEndDate, telemetry.Error(ctx, span, err, "failed to list customer coupons")
- }
- if len(couponList) == 0 {
- return trialEndDate, nil
- }
- appliedCoupon := couponList[0]
- trialEndDate = time.Now().UTC().AddDate(0, appliedCoupon.FrequencyDurationRemaining, 0).Format(time.RFC3339)
- return trialEndDate, nil
- }
- // CreateCreditsGrant will create a new credit grant for the customer with the specified amount
- func (m LagoClient) CreateCreditsGrant(ctx context.Context, projectID uint, name string, grantAmount int64, sandboxEnabled bool) (err error) {
- ctx, span := telemetry.NewSpan(ctx, "create-credits-grant")
- defer span.End()
- if projectID == 0 {
- return telemetry.Error(ctx, span, err, "project id empty")
- }
- customerID := m.generateLagoID(CustomerIDPrefix, projectID, sandboxEnabled)
- 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",
- }
- _, lagoErr := m.client.Wallet().Create(ctx, walletInput)
- if lagoErr != nil {
- return telemetry.Error(ctx, span, lagoErr.Err, "failed to create wallet")
- }
- return nil
- }
- // 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, lagoErr.Err, "failed to update credits grant")
- }
- return nil
- }
- // ListCustomerUsage will return the aggregated usage for a customer
- func (m LagoClient) ListCustomerUsage(ctx context.Context, customerID string, subscriptionID string, currentPeriod bool, previousPeriods int) (usageList []types.Usage, err error) {
- ctx, span := telemetry.NewSpan(ctx, "list-customer-usage")
- defer span.End()
- if subscriptionID == "" {
- return usageList, telemetry.Error(ctx, span, err, "subscription id empty")
- }
- if currentPeriod {
- customerUsageInput := &lago.CustomerUsageInput{
- ExternalSubscriptionID: subscriptionID,
- }
- currentUsage, lagoErr := m.client.Customer().CurrentUsage(ctx, customerID, customerUsageInput)
- if lagoErr != nil {
- return usageList, telemetry.Error(ctx, span, lagoErr.Err, "failed to get customer usage")
- }
- if currentUsage == nil {
- return usageList, nil
- }
- usage := createUsageFromLagoUsage(*currentUsage)
- usageList = append(usageList, usage)
- } else {
- url := fmt.Sprintf("%s/api/v1/customers/%s/past_usage?external_subscription_id=%s&periods_count=%d", lagoBaseURL, customerID, subscriptionID, previousPeriods)
- req, err := http.NewRequest("GET", url, nil)
- if err != nil {
- return usageList, 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)
- if err != nil {
- return usageList, telemetry.Error(ctx, span, err, "failed to get customer credits")
- }
- var previousUsage lago.CustomerPastUsageResult
- err = json.NewDecoder(resp.Body).Decode(&previousUsage)
- if err != nil {
- return usageList, telemetry.Error(ctx, span, err, "failed to decode usage list response")
- }
- for _, pastUsage := range previousUsage.UsagePeriods {
- usage := createUsageFromLagoUsage(pastUsage)
- usageList = append(usageList, usage)
- }
- }
- return usageList, nil
- }
- // IngestEvents sends a list of billing events to Lago's ingest endpoint
- func (m LagoClient) IngestEvents(ctx context.Context, subscriptionID string, events []types.BillingEvent, enableSandbox bool) (err error) {
- ctx, span := telemetry.NewSpan(ctx, "ingest-billing-events")
- defer span.End()
- if len(events) == 0 {
- return nil
- }
- for i := 0; i < len(events); i += maxIngestEventLimit {
- end := i + maxIngestEventLimit
- if end > len(events) {
- end = len(events)
- }
- batch := events[i:end]
- var batchInput []lago.EventInput
- for i := range batch {
- projectID, err := strconv.ParseUint(batch[i].CustomerID, 10, 64)
- if err != nil {
- return telemetry.Error(ctx, span, err, "failed to parse project id")
- }
- if enableSandbox {
- // For Porter Cloud, we can't infer the project ID from the request, so we
- // instead use the one in the billing event
- subscriptionID = m.generateLagoID(SubscriptionIDPrefix, uint(projectID), enableSandbox)
- }
- event := lago.EventInput{
- TransactionID: batch[i].TransactionID,
- ExternalSubscriptionID: subscriptionID,
- Code: batch[i].EventType,
- Properties: batch[i].Properties,
- }
- batchInput = append(batchInput, event)
- }
- // Retry each batch to make sure all events are ingested
- var currentAttempts int
- for currentAttempts := 0; currentAttempts < defaultMaxRetries; currentAttempts++ {
- _, lagoErr := m.client.Event().Batch(ctx, &batchInput)
- if lagoErr == nil {
- return nil
- }
- }
- if currentAttempts == defaultMaxRetries {
- return telemetry.Error(ctx, span, err, "max number of retry attempts reached with no success")
- }
- }
- return nil
- }
- // ListCustomerFinalizedInvoices will return all finalized invoices for the customer
- func (m LagoClient) ListCustomerFinalizedInvoices(ctx context.Context, projectID uint, enableSandbox bool) (invoiceList []types.Invoice, err error) {
- ctx, span := telemetry.NewSpan(ctx, "list-customer-invoices")
- defer span.End()
- if projectID == 0 {
- return invoiceList, telemetry.Error(ctx, span, err, "project id cannot be empty")
- }
- customerID := m.generateLagoID(CustomerIDPrefix, projectID, enableSandbox)
- invoiceListInput := &lago.InvoiceListInput{
- ExternalCustomerID: customerID,
- Status: lago.InvoiceStatusFinalized,
- }
- invoices, lagoErr := m.client.Invoice().GetList(ctx, invoiceListInput)
- if lagoErr != nil {
- return invoiceList, telemetry.Error(ctx, span, lagoErr.Err, "failed to list invoices")
- }
- for _, invoice := range invoices.Invoices {
- invoiceReq, lagoErr := m.client.Invoice().Download(ctx, invoice.LagoID.String())
- if lagoErr != nil {
- return invoiceList, telemetry.Error(ctx, span, lagoErr.Err, "failed to download invoice")
- }
- var fileURL string
- if invoiceReq == nil {
- fileURL = invoice.FileURL
- } else {
- fileURL = invoiceReq.FileURL
- }
- invoiceList = append(invoiceList, types.Invoice{
- HostedInvoiceURL: fileURL,
- Status: string(invoice.Status),
- Created: invoice.IssuingDate,
- })
- }
- return invoiceList, nil
- }
- // createCustomer will create the customer in Lago
- func (m LagoClient) createCustomer(ctx context.Context, userEmail string, projectName string, projectID uint, billingID string, sandboxEnabled bool) (customerID string, err error) {
- ctx, span := telemetry.NewSpan(ctx, "create-lago-customer")
- defer span.End()
- customerID = m.generateLagoID(CustomerIDPrefix, projectID, sandboxEnabled)
- customerInput := &lago.CustomerInput{
- ExternalID: customerID,
- Name: projectName,
- Email: userEmail,
- BillingConfiguration: lago.CustomerBillingConfigurationInput{
- PaymentProvider: lago.PaymentProviderStripe,
- ProviderCustomerID: billingID,
- Sync: false,
- SyncWithProvider: false,
- },
- }
- _, lagoErr := m.client.Customer().Create(ctx, customerInput)
- if lagoErr != nil {
- return customerID, telemetry.Error(ctx, span, lagoErr.Err, "failed to create lago customer")
- }
- return customerID, nil
- }
- // addCustomerPlan will create a plan subscription for the customer
- func (m LagoClient) addCustomerPlan(ctx context.Context, customerID string, planID string, subscriptionID string, startingAt *time.Time, endingAt *time.Time) (err error) {
- ctx, span := telemetry.NewSpan(ctx, "add-lago-customer-plan")
- defer span.End()
- if customerID == "" || planID == "" {
- return telemetry.Error(ctx, span, err, "project and plan id are required")
- }
- subscriptionInput := &lago.SubscriptionInput{
- ExternalCustomerID: customerID,
- ExternalID: subscriptionID,
- PlanCode: planID,
- SubscriptionAt: startingAt,
- EndingAt: endingAt,
- BillingTime: lago.Calendar,
- }
- _, lagoErr := m.client.Subscription().Create(ctx, subscriptionInput)
- if lagoErr != nil {
- return telemetry.Error(ctx, span, lagoErr.Err, "failed to create subscription")
- }
- return nil
- }
- func (m LagoClient) getCustomerActiveSubscription(ctx context.Context, customerID string) (subscriptions []types.Subscription, err error) {
- ctx, span := telemetry.NewSpan(ctx, "list-customer-active-subscriptions")
- defer span.End()
- url := fmt.Sprintf("%s/api/v1/subscriptions?external_customer_id=%s&status[]=%s", lagoBaseURL, customerID, lago.SubscriptionStatusActive)
- req, err := http.NewRequest("GET", url, nil)
- if err != nil {
- return subscriptions, telemetry.Error(ctx, span, err, "failed to create list subscriptions request")
- }
- req.Header.Set("Authorization", "Bearer "+m.lagoApiKey)
- client := &http.Client{}
- resp, err := client.Do(req)
- if err != nil {
- return subscriptions, telemetry.Error(ctx, span, err, "failed to get customer subscriptions")
- }
- var response struct {
- Subscriptions []types.Subscription `json:"subscriptions"`
- }
- err = json.NewDecoder(resp.Body).Decode(&response)
- if err != nil {
- return subscriptions, telemetry.Error(ctx, span, err, "failed to decode subscriptions list response")
- }
- err = resp.Body.Close()
- if err != nil {
- return subscriptions, telemetry.Error(ctx, span, err, "failed to close response body")
- }
- return response.Subscriptions, 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 wallets")
- }
- 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 (m LagoClient) listCustomerAppliedCoupons(ctx context.Context, customerID string) (couponList []types.AppliedCoupon, err error) {
- ctx, span := telemetry.NewSpan(ctx, "list-lago-customer-coupons")
- 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/applied_coupons?external_customer_id=%s&status=%s", lagoBaseURL, customerID, lago.AppliedCouponStatusActive)
- req, err := http.NewRequest("GET", url, nil)
- if err != nil {
- return couponList, telemetry.Error(ctx, span, err, "failed to create coupons list request")
- }
- req.Header.Set("Authorization", "Bearer "+m.lagoApiKey)
- client := &http.Client{}
- resp, err := client.Do(req)
- if err != nil {
- return couponList, telemetry.Error(ctx, span, err, "failed to get customer coupons")
- }
- response := struct {
- AppliedCoupons []types.AppliedCoupon `json:"applied_coupons"`
- }{}
- err = json.NewDecoder(resp.Body).Decode(&response)
- if err != nil {
- return couponList, telemetry.Error(ctx, span, err, "failed to decode coupons list response")
- }
- err = resp.Body.Close()
- if err != nil {
- return couponList, telemetry.Error(ctx, span, err, "failed to close response body")
- }
- return response.AppliedCoupons, nil
- }
- func createUsageFromLagoUsage(lagoUsage lago.CustomerUsage) types.Usage {
- usage := types.Usage{}
- usage.FromDatetime = lagoUsage.FromDatetime.Format(time.RFC3339)
- usage.ToDatetime = lagoUsage.ToDatetime.Format(time.RFC3339)
- usage.TotalAmountCents = int64(lagoUsage.TotalAmountCents)
- usage.ChargesUsage = make([]types.ChargeUsage, len(lagoUsage.ChargesUsage))
- for i, charge := range lagoUsage.ChargesUsage {
- usage.ChargesUsage[i] = types.ChargeUsage{
- Units: charge.Units,
- AmountCents: int64(charge.AmountCents),
- AmountCurrency: string(charge.AmountCurrency),
- BillableMetric: types.BillableMetric{
- Name: charge.BillableMetric.Name,
- },
- }
- }
- return usage
- }
- func (m LagoClient) generateLagoID(prefix string, projectID uint, sandboxEnabled bool) string {
- if sandboxEnabled {
- return fmt.Sprintf("cloud_%s_%d", prefix, projectID)
- }
- return fmt.Sprintf("%s_%d", prefix, projectID)
- }
|